diff --git a/components/AtlasChat.tsx b/components/AtlasChat.tsx index 3550387..26c11c9 100644 --- a/components/AtlasChat.tsx +++ b/components/AtlasChat.tsx @@ -1,207 +1,109 @@ "use client"; -import { useState, useRef, useEffect, useCallback } from "react"; -import { Textarea } from "@/components/ui/textarea"; -import { Button } from "@/components/ui/button"; -import { ScrollArea } from "@/components/ui/scroll-area"; +import { useEffect, useRef } from "react"; import { - Send, - Loader2, - User, - RotateCcw, - FileText, - CheckCircle2, - ChevronDown, - ChevronUp, -} from "lucide-react"; + AssistantRuntimeProvider, + useLocalRuntime, + type ChatModelAdapter, +} from "@assistant-ui/react"; +import { Thread } from "@/components/assistant-ui/thread"; +import { Button } from "@/components/ui/button"; +import { FileText, RotateCcw } from "lucide-react"; // --------------------------------------------------------------------------- -// Types +// Props // --------------------------------------------------------------------------- -interface Message { - role: "user" | "assistant"; - content: string; -} - interface AtlasChatProps { projectId: string; projectName?: string; - initialMessage?: string; } // --------------------------------------------------------------------------- -// Atlas avatar — distinct from orchestrator +// Runtime adapter — calls our existing /api/projects/[id]/atlas-chat endpoint // --------------------------------------------------------------------------- -function AtlasAvatar() { - return ( -
- A -
- ); +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 || "…" }], + }; + }, + }; } // --------------------------------------------------------------------------- -// PRD preview panel +// Inner component — has access to runtime context // --------------------------------------------------------------------------- -function PrdPanel({ content }: { content: string }) { - const [expanded, setExpanded] = useState(false); - const preview = content.split("\n").slice(0, 6).join("\n"); +function AtlasChatInner({ + projectId, + projectName, + runtime, +}: AtlasChatProps & { runtime: ReturnType }) { + const greeted = useRef(false); - return ( -
-
- - - PRD Generated - - - Ready for architecture phase - - -
-
-
-          {expanded ? content : preview}
-        
- {!expanded && ( - - )} -
-
- ); -} - -// --------------------------------------------------------------------------- -// Main component -// --------------------------------------------------------------------------- - -export function AtlasChat({ projectId, projectName, initialMessage }: AtlasChatProps) { - const [messages, setMessages] = useState([]); - const [input, setInput] = useState(""); - const [loading, setLoading] = useState(false); - const [prdContent, setPrdContent] = useState(null); - const [error, setError] = useState(null); - const [started, setStarted] = useState(false); - const bottomRef = useRef(null); - const textareaRef = useRef(null); - - // Auto-scroll to latest message + // Send Atlas's opening message automatically on first load useEffect(() => { - bottomRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [messages, loading]); + if (greeted.current) return; + greeted.current = true; - // Kick off Atlas with its opening message on first load - useEffect(() => { - if (started) return; - setStarted(true); - sendMessage( - initialMessage || - `Hey — I'm starting a new project called "${projectName || "my project"}". I'd love your help defining what we're building.` - ); + 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 t = setTimeout(() => { + runtime.thread.composer.setText(opener); + runtime.thread.composer.send(); + }, 300); + + return () => clearTimeout(t); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const sendMessage = useCallback( - async (text: string) => { - if (!text.trim() || loading) return; - setError(null); - - const userMsg: Message = { role: "user", content: text }; - setMessages((prev) => [...prev, userMsg]); - setInput(""); - setLoading(true); - - try { - const res = await fetch( - `/api/projects/${projectId}/atlas-chat`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ message: text }), - } - ); - - const data = await res.json(); - - if (!res.ok) { - setError(data.error || "Something went wrong. Please try again."); - setMessages((prev) => prev.slice(0, -1)); - return; - } - - const assistantMsg: Message = { - role: "assistant", - content: data.reply || "...", - }; - setMessages((prev) => [...prev, assistantMsg]); - - if (data.prdContent) { - setPrdContent(data.prdContent); - } - } catch { - setError("Couldn't reach Atlas. Check your connection and try again."); - setMessages((prev) => prev.slice(0, -1)); - } finally { - setLoading(false); - setTimeout(() => textareaRef.current?.focus(), 50); - } - }, - [loading, projectId] - ); - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - sendMessage(input); - } - }; - const handleReset = async () => { if (!confirm("Start the discovery conversation over from scratch?")) return; await fetch(`/api/projects/${projectId}/atlas-chat`, { method: "DELETE" }); - setMessages([]); - setPrdContent(null); - setStarted(false); - setError(null); + // Reload to get a fresh runtime state + window.location.reload(); }; return ( -
+
{/* Header */} -
+
- +
+ A +

Atlas

Product Requirements

- {prdContent && ( -
- - PRD ready -
- )}
- {/* Messages */} - -
- {messages.map((msg, i) => ( -
- {msg.role === "assistant" && } - -
-

- {msg.content} -

-
- - {msg.role === "user" && ( -
- -
- )} -
- ))} - - {/* Typing indicator */} - {loading && ( -
- -
-
- - - -
-
-
- )} - - {/* Error */} - {error && ( -
- {error} -
- )} - - {/* PRD panel */} - {prdContent && } - -
-
- - - {/* Input */} -
- {prdContent ? ( -

- PRD complete — the platform is now architecting your solution. -

- ) : ( -
-