"use client"; import { useState, useRef, useEffect, useCallback } from "react"; import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Send, Loader2, Wrench, Bot, User, RotateCcw, ChevronDown, ChevronUp, Sparkles, } from "lucide-react"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- interface Message { role: "user" | "assistant"; content: string; toolCalls?: string[]; turns?: number; model?: string; reasoning?: string | null; error?: boolean; } interface OrchestratorChatProps { projectId: string; projectName?: string; placeholder?: string; } // --------------------------------------------------------------------------- // Friendly labels for tool call names // --------------------------------------------------------------------------- const TOOL_LABELS: Record = { spawn_agent: "Dispatched agent", get_job_status: "Checked job", list_repos: "Listed repos", list_all_issues: "Checked issues", list_all_apps: "Checked deployments", get_app_status: "Checked app status", read_repo_file: "Read file", deploy_app: "Triggered deploy", gitea_create_issue: "Created issue", gitea_list_issues: "Listed issues", gitea_close_issue: "Closed issue", gitea_comment_issue: "Added comment", }; function friendlyToolName(raw: string): string { return TOOL_LABELS[raw] ?? raw.replace(/_/g, " "); } // --------------------------------------------------------------------------- // Suggestion chips shown before the first message // --------------------------------------------------------------------------- const SUGGESTIONS = [ "What's the current status of this project?", "Check if there are any open issues or PRs", "What was the last deployment?", "Write a quick summary of what's been built so far", ]; // --------------------------------------------------------------------------- // Single message bubble // --------------------------------------------------------------------------- function MessageBubble({ msg }: { msg: Message }) { const [showReasoning, setShowReasoning] = useState(false); const isUser = msg.role === "user"; return (
{/* Avatar */}
{isUser ? : }
{/* Bubble */}
{msg.content}
{/* Tool calls & meta */} {!isUser && (
{msg.toolCalls && msg.toolCalls.length > 0 && (
{/* Deduplicate tool calls before rendering */} {[...new Set(msg.toolCalls)].map((t, i) => ( {friendlyToolName(t)} ))}
)} {msg.reasoning && ( )} {msg.model && msg.model !== "unknown" && ( {msg.model.includes("glm") ? "GLM-5" : msg.model.includes("gemini") ? "Gemini" : msg.model} )}
)} {/* Reasoning panel */} {!isUser && showReasoning && msg.reasoning && (
{msg.reasoning}
)}
); } // --------------------------------------------------------------------------- // Typing indicator // --------------------------------------------------------------------------- function TypingIndicator() { return (
{[0, 1, 2].map(i => (
))}
); } // --------------------------------------------------------------------------- // Main component // --------------------------------------------------------------------------- export function OrchestratorChat({ projectId, projectName, placeholder = "Ask your AI team anything…", }: OrchestratorChatProps) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const bottomRef = useRef(null); const textareaRef = useRef(null); const hasMessages = messages.length > 0; const scrollToBottom = useCallback(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, []); useEffect(() => { scrollToBottom(); }, [messages, loading]); const sendMessage = useCallback( async (text: string) => { const trimmed = text.trim(); if (!trimmed || loading) return; setInput(""); setMessages(prev => [...prev, { role: "user", content: trimmed }]); setLoading(true); try { const res = await fetch(`/api/projects/${projectId}/agent-chat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: trimmed }), }); const data = await res.json(); if (!res.ok) { setMessages(prev => [ ...prev, { role: "assistant", content: data.error ?? "Something went wrong.", error: true }, ]); } else { setMessages(prev => [ ...prev, { role: "assistant", content: data.reply || "(no reply)", toolCalls: data.toolCalls, turns: data.turns, model: data.model, reasoning: data.reasoning, }, ]); } } catch (err) { setMessages(prev => [ ...prev, { role: "assistant", content: err instanceof Error ? err.message : "Network error — is the agent runner online?", error: true, }, ]); } finally { setLoading(false); setTimeout(() => textareaRef.current?.focus(), 50); } }, [projectId, loading] ); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(input); } }; const clearChat = async () => { setMessages([]); try { await fetch(`/api/projects/${projectId}/agent-chat`, { method: "DELETE" }); } catch { /* best-effort */ } }; return (
{/* Header */}
{projectName ? `${projectName} AI` : "Project AI"} GLM-5
{hasMessages && ( )}
{/* Messages */}
{!hasMessages ? ( /* Empty state — Lovable-style centered prompt */

What should we build?

Your AI team is ready. Ask them anything about this project.

{SUGGESTIONS.map(s => ( ))}
) : (
{messages.map((msg, i) => ( ))} {loading && }
)}
{/* Input */}