"use client";
import { useEffect, useRef, useState } from "react";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import type { UIMessage } from "ai";
import { Streamdown } from "streamdown";
import { ArrowUp, ChevronRight, Loader2, Sparkles, Wrench } from "lucide-react";
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
import { cn } from "@/lib/utils";
type AnyPart = {
type: string;
text?: string;
toolName?: string;
input?: unknown;
output?: unknown;
state?: string;
};
function ToolPart({ part }: { part: AnyPart }) {
const [open, setOpen] = useState(false);
const name = part.toolName ?? part.type.replace(/^tool-/, "");
const inputStr =
part.input && typeof part.input === "object"
? (JSON.stringify(part.input).length > 80
? JSON.stringify(part.input).slice(0, 80) + "…"
: JSON.stringify(part.input))
: String(part.input ?? "");
const done = part.state === "output-available" || part.output != null;
return (
{open && part.output != null && (
{typeof part.output === "string" ? part.output : JSON.stringify(part.output, null, 2)}
)}
);
}
function MessageBubble({ message }: { message: UIMessage }) {
const isUser = message.role === "user";
const parts = (message.parts ?? []) as AnyPart[];
return (
{parts.map((part, i) => {
if (part.type === "text") {
return isUser ? (
{part.text}
) : (
{part.text ?? ""}
);
}
if (part.type === "dynamic-tool" || part.type.startsWith("tool-")) {
return
;
}
return null;
})}
);
}
export function ChatDock({ projectId }: { projectId: string }) {
const [input, setInput] = useState("");
const scrollRef = useRef(null);
const { messages, sendMessage, status, setMessages } = useChat({
id: `chat-${projectId}`,
transport: new DefaultChatTransport({ api: `/api/projects/${projectId}/chat` }),
});
// Hydrate prior conversation on mount.
useEffect(() => {
let cancelled = false;
fetch(`/api/projects/${projectId}/chat`)
.then((r) => r.json())
.then((d: { messages?: UIMessage[] }) => {
if (!cancelled && d.messages?.length) setMessages(d.messages);
})
.catch(() => {});
return () => {
cancelled = true;
};
}, [projectId, setMessages]);
useEffect(() => {
const el = scrollRef.current;
if (el) el.scrollTop = el.scrollHeight;
}, [messages, status]);
const busy = status === "submitted" || status === "streaming";
function submit(e: React.FormEvent) {
e.preventDefault();
const text = input.trim();
if (!text || busy) return;
sendMessage({ text });
setInput("");
}
return (
);
}