"use client"; import { useEffect, useRef, useState, useCallback } from "react"; import { useSession } from "next-auth/react"; interface ChatMessage { role: "user" | "assistant"; content: string; } interface AtlasChatProps { projectId: string; projectName?: string; } // --------------------------------------------------------------------------- // Markdown-lite renderer — handles **bold**, newlines, numbered/bullet lists // --------------------------------------------------------------------------- function renderContent(text: string) { return text.split("\n").map((line, i) => { const parts = line.split(/(\*\*.*?\*\*)/g).map((seg, j) => seg.startsWith("**") && seg.endsWith("**") ? {seg.slice(2, -2)} : {seg} ); return
{parts}
; }); } // --------------------------------------------------------------------------- // Message row // --------------------------------------------------------------------------- function MessageRow({ msg, userInitial }: { msg: ChatMessage; userInitial: string }) { const isAtlas = msg.role === "assistant"; return (
{/* Avatar */}
{isAtlas ? "A" : userInitial}
{/* Label */}
{isAtlas ? "Atlas" : "You"}
{/* Content */}
{renderContent(msg.content)}
); } // --------------------------------------------------------------------------- // Typing indicator // --------------------------------------------------------------------------- function TypingIndicator() { return (
A
{[0, 1, 2].map(d => (
))}
); } // --------------------------------------------------------------------------- // Main component // --------------------------------------------------------------------------- export function AtlasChat({ projectId }: AtlasChatProps) { const { data: session } = useSession(); const userInitial = session?.user?.name?.[0]?.toUpperCase() ?? session?.user?.email?.[0]?.toUpperCase() ?? "Y"; const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [isStreaming, setIsStreaming] = useState(false); const [historyLoaded, setHistoryLoaded] = useState(false); const initTriggered = useRef(false); const endRef = useRef(null); // Scroll to bottom whenever messages change useEffect(() => { endRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, isStreaming]); // Send a message to Atlas — optionally hidden from UI (for init trigger) const sendToAtlas = useCallback(async (text: string, hideUserMsg = false) => { if (!hideUserMsg) { setMessages(prev => [...prev, { role: "user", content: text }]); } setIsStreaming(true); try { const res = await fetch(`/api/projects/${projectId}/atlas-chat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: text }), }); 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(); // alreadyStarted means the init was called but history already exists — ignore if (data.alreadyStarted) return; if (data.reply) { setMessages(prev => [...prev, { role: "assistant", content: data.reply }]); } } catch (e) { const msg = e instanceof Error ? e.message : "Something went wrong."; setMessages(prev => [...prev, { role: "assistant", content: msg }]); } finally { setIsStreaming(false); } }, [projectId]); // On mount: load stored history; if empty, trigger Atlas greeting useEffect(() => { if (historyLoaded) return; fetch(`/api/projects/${projectId}/atlas-chat`) .then(r => r.json()) .then((data: { messages: ChatMessage[] }) => { const stored = data.messages ?? []; setMessages(stored); setHistoryLoaded(true); // Only trigger greeting if there's genuinely no history yet if (stored.length === 0 && !initTriggered.current) { initTriggered.current = true; sendToAtlas("__atlas_init__", true); } }) .catch(() => { setHistoryLoaded(true); // If we can't load, still try to greet on first open if (!initTriggered.current) { initTriggered.current = true; sendToAtlas("__atlas_init__", true); } }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [projectId]); const handleSend = () => { const text = input.trim(); if (!text || isStreaming) return; setInput(""); sendToAtlas(text, false); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }; const isEmpty = messages.length === 0 && !isStreaming; return (
{/* Empty state */} {isEmpty && (
A

Atlas

Your product strategist. Let's define what you're building.

)} {/* Messages */} {!isEmpty && (
{messages.map((msg, i) => ( ))} {isStreaming && }
)} {/* Loading history state */} {isEmpty && isStreaming && (
)} {/* Input bar */}