import { z } from "zod"; /** * Chat-only "generative UI" tools. Unlike the data {@link capabilities} (which * are also exposed over MCP), these are presentation directives the model calls * to render rich UI inline in the chat — a chart, a table, a row of stat * cards — instead of a markdown wall. Their `execute` simply echoes the input; * the shared ChatContent renders the echoed data through the json-render * catalog. They are NOT in the `capabilities` array (MCP has no UI), so the two * chat routes add them separately. * * The input schemas map 1:1 to the json-render catalog components in * `@scribe/ui` (Chart / Table / StatCard) — keep them in sync. */ export const RENDER_CHART = "renderChart"; export const RENDER_TABLE = "renderTable"; export const RENDER_STAT_CARDS = "renderStatCards"; export const renderChartInputSchema = z.object({ type: z .enum(["bar", "line", "area", "pie"]) .default("bar") .describe( "Chart type. bar/line for comparison or trend, pie for parts-of-a-whole, area for cumulative.", ), title: z.string().optional().describe("Short chart title"), unit: z.string().optional().describe("Unit suffix, e.g. 'h', 'm', 'events'"), data: z .array(z.object({ label: z.string(), value: z.number() })) .min(1) .max(50) .describe( "One numeric value per labeled category, highest-first; cap to the top ~12 yourself.", ), }); export const renderTableInputSchema = z.object({ title: z.string().optional().describe("Short table title"), columns: z.array(z.string()).min(1).max(8).describe("Column headers"), rows: z .array(z.array(z.string())) .min(1) .max(50) .describe("Rows; each row has one string per column, in column order"), }); export const renderStatCardsInputSchema = z.object({ cards: z .array( z.object({ label: z.string(), value: z.string().describe("Headline value, pre-formatted"), sublabel: z.string().optional(), }), ) .min(1) .max(6), }); /** * Data tools whose results the chat UI ALREADY visualizes deterministically * (see `report-spec.ts` / `MappingProposal` in `@scribe/ui` — the switch there * is the rendering implementation; this record is what the MODEL is told about * it). Keyed by tool name; the value describes the UI the user sees the moment * the tool completes. `buildChatSystemPrompt` injects this so the model knows * the result is already on screen and doesn't re-render the same numbers via * renderChart/renderTable/renderStatCards or restate them as markdown. Keep in * sync with the report-spec switch when adding a visualized tool. */ export const AUTO_RENDERED_TOOL_UI: Readonly> = { generateReport: 'a "Time by project" bar chart plus Total time / Events / Active days stat cards', getReport: 'a "Time by project" bar chart plus Total time / Events / Active days stat cards', saveReport: "Total time / Events / Active days stat cards", getProjectActivity: "a per-source hours bar chart plus a Total hours stat card", listProjects: "a table of projects (name, client, rate, type)", listClients: "a table of clients (name, email, rate, currency)", listInvoices: "a table of invoices (number, status, total, period)", getUnassignedWork: "Total unassigned / Groups stat cards plus a per-group bar chart", proposeMappings: "an interactive mapping card with a project dropdown and Map button per group", }; export interface ChatUiTool { name: string; description: string; /** Any zod schema — the tools hold different input shapes in one list. */ inputSchema: z.ZodTypeAny; } /** * The render tools, ready for each chat route to adapt to an AI SDK `tool()` * with `execute: (input) => input`. */ export const chatUiTools: readonly ChatUiTool[] = [ { name: RENDER_CHART, description: "Render a bar, line, area, or pie chart inline in the chat to visualize a numeric breakdown, comparison, trend, cumulative series, or parts-of-a-whole. Pick the chart type that best fits the data. STRONGLY PREFER this over a markdown table whenever you are showing magnitudes the user should compare at a glance. You may call it alongside a short text explanation.", inputSchema: renderChartInputSchema, }, { name: RENDER_TABLE, description: "Render a table inline in the chat. Use for structured lists where columns matter (projects, clients, invoices, unassigned groups, proposed mappings) — prefer this over hand-formatting a markdown table.", inputSchema: renderTableInputSchema, }, { name: RENDER_STAT_CARDS, description: "Render a row of stat cards (headline numbers) — totals like total hours, event count, active days, or number of unassigned groups. Use to lead a summary before the detail.", inputSchema: renderStatCardsInputSchema, }, ];