Apply Stackless chat design to Atlas thread

- Remove card container (no more rounded-2xl, ring, 600px height)
- Chat fills the full layout space naturally
- Avatars: 28x28 rounded-7 squares (black for Atlas, warm gray for user)
- Both sides use same avatar+label layout (no right-aligned bubbles)
- Sender labels: tiny uppercase ATLAS / YOU above each message
- Input bar: white pill with border, Send button, Stop for streaming
- User initial pulled from session (name or email first letter)

Made-with: Cursor
This commit is contained in:
2026-03-02 16:15:25 -08:00
parent 94bb9dbeb4
commit 9858a7fa15
2 changed files with 220 additions and 156 deletions

View File

@@ -1,28 +1,19 @@
"use client";
import { useEffect, useRef } from "react";
import { useSession } from "next-auth/react";
import {
AssistantRuntimeProvider,
useLocalRuntime,
type ChatModelAdapter,
} from "@assistant-ui/react";
import { Thread } from "@/components/assistant-ui/thread";
import { Button } from "@/components/ui/button";
import { RotateCcw } from "lucide-react";
// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------
interface AtlasChatProps {
projectId: string;
projectName?: string;
}
// ---------------------------------------------------------------------------
// Runtime adapter — calls our existing /api/projects/[id]/atlas-chat endpoint
// ---------------------------------------------------------------------------
function makeAtlasAdapter(projectId: string): ChatModelAdapter {
return {
async run({ messages, abortSignal }) {
@@ -46,78 +37,49 @@ function makeAtlasAdapter(projectId: string): ChatModelAdapter {
}
const data = await res.json();
return {
content: [{ type: "text", text: data.reply || "…" }],
};
return { content: [{ type: "text", text: data.reply || "…" }] };
},
};
}
// ---------------------------------------------------------------------------
// Inner component — has access to runtime context
// ---------------------------------------------------------------------------
function AtlasChatInner({
projectId,
projectName,
userInitial,
runtime,
}: AtlasChatProps & { runtime: ReturnType<typeof useLocalRuntime> }) {
}: AtlasChatProps & {
userInitial: string;
runtime: ReturnType<typeof useLocalRuntime>;
}) {
const greeted = useRef(false);
// Send Atlas's opening message automatically on first load
useEffect(() => {
if (greeted.current) return;
greeted.current = true;
const opener =
`Hey — I'm starting a new project called "${projectName || "my project"}". I'd love your help defining what we're building.`;
// Small delay so the thread is mounted before we submit
const opener = `Hey — I'm starting a new project called "${projectName || "my project"}". I'd love your help defining what we're building.`;
const t = setTimeout(() => {
runtime.thread.composer.setText(opener);
runtime.thread.composer.send();
}, 300);
return () => clearTimeout(t);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleReset = async () => {
if (!confirm("Start the discovery conversation over from scratch?")) return;
await fetch(`/api/projects/${projectId}/atlas-chat`, { method: "DELETE" });
// Reload to get a fresh runtime state
window.location.reload();
};
return (
<div className="flex flex-col rounded-2xl overflow-hidden bg-card ring-1 ring-border" style={{ height: "600px" }}>
{/* Minimal header bar */}
<div className="flex items-center justify-end px-4 py-2.5 shrink-0 border-b border-border">
<Button
variant="ghost"
size="sm"
onClick={handleReset}
className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground"
title="Start over"
>
<RotateCcw className="w-3.5 h-3.5" />
</Button>
</div>
{/* Thread */}
<div className="flex-1 overflow-hidden">
<Thread />
</div>
// No card — fills the layout space directly
<div style={{ height: "100%", display: "flex", flexDirection: "column" }}>
<Thread userInitial={userInitial} />
</div>
);
}
// ---------------------------------------------------------------------------
// Main export — wraps with runtime provider
// ---------------------------------------------------------------------------
export function AtlasChat({ projectId, projectName }: AtlasChatProps) {
const { data: session } = useSession();
const userInitial =
session?.user?.name?.[0]?.toUpperCase() ??
session?.user?.email?.[0]?.toUpperCase() ??
"Y";
const adapter = makeAtlasAdapter(projectId);
const runtime = useLocalRuntime(adapter);
@@ -126,6 +88,7 @@ export function AtlasChat({ projectId, projectName }: AtlasChatProps) {
<AtlasChatInner
projectId={projectId}
projectName={projectName}
userInitial={userInitial}
runtime={runtime}
/>
</AssistantRuntimeProvider>