feat: implement Story 3.1 — chat panel UI with streaming AI responses

Add AI copilot chat panel to the diagram editor with streaming responses,
chat history persistence, and markdown rendering. Includes copilot API route,
diagram-aware system prompt, and schema with 15 passing tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-28 10:03:43 +00:00
parent 9d13d0f562
commit 26215d9060
20 changed files with 3223 additions and 37 deletions

View File

@@ -0,0 +1,47 @@
import { Hono } from "hono";
import * as z from "zod";
import { getCopilotHistory, streamCopilot } from "@turbostarter/ai/copilot/api";
import { copilotMessageSchema } from "@turbostarter/ai/copilot/schema";
import { enforceAuth, deductCredits, rateLimiter, validate } from "../../../middleware";
import type { User } from "@turbostarter/auth";
const chatIdQuerySchema = z.object({
chatId: z.string(),
});
export const copilotRouter = new Hono<{
Variables: {
user: User;
};
}>()
.get(
"/messages",
enforceAuth,
validate("query", chatIdQuerySchema),
async (c) => {
const { chatId } = c.req.valid("query");
const messages = await getCopilotHistory(chatId);
return c.json(messages);
},
)
.post(
"/",
enforceAuth,
rateLimiter,
validate("json", copilotMessageSchema),
async (c) => {
const input = c.req.valid("json");
// Deduct 1 credit per copilot message
await deductCredits(1, "copilot")(c, async () => { /* noop */ });
return streamCopilot({
...input,
signal: c.req.raw.signal,
userId: c.var.user.id,
});
},
);

View File

@@ -5,6 +5,7 @@ import { getUserCredits } from "@turbostarter/ai/credits/server";
import { enforceAuth } from "../../middleware";
import { chatRouter } from "./chat";
import { copilotRouter } from "./copilot/router";
import { imageRouter } from "./image";
import { pdfRouter } from "./pdf";
import { sttRouter } from "./stt";
@@ -13,6 +14,7 @@ import { ttsRouter } from "./tts";
export const aiRouter = new Hono()
.use(enforceAuth)
.route("/chat", chatRouter)
.route("/copilot", copilotRouter)
.route("/pdf", pdfRouter)
.route("/image", imageRouter)
.route("/tts", ttsRouter)