"use client"; import { useEffect, useRef } from "react"; import { useRouter } from "next/navigation"; import { useChat } from "@ai-sdk/react"; import { DefaultChatTransport, isToolUIPart, lastAssistantMessageIsCompleteWithToolCalls } from "ai"; import { Conversation, ConversationContent, ConversationEmptyState, ConversationScrollButton, } from "@/components/ai-elements/conversation"; import { Message, MessageContent, MessageResponse } from "@/components/ai-elements/message"; import { Reasoning, ReasoningContent, ReasoningTrigger } from "@/components/ai-elements/reasoning"; import { PromptInput, PromptInputBody, PromptInputFooter, PromptInputSubmit, PromptInputTextarea, PromptInputTools, type PromptInputMessage, } from "@/components/ai-elements/prompt-input"; import { Shimmer } from "@/components/ai-elements/shimmer"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { QuizCard } from "@/components/quiz-card"; import { AnsweredCard } from "@/components/answered-card"; import type { TeacherUIMessage } from "@/src/agent/ui-message"; import type { QuestionAnswerData } from "@/src/core/question-core"; import { findPendingToolCall } from "@/src/core/pending"; /** * The teaching session. All chat state lives in useChat; the server persists * the canonical transcript after every turn. A pending quiz/ask is just an * unanswered tool part — so reload/resume mid-question works for free. */ export function Chat({ chatId, title, initialMessages, initialPrompt, }: { chatId: string; title: string; initialMessages: TeacherUIMessage[]; initialPrompt?: string; }) { const router = useRouter(); const { messages, sendMessage, addToolOutput, status, error, stop } = useChat({ id: chatId, messages: initialMessages, transport: new DefaultChatTransport({ api: "/api/chat", body: { chatId } }), // After the learner answers (addToolOutput), automatically send the // continuation request so the teacher reacts to the answer. sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls, }); // New chat with a topic: send the first message exactly once. const sentInitial = useRef(false); useEffect(() => { if (!sentInitial.current && initialPrompt && initialMessages.length === 0) { sentInitial.current = true; void sendMessage({ text: initialPrompt }); } }, [initialPrompt, initialMessages.length, sendMessage]); const pending = findPendingToolCall(messages); const busy = status === "submitted" || status === "streaming"; const submitAnswer = (toolCallId: string, tool: "quiz" | "ask_user_question", output: QuestionAnswerData) => { void addToolOutput({ tool, toolCallId, output }); }; const onComposerSubmit = (msg: PromptInputMessage) => { if (!msg.text.trim() || busy || pending) return; void sendMessage({ text: msg.text }); }; return (
{title}
{busy && ( )}
{messages.length === 0 && !busy && ( )} {messages.map((m, mi) => ( {m.role === "user" ? (
{m.parts.filter((p) => p.type === "text").map((p) => p.text).join("\n")}
) : ( m.parts.map((part, pi) => { const isLastPart = mi === messages.length - 1 && pi === m.parts.length - 1; switch (part.type) { case "text": return {part.text}; case "reasoning": return ( {part.text} ); case "step-start": return pi > 0 ? : null; case "tool-quiz": case "tool-ask_user_question": { const tool = part.type === "tool-quiz" ? ("quiz" as const) : ("ask_user_question" as const); if (part.state === "output-available") { return ; } if (part.state === "input-available") { return ( submitAnswer(part.toolCallId, tool, output)} /> ); } // input-streaming: the question is still being generated return (
Preparing a question…
); } default: return null; } }) )}
))} {status === "submitted" && Thinking…} {error && (
Something went wrong: {error.message}
)}
{pending && (
Answer the question above to continue.
)}
); }