mirror of
https://github.com/supabase/supabase.git
synced 2026-10-12 21:05:06 +03:00
extra tool
This commit is contained in:
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 = {
|
||||
|
||||
+79
@@ -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,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
+2
@@ -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,
|
||||
]
|
||||
Reference in new issue
Block a user