diff --git a/components/AtlasChat.tsx b/components/AtlasChat.tsx index 7a374d3..0e6e41c 100644 --- a/components/AtlasChat.tsx +++ b/components/AtlasChat.tsx @@ -2,6 +2,8 @@ import { useEffect, useRef, useState, useCallback } from "react"; import { useSession } from "next-auth/react"; +import { useParams } from "next/navigation"; +import Link from "next/link"; interface ChatMessage { role: "user" | "assistant"; @@ -14,9 +16,11 @@ interface AtlasChatProps { } // --------------------------------------------------------------------------- -// Phase marker — Atlas appends [[PHASE_COMPLETE:{...}]] when a phase wraps up +// Markers — Atlas appends these at end of messages to signal UI actions // --------------------------------------------------------------------------- + const PHASE_MARKER_RE = /\[\[PHASE_COMPLETE:(.*?)\]\]/s; +const NEXT_STEP_RE = /\[\[NEXT_STEP:(.*?)\]\]/s; interface PhasePayload { phase: string; @@ -25,15 +29,33 @@ interface PhasePayload { data: Record; } -function extractPhase(text: string): { clean: string; phase: PhasePayload | null } { - const match = text.match(PHASE_MARKER_RE); - if (!match) return { clean: text, phase: null }; - try { - const phase = JSON.parse(match[1]) as PhasePayload; - return { clean: text.replace(PHASE_MARKER_RE, "").trimEnd(), phase }; - } catch { - return { clean: text.replace(PHASE_MARKER_RE, "").trimEnd(), phase: null }; +interface NextStepPayload { + action: string; + label: string; +} + +function extractMarkers(text: string): { + clean: string; + phase: PhasePayload | null; + nextStep: NextStepPayload | null; +} { + let clean = text; + let phase: PhasePayload | null = null; + let nextStep: NextStepPayload | null = null; + + const phaseMatch = clean.match(PHASE_MARKER_RE); + if (phaseMatch) { + try { phase = JSON.parse(phaseMatch[1]); } catch { /* ignore */ } + clean = clean.replace(PHASE_MARKER_RE, "").trimEnd(); } + + const nextMatch = clean.match(NEXT_STEP_RE); + if (nextMatch) { + try { nextStep = JSON.parse(nextMatch[1]); } catch { /* ignore */ } + clean = clean.replace(NEXT_STEP_RE, "").trimEnd(); + } + + return { clean, phase, nextStep }; } // --------------------------------------------------------------------------- @@ -54,12 +76,27 @@ function renderContent(text: string | null | undefined) { // --------------------------------------------------------------------------- // Message row // --------------------------------------------------------------------------- -function MessageRow({ msg, userInitial, projectId }: { msg: ChatMessage; userInitial: string; projectId: string }) { +function MessageRow({ + msg, userInitial, projectId, workspace, +}: { + msg: ChatMessage; + userInitial: string; + projectId: string; + workspace: string; +}) { const isAtlas = msg.role === "assistant"; - const { clean, phase } = isAtlas ? extractPhase(msg.content ?? "") : { clean: msg.content ?? "", phase: null }; + const { clean, phase, nextStep } = isAtlas + ? extractMarkers(msg.content ?? "") + : { clean: msg.content ?? "", phase: null, nextStep: null }; + + // Phase save state const [saved, setSaved] = useState(false); const [saving, setSaving] = useState(false); + // Architecture generation state + const [archState, setArchState] = useState<"idle" | "loading" | "done" | "error">("idle"); + const [archError, setArchError] = useState(null); + const handleSavePhase = async () => { if (!phase || saved || saving) return; setSaving(true); @@ -70,13 +107,30 @@ function MessageRow({ msg, userInitial, projectId }: { msg: ChatMessage; userIni body: JSON.stringify(phase), }); setSaved(true); - } catch { - // swallow — user can retry - } finally { + } catch { /* swallow — user can retry */ } finally { setSaving(false); } }; + const handleGenerateArchitecture = async () => { + if (archState !== "idle") return; + setArchState("loading"); + setArchError(null); + try { + const res = await fetch(`/api/projects/${projectId}/architecture`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}), + }); + const d = await res.json(); + if (!res.ok) throw new Error(d.error || "Generation failed"); + setArchState("done"); + } catch (e) { + setArchError(e instanceof Error ? e.message : "Something went wrong"); + setArchState("error"); + } + }; + return (
{/* Avatar */} @@ -107,7 +161,8 @@ function MessageRow({ msg, userInitial, projectId }: { msg: ChatMessage; userIni }}> {renderContent(clean)}
- {/* Phase save button — only shown when Atlas signals phase completion */} + + {/* Phase save button */} {phase && (
+
+ ) : ( +
+
+ Next: Technical architecture +
+

+ The AI will read your PRD and recommend the apps, services, and infrastructure your product needs. Takes about 30 seconds. +

+ +
+ )} + + )} ); @@ -171,6 +307,8 @@ function TypingIndicator() { // --------------------------------------------------------------------------- export function AtlasChat({ projectId }: AtlasChatProps) { const { data: session } = useSession(); + const params = useParams(); + const workspace = (params?.workspace as string) ?? ""; const userInitial = session?.user?.name?.[0]?.toUpperCase() ?? session?.user?.email?.[0]?.toUpperCase() ?? @@ -292,6 +430,7 @@ export function AtlasChat({ projectId }: AtlasChatProps) { {/* Empty state */} @@ -336,7 +475,7 @@ export function AtlasChat({ projectId }: AtlasChatProps) { Reset {visibleMessages.map((msg, i) => ( - + ))} {isStreaming && }