Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 26 additions & 13 deletions vite-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -137,7 +138,7 @@ const App = observer(() => {
return (
<div className="min-h-screen bg-gray-50">
<nav className="bg-white border-b border-gray-200">
<div className="max-w-7xl mx-auto px-3">
<div className="max-w-full mx-auto px-1">
<div className="flex justify-between items-center h-10">
<div className="flex items-center space-x-2">
<a href="https://evalprotocol.io" target="_blank">
Expand All @@ -164,18 +165,30 @@ const App = observer(() => {
</div>
</nav>

<main className="max-w-7xl mx-auto px-3 py-4">
<Routes>
<Route path="/" element={<Navigate to="/table" replace />} />
<Route
path="/table"
element={<Dashboard onRefresh={handleManualRefresh} />}
/>
<Route
path="/pivot"
element={<Dashboard onRefresh={handleManualRefresh} />}
/>
</Routes>
<main className="max-w-full mx-auto px-3 py-1">
<div className="flex gap-1">
{/* Left side - Main content (2/3 width) - Hidden on small screens */}
<div className="flex-1 min-w-0 hidden sm:block">
<Routes>
<Route path="/" element={<Navigate to="/table" replace />} />
<Route
path="/table"
element={<Dashboard onRefresh={handleManualRefresh} />}
/>
<Route
path="/pivot"
element={<Dashboard onRefresh={handleManualRefresh} />}
/>
</Routes>
</div>

{/* Right side - Chat window (1/3 width on small+ screens, full width on extra small screens) - Sticky */}
<div className="w-full sm:w-1/3 sm:min-w-[300px]">
<div className="sticky top-2">
<ChatWindow className="w-full" />
</div>
</div>
</div>
</main>
</div>
);
Expand Down
85 changes: 85 additions & 0 deletions vite-app/src/components/BubbleContainer.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={`flex ${isUser ? "justify-end" : "justify-start"} mb-1`}>
<div
className={`max-w-sm lg:max-w-md xl:max-w-lg px-2 py-1 border text-xs relative overflow-scroll ${
isUser
? "bg-blue-50 border-blue-200 text-blue-900"
: isAssistant
? "bg-gray-50 border-gray-200 text-gray-800"
: isSystem
? "bg-gray-50 border-gray-200 text-gray-800"
: isTool
? "bg-green-50 border-green-200 text-green-900"
: isThinking
? "bg-gray-100 border-gray-200 text-gray-800"
: "bg-yellow-50 border-yellow-200 text-yellow-900"
}`}
>
{/* Copy button positioned in top-right corner */}
{showCopyButton && onCopy && (
<div className="absolute top-1 right-1">
<Tooltip
content={copySuccess ? "Copied!" : "Copy message to clipboard"}
position="top"
>
<Button
onClick={handleCopy}
size="sm"
variant="secondary"
className={`p-0.5 h-5 text-[10px] opacity-60 hover:opacity-100 transition-opacity cursor-pointer ${
isUser
? "text-blue-600 hover:bg-blue-100"
: isSystem
? "text-gray-600 hover:bg-gray-100"
: isTool
? "text-green-600 hover:bg-green-100"
: isThinking
? "text-gray-600 hover:bg-gray-100"
: "text-yellow-600 hover:bg-yellow-100"
}`}
>
Copy
</Button>
</Tooltip>
</div>
)}

<div className="font-semibold text-xs mb-0.5 capitalize pr-8">
{role}
</div>
{children}
</div>
</div>
);
};
69 changes: 69 additions & 0 deletions vite-app/src/components/ChatMessages.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(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 (
<div
ref={scrollContainerRef}
className="flex-1 overflow-y-auto p-3 space-y-2"
>
{messages.length === 0 ? (
<div className="flex items-center justify-center h-full text-gray-500">
<div className="text-center">
<div className="text-lg mb-2">🤖</div>
<div className="text-sm">
Start a conversation with the AI assistant
</div>
<div className="text-xs mt-1">
Ask about your evaluation data, trends, or insights
</div>
</div>
</div>
) : (
messages.map((message, index) => (
<MessageBubble key={index} message={message} />
))
)}

{isLoading && <ThinkingBubble />}
</div>
);
};
Loading
Loading