extra tool

This commit is contained in:
Saxon Fletcher committed 2025-11-19 10:27:38 +10:00
1 parent 885ef4129e
commit f9c2a8462a
6 files changed
+215 -1

No files matched your search

@@ -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 <Chart key={`${role}-${i}`} {...payload} />
}
default:
return null
}
@@ -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<string, string | number>
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 (
<Card>
<CardHeader>
<CardTitle>{primaryText}</CardTitle>
{secondaryText ? <CardDescription>{secondaryText}</CardDescription> : null}
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig}>
<BarChart accessibilityLayer data={data}>
<CartesianGrid vertical={false} />
<XAxis
axisLine={false}
dataKey={xAxis}
tickFormatter={(value) => String(value).slice(0, 3)}
tickLine={false}
tickMargin={10}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
<Bar dataKey={yAxis} fill={fillColor} radius={8} />
</BarChart>
</ChartContainer>
</CardContent>
{(tertiaryText ?? null) ? (
<CardFooter className="flex-col items-start gap-2 text-sm">
<div className="text-muted-foreground leading-none">{tertiaryText}</div>
</CardFooter>
) : null}
</Card>
)
}
@@ -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",
@@ -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 = {
@@ -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<CountTasksInput> = {
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,
}
}
},
}
@@ -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,
]