From f9c2a8462a2e75cb3db76d645d142c4fc034f590 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Wed, 19 Nov 2025 10:27:38 +1000 Subject: [PATCH] extra tool --- .../assistant/components/assistant-widget.tsx | 11 +++ .../assistant/components/parts/chart.tsx | 78 ++++++++++++++++++ .../blocks/assistant/registry-item.json | 8 +- .../supabase/functions/chat/index.ts | 38 +++++++++ .../functions/mcp-server/tools/count-tasks.ts | 79 +++++++++++++++++++ .../functions/mcp-server/tools/registry.ts | 2 + 6 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 apps/ui-library/registry/default/blocks/assistant/components/parts/chart.tsx create mode 100644 apps/ui-library/registry/default/blocks/assistant/supabase/functions/mcp-server/tools/count-tasks.ts diff --git a/apps/ui-library/registry/default/blocks/assistant/components/assistant-widget.tsx b/apps/ui-library/registry/default/blocks/assistant/components/assistant-widget.tsx index da29574ba5d..89e4ebbe635 100644 --- a/apps/ui-library/registry/default/blocks/assistant/components/assistant-widget.tsx +++ b/apps/ui-library/registry/default/blocks/assistant/components/assistant-widget.tsx @@ -40,6 +40,7 @@ import { DefaultChatTransport } from 'ai' import { CheckIcon, MessageCircle } from 'lucide-react' import { useCallback, useState } from 'react' import { toast } from 'sonner' +import { Chart, type ChartProps } from './parts/chart' import { Row, type RowItem } from './parts/row' const CHAT_API_URL = process.env.NEXT_PUBLIC_CHAT_API_URL @@ -97,6 +98,8 @@ type RenderRowToolInput = { rows: RowItem[] } +type RenderChartToolInput = ChartProps + interface AssistantWidgetProps { /** * Override the trigger positioning if you need to embed the widget in a scoped container. @@ -236,6 +239,14 @@ export const AssistantWidget = ({ /> ) } + case 'tool-renderChart': { + const payload = (part.input ?? part.output) as + | RenderChartToolInput + | undefined + if (!payload) return null + + return + } default: return null } diff --git a/apps/ui-library/registry/default/blocks/assistant/components/parts/chart.tsx b/apps/ui-library/registry/default/blocks/assistant/components/parts/chart.tsx new file mode 100644 index 00000000000..d76ef7f9238 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/assistant/components/parts/chart.tsx @@ -0,0 +1,78 @@ +'use client' + +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/registry/default/components/ui/card' +import { + type ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from '@/registry/default/components/ui/chart' +import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts' + +export type ChartDataPoint = Record + +export type ChartProps = { + primaryText: string + secondaryText?: string + tertiaryText?: string + data: ChartDataPoint[] + xAxis: string + yAxis: string +} + +export function Chart({ + primaryText, + secondaryText, + tertiaryText, + data, + xAxis, + yAxis, +}: ChartProps) { + if (!data.length) return null + + const chartConfig = { + [yAxis]: { + label: yAxis, + color: 'var(--chart-1)', + }, + } satisfies ChartConfig + + const fillColor = `var(--color-${yAxis}, var(--chart-1))` + + return ( + + + {primaryText} + {secondaryText ? {secondaryText} : null} + + + + + + String(value).slice(0, 3)} + tickLine={false} + tickMargin={10} + /> + } /> + + + + + {(tertiaryText ?? null) ? ( + +
{tertiaryText}
+
+ ) : null} +
+ ) +} diff --git a/apps/ui-library/registry/default/blocks/assistant/registry-item.json b/apps/ui-library/registry/default/blocks/assistant/registry-item.json index cb22667e629..e4f2c9599f9 100644 --- a/apps/ui-library/registry/default/blocks/assistant/registry-item.json +++ b/apps/ui-library/registry/default/blocks/assistant/registry-item.json @@ -8,6 +8,7 @@ "input", "popover", "scroll-area", + "chart", "@ai-elements/conversation", "@ai-elements/message", "@ai-elements/prompt-input", @@ -26,7 +27,8 @@ "zod@latest", "nanoid@latest", "sonner@latest", - "lucide-react@latest" + "lucide-react@latest", + "recharts@latest" ], "files": [ { @@ -37,6 +39,10 @@ "path": "registry/default/blocks/assistant/components/parts/row.tsx", "type": "registry:component" }, + { + "path": "registry/default/blocks/assistant/components/parts/chart.tsx", + "type": "registry:component" + }, { "path": "registry/default/blocks/assistant/lib/supabase/server.ts", "type": "registry:file", diff --git a/apps/ui-library/registry/default/blocks/assistant/supabase/functions/chat/index.ts b/apps/ui-library/registry/default/blocks/assistant/supabase/functions/chat/index.ts index 5fa6768c736..56fa6a42c59 100644 --- a/apps/ui-library/registry/default/blocks/assistant/supabase/functions/chat/index.ts +++ b/apps/ui-library/registry/default/blocks/assistant/supabase/functions/chat/index.ts @@ -25,6 +25,32 @@ const rowSchema = z.object({ .describe('Optional list of quick actions the user can trigger for this row.'), }) +const chartSchema = z.object({ + primaryText: z.string().describe('Primary title shown at the top of the chart card.'), + secondaryText: z + .string() + .optional() + .describe('Optional short description shown under the title.'), + tertiaryText: z + .string() + .optional() + .describe('Optional supporting text shown beneath the chart.'), + data: z + .array( + z.record(z.union([z.string(), z.number()])).describe( + 'Data point object containing axis and value fields.' + ) + ) + .min(1) + .describe('Data points to plot on the chart.'), + xAxis: z + .string() + .describe('Key inside each data point to use for the X-axis labels (e.g., "month").'), + yAxis: z + .string() + .describe('Key inside each data point to use for the bar height values (e.g., "desktop").'), +}) + const renderRowTool = tool({ description: 'Render a task row to summarize Supabase records, including follow-up actions the user can take.', @@ -39,8 +65,20 @@ const renderRowTool = tool({ }, }) +const renderChartTool = tool({ + description: 'Render a bar chart summarizing Supabase metrics for the user.', + inputSchema: chartSchema, + execute: async () => { + return { + success: true, + message: 'Chart has been shown to the user', + } + }, +}) + const localTools = { renderRow: renderRowTool, + renderChart: renderChartTool, } type ChatRequestBody = { diff --git a/apps/ui-library/registry/default/blocks/assistant/supabase/functions/mcp-server/tools/count-tasks.ts b/apps/ui-library/registry/default/blocks/assistant/supabase/functions/mcp-server/tools/count-tasks.ts new file mode 100644 index 00000000000..8f68f669a0b --- /dev/null +++ b/apps/ui-library/registry/default/blocks/assistant/supabase/functions/mcp-server/tools/count-tasks.ts @@ -0,0 +1,79 @@ +import { z } from 'zod' +import type { ToolDefinition } from './types.ts' + +type CountTasksInput = { + createdAtStart?: string + createdAtEnd?: string + completedAtStart?: string + completedAtEnd?: string +} + +export const countTasksTool: ToolDefinition = { + name: 'countTasks', + description: + 'Count tasks that were created and/or completed between the provided date ranges. Dates should be ISO strings (YYYY-MM-DD or full timestamp).', + authErrorMessage: 'Error: Authentication required to count tasks.', + inputSchema: z.object({ + createdAtStart: z + .string() + .optional() + .describe('Lower bound for created_at (inclusive). Accepts ISO dates or timestamps.'), + createdAtEnd: z + .string() + .optional() + .describe('Upper bound for created_at (inclusive). Accepts ISO dates or timestamps.'), + completedAtStart: z + .string() + .optional() + .describe('Lower bound for completed_at (inclusive). Accepts ISO dates or timestamps.'), + completedAtEnd: z + .string() + .optional() + .describe('Upper bound for completed_at (inclusive). Accepts ISO dates or timestamps.'), + }), + run: async (supabase, { createdAtStart, createdAtEnd, completedAtStart, completedAtEnd }) => { + try { + const { data, error } = await supabase.rpc('count_tasks', { + created_at_start: createdAtStart ?? null, + created_at_end: createdAtEnd ?? null, + completed_at_start: completedAtStart ?? null, + completed_at_end: completedAtEnd ?? null, + }) + + if (error) { + return { + content: [ + { + type: 'text', + text: `Error counting tasks: ${error.message}`, + }, + ], + isError: true, + } + } + + const total = Array.isArray(data) + ? (data[0]?.total as number | undefined) ?? 0 + : (data as number | null) ?? 0 + + return { + content: [ + { + type: 'text', + text: `Found ${total} task(s) matching the provided filters.`, + }, + ], + } + } catch (err) { + return { + content: [ + { + type: 'text', + text: `Unexpected error: ${err instanceof Error ? err.message : String(err)}`, + }, + ], + isError: true, + } + } + }, +} diff --git a/apps/ui-library/registry/default/blocks/assistant/supabase/functions/mcp-server/tools/registry.ts b/apps/ui-library/registry/default/blocks/assistant/supabase/functions/mcp-server/tools/registry.ts index 81120ec968b..b9e512eab6a 100644 --- a/apps/ui-library/registry/default/blocks/assistant/supabase/functions/mcp-server/tools/registry.ts +++ b/apps/ui-library/registry/default/blocks/assistant/supabase/functions/mcp-server/tools/registry.ts @@ -1,6 +1,7 @@ import { createTaskTool } from './create-task.ts' import { deleteTaskTool } from './delete-task.ts' import { getUserTasksTool } from './get-user-tasks.ts' +import { countTasksTool } from './count-tasks.ts' import { updateTaskTool } from './update-task.ts' import type { ToolDefinition } from './types.ts' @@ -9,4 +10,5 @@ export const toolRegistry: ToolDefinition[] = [ createTaskTool, updateTaskTool, deleteTaskTool, + countTasksTool, ]