diff --git a/vite-app/src/App.tsx b/vite-app/src/App.tsx index f322333d..a86df02a 100644 --- a/vite-app/src/App.tsx +++ b/vite-app/src/App.tsx @@ -4,6 +4,7 @@ import { observer } from "mobx-react"; import Dashboard from "./components/Dashboard"; import Button from "./components/Button"; import StatusIndicator from "./components/StatusIndicator"; +import { ChatWindow } from "./components/ChatWindow"; import { EvaluationRowSchema, type EvaluationRow } from "./types/eval-protocol"; import { WebSocketServerMessageSchema } from "./types/websocket"; import { GlobalState } from "./GlobalState"; @@ -137,7 +138,7 @@ const App = observer(() => { return (
-
- - } /> - } - /> - } - /> - +
+
+ {/* Left side - Main content (2/3 width) - Hidden on small screens */} +
+ + } /> + } + /> + } + /> + +
+ + {/* Right side - Chat window (1/3 width on small+ screens, full width on extra small screens) - Sticky */} +
+
+ +
+
+
); diff --git a/vite-app/src/components/BubbleContainer.tsx b/vite-app/src/components/BubbleContainer.tsx new file mode 100644 index 00000000..ed38e56a --- /dev/null +++ b/vite-app/src/components/BubbleContainer.tsx @@ -0,0 +1,85 @@ +import type { ReactNode } from "react"; +import Button from "./Button"; +import { Tooltip } from "./Tooltip"; + +interface BubbleContainerProps { + role: "user" | "assistant" | "system" | "tool" | "thinking"; + children: ReactNode; + onCopy?: () => void; + copySuccess?: boolean; + showCopyButton?: boolean; +} + +export const BubbleContainer = ({ + role, + children, + onCopy, + copySuccess = false, + showCopyButton = true, +}: BubbleContainerProps) => { + const isUser = role === "user"; + const isSystem = role === "system"; + const isTool = role === "tool"; + const isAssistant = role === "assistant"; + const isThinking = role === "thinking"; + + const handleCopy = async () => { + if (onCopy) { + await onCopy(); + } + }; + + return ( +
+
+ {/* Copy button positioned in top-right corner */} + {showCopyButton && onCopy && ( +
+ + + +
+ )} + +
+ {role} +
+ {children} +
+
+ ); +}; diff --git a/vite-app/src/components/ChatMessages.tsx b/vite-app/src/components/ChatMessages.tsx new file mode 100644 index 00000000..639e80d1 --- /dev/null +++ b/vite-app/src/components/ChatMessages.tsx @@ -0,0 +1,69 @@ +import { useRef, useEffect } from "react"; +import type { Message } from "../types/eval-protocol"; +import { MessageBubble } from "./MessageBubble"; +import { ThinkingBubble } from "./ThinkingBubble"; + +interface ChatMessagesProps { + messages: Message[]; + isLoading?: boolean; +} + +export const ChatMessages = ({ + messages, + isLoading = false, +}: ChatMessagesProps) => { + const scrollContainerRef = useRef(null); + const prevMessagesLengthRef = useRef(0); + + // Auto-scroll to bottom when new messages come in + useEffect(() => { + // On first render, just set the initial length without scrolling + if (prevMessagesLengthRef.current === 0) { + prevMessagesLengthRef.current = messages.length; + return; + } + + // Only scroll if we have messages and the number of messages has increased + // This prevents scrolling on initial mount or when messages are removed + if ( + messages.length > 0 && + messages.length > prevMessagesLengthRef.current + ) { + if (scrollContainerRef.current) { + scrollContainerRef.current.scrollTo({ + top: scrollContainerRef.current.scrollHeight, + behavior: "smooth", + }); + } + } + // Update the previous length for the next comparison + prevMessagesLengthRef.current = messages.length; + }, [messages]); + + return ( +
+ {messages.length === 0 ? ( +
+
+
🤖
+
+ Start a conversation with the AI assistant +
+
+ Ask about your evaluation data, trends, or insights +
+
+
+ ) : ( + messages.map((message, index) => ( + + )) + )} + + {isLoading && } +
+ ); +}; diff --git a/vite-app/src/components/ChatWindow.tsx b/vite-app/src/components/ChatWindow.tsx new file mode 100644 index 00000000..1b692c02 --- /dev/null +++ b/vite-app/src/components/ChatWindow.tsx @@ -0,0 +1,244 @@ +import { useState } from "react"; +import { observer } from "mobx-react"; +import { ChatMessages } from "./ChatMessages"; +import Textarea from "./Textarea"; +import Button from "./Button"; +import { AgentService } from "../services/AgentService"; +import type { Message } from "../types/eval-protocol"; +import { ChatState } from "../stores/ChatState"; + +interface ChatWindowProps { + className?: string; +} + +// Create singleton instances at module level +const agentService = new AgentService(); +const chatState = new ChatState(); + +export const ChatWindow = observer(({ className = "" }: ChatWindowProps) => { + const [chatInput, setChatInput] = useState(""); + + const processMessage = async (message: string) => { + // Add user message + const userMessage: Message = { + role: "user", + content: message, + }; + chatState.addMessage(userMessage); + chatState.setLoading(true); + chatState.setError(null); + + try { + // For now, simulate AI response with tool calls + // In a real implementation, you'd call an AI service here + await simulateAIResponse(message); + } catch (error) { + chatState.setError( + error instanceof Error ? error.message : "Unknown error" + ); + } finally { + chatState.setLoading(false); + } + }; + + const simulateAIResponse = async (userMessage: string) => { + // Simulate AI thinking time + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Generate a simple response based on the message + let response = "I understand you want to analyze your evaluation data. "; + let toolCalls = []; + + // Simple keyword-based tool call generation + if ( + userMessage.toLowerCase().includes("failed") || + userMessage.toLowerCase().includes("error") + ) { + response += "Let me find failed evaluations for you."; + toolCalls = [ + { + id: agentService.generateToolCallId(), + name: "analyzeData", + parameters: { + filters: [ + { + field: "evaluation_result.score", + operator: "<", + value: "0.5", + }, + ], + visualizationType: "table", + }, + }, + ]; + } else if ( + userMessage.toLowerCase().includes("model") || + userMessage.toLowerCase().includes("compare") + ) { + response += "Let me compare model performance for you."; + toolCalls = [ + { + id: agentService.generateToolCallId(), + name: "analyzeData", + parameters: { + groupBy: ["input_metadata.completion_params.model"], + aggregations: [ + { + field: "evaluation_result.score", + operation: "avg", + alias: "average_score", + }, + ], + visualizationType: "chart", + }, + }, + ]; + } else if ( + userMessage.toLowerCase().includes("trend") || + userMessage.toLowerCase().includes("time") + ) { + response += "Let me analyze trends over time for you."; + toolCalls = [ + { + id: agentService.generateToolCallId(), + name: "analyzeData", + parameters: { + groupBy: ["created_at"], + aggregations: [ + { + field: "evaluation_result.score", + operation: "avg", + alias: "average_score", + }, + ], + visualizationType: "chart", + }, + }, + ]; + } else { + response += "Let me show you a general overview of your data."; + toolCalls = [ + { + id: agentService.generateToolCallId(), + name: "analyzeData", + parameters: { + limit: 10, + visualizationType: "table", + }, + }, + ]; + } + + // Add AI message + const aiMessage: Message = { + role: "assistant", + content: response, + tool_calls: toolCalls.map((tc) => ({ + id: tc.id, + type: "function" as const, + function: { + name: tc.name, + arguments: JSON.stringify(tc.parameters), + }, + })), + }; + chatState.addMessage(aiMessage); + + // Execute tool calls + if (toolCalls.length > 0) { + const toolResults = []; + for (const toolCall of toolCalls) { + const result = await agentService.executeToolCall(toolCall); + toolResults.push(result); + } + chatState.addToolResultsToLastMessage(toolResults); + } + }; + + const handleSendMessage = async () => { + if (!chatInput.trim()) return; + + const message = chatInput.trim(); + setChatInput(""); + await processMessage(message); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }; + + const handleSuggestionClick = (suggestion: string) => { + setChatInput(suggestion); + }; + + const handleClearChat = () => { + chatState.clearMessages(); + // Re-add welcome message + chatState.addWelcomeMessage(); + }; + + const toolSuggestions = agentService.getToolSuggestions(); + + return ( +
+
+ {/* Chat header - following Dashboard pattern */} +
+
+

AI Assistant

+ +
+
+ + {/* Chat messages */} + + + {/* Tool suggestions */} +
+
+ Try asking: + {toolSuggestions.map((suggestion, index) => ( + + ))} +
+
+ + {/* Chat input */} +
+