Wave 0: shared contracts + extension points

- src/core/contracts.ts: plan tree / graph / ledger / job protocol / artifact
  registry types — the single source of truth for all workstreams
- tools: per-request factory createTeacherTools({chatId}) composing
  questionTools + planTools (stub) + jobTools (stub); quiz gains nodeId
- prompt composition: system-prompt.md + sorted src/agent/prompts/*.md
- ChatRecord.tree + saveTree; pending gate scoped to quiz/ask (+findPendingJobCall)
- chat.tsx: artifact renderer extension point + ChatActionsContext.syncFromServer
This commit is contained in:
Amos
2026-08-22 09:06:24 +03:00
parent c338f468f8
commit c55bae0de6
11 changed files with 429 additions and 23 deletions
+34 -3
View File
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { createContext, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport, isToolUIPart, lastAssistantMessageIsCompleteWithToolCalls } from "ai";
import { useStickToBottomContext } from "use-stick-to-bottom";
@@ -15,16 +15,30 @@ import { Reasoning, ReasoningContent, ReasoningTrigger } from "@/components/ai-e
import { Shimmer } from "@/components/ai-elements/shimmer";
import { Separator } from "@/components/ui/separator";
import { AnsweredCard } from "@/components/answered-card";
import { artifactRenderers } from "@/components/artifacts";
import { Composer } from "@/components/composer";
import { QuestionDock } from "@/components/question-dock";
import { SidebarToggle } from "@/components/sidebar";
import type { TeacherUIMessage } from "@/src/agent/ui-message";
import type { ArtifactPartProps } from "@/src/core/contracts";
import type { QuestionAnswerData } from "@/src/core/question-core";
import { findPendingToolCall } from "@/src/core/pending";
/** One width for everything: transcript, cards, and the bottom blob share this column. */
const COLUMN = "mx-auto w-full max-w-3xl px-4";
/**
* Server-sync channel for artifact renderers (job cards): when a background
* job flips its tool part to output-available server-side, the card polls
* /api/jobs, then calls syncFromServer() — fresh messages replace local state
* and the sendAutomaticallyWhen continuation resumes the teacher.
* (contracts.ts "THE ASYNC PROTOCOL" §4.)
*/
export const ChatActionsContext = createContext<{
chatId: string;
syncFromServer: () => Promise<void>;
} | null>(null);
/** useLayoutEffect that is silent during SSR (client components still render on the server in Next). */
const useIsoLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
@@ -48,7 +62,7 @@ export function Chat({
initialMessages: TeacherUIMessage[];
initialPrompt?: string;
}) {
const { messages, sendMessage, addToolOutput, status, error, stop } =
const { messages, sendMessage, addToolOutput, setMessages, status, error, stop } =
useChat<TeacherUIMessage>({
id: chatId,
messages: initialMessages,
@@ -80,6 +94,13 @@ export function Chat({
const [text, setText] = useState("");
const canSend = text.trim().length > 0 && !pending;
const syncFromServer = useCallback(async () => {
const res = await fetch(`/api/chats/${chatId}`);
if (!res.ok) return;
const record = (await res.json()) as { messages?: TeacherUIMessage[] };
if (Array.isArray(record.messages)) setMessages(record.messages);
}, [chatId, setMessages]);
const onSend = () => {
const t = text.trim();
if (!t || busy || pending) return;
@@ -88,6 +109,7 @@ export function Chat({
};
return (
<ChatActionsContext.Provider value={{ chatId, syncFromServer }}>
<div className="flex h-dvh flex-col">
<header className="flex items-center gap-3 px-3 py-2">
<SidebarToggle />
@@ -138,8 +160,16 @@ export function Chat({
}
return null;
}
default:
default: {
// Artifact extension point (contracts.ts): job
// parts and future artifacts render via the
// registry — add renderers there, not here.
if (part.type.startsWith("tool-")) {
const Renderer = artifactRenderers[part.type.slice("tool-".length)];
if (Renderer) return <Renderer key={pi} part={part as ArtifactPartProps["part"]} />;
}
return null;
}
}
})
)}
@@ -182,6 +212,7 @@ export function Chat({
</Conversation>
</div>
</div>
</ChatActionsContext.Provider>
);
}