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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user