Files
vibn-frontend/components/AtlasChat.tsx
Mark Henderson 9858a7fa15 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
2026-03-02 16:15:25 -08:00

97 lines
2.7 KiB
TypeScript

"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";
interface AtlasChatProps {
projectId: string;
projectName?: string;
}
function makeAtlasAdapter(projectId: string): ChatModelAdapter {
return {
async run({ messages, abortSignal }) {
const lastUser = [...messages].reverse().find((m) => m.role === "user");
const text =
lastUser?.content
.filter((p) => p.type === "text")
.map((p) => (p as { type: "text"; text: string }).text)
.join("") ?? "";
const res = await fetch(`/api/projects/${projectId}/atlas-chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: text }),
signal: abortSignal,
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || "Atlas is unavailable. Please try again.");
}
const data = await res.json();
return { content: [{ type: "text", text: data.reply || "…" }] };
},
};
}
function AtlasChatInner({
projectId,
projectName,
userInitial,
runtime,
}: AtlasChatProps & {
userInitial: string;
runtime: ReturnType<typeof useLocalRuntime>;
}) {
const greeted = useRef(false);
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.`;
const t = setTimeout(() => {
runtime.thread.composer.setText(opener);
runtime.thread.composer.send();
}, 300);
return () => clearTimeout(t);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
// No card — fills the layout space directly
<div style={{ height: "100%", display: "flex", flexDirection: "column" }}>
<Thread userInitial={userInitial} />
</div>
);
}
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);
return (
<AssistantRuntimeProvider runtime={runtime}>
<AtlasChatInner
projectId={projectId}
projectName={projectName}
userInitial={userInitial}
runtime={runtime}
/>
</AssistantRuntimeProvider>
);
}