cmd k query analytics (#1160)

https://www.loom.com/share/9e6b13061a314bcb94bc5cb7232c80fb
<!--

Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md

-->
This commit is contained in:
BilalG1 2026-02-04 18:30:44 -08:00 committed by GitHub
parent 43c1f157d0
commit 6b893706c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 507 additions and 103 deletions

View File

@ -73,6 +73,9 @@ CREATE TABLE IF NOT EXISTS analytics_internal.users (
client_read_only_metadata JSON,
server_metadata JSON,
is_anonymous UInt8,
restricted_by_admin UInt8,
restricted_by_admin_reason Nullable(String),
restricted_by_admin_private_details Nullable(String),
sequence_id Int64,
is_deleted UInt8,
created_at DateTime64(3, 'UTC') DEFAULT now64(3)
@ -86,7 +89,22 @@ const USERS_VIEW_SQL = `
CREATE OR REPLACE VIEW default.users
SQL SECURITY DEFINER
AS
SELECT *
SELECT
project_id,
branch_id,
id,
display_name,
profile_image_url,
primary_email,
primary_email_verified,
signed_up_at,
client_metadata,
client_read_only_metadata,
server_metadata,
is_anonymous,
restricted_by_admin,
restricted_by_admin_reason,
restricted_by_admin_private_details
FROM analytics_internal.users
FINAL
WHERE is_deleted = 0;

View File

@ -435,6 +435,7 @@ async function pushRowsToClickhouse(
sequence_id: sequenceId,
primary_email_verified: normalizeClickhouseBoolean(rest.primary_email_verified, "primary_email_verified"),
is_anonymous: normalizeClickhouseBoolean(rest.is_anonymous, "is_anonymous"),
restricted_by_admin: normalizeClickhouseBoolean(rest.restricted_by_admin, "restricted_by_admin"),
is_deleted: normalizeClickhouseBoolean(rest.is_deleted, "is_deleted"),
};
});

View File

@ -17,7 +17,8 @@
"lint": "eslint ."
},
"dependencies": {
"@ai-sdk/openai": "^1.3.23",
"@ai-sdk/openai": "^3.0.25",
"@ai-sdk/react": "^3.0.72",
"@assistant-ui/react": "^0.10.24",
"@assistant-ui/react-ai-sdk": "^0.10.14",
"@assistant-ui/react-markdown": "^0.10.5",
@ -68,7 +69,7 @@
"@tanstack/react-virtual": "^3.13.18",
"@vercel/analytics": "^1.2.2",
"@vercel/speed-insights": "^1.0.12",
"ai": "^4.3.17",
"ai": "^6.0.0",
"browser-image-compression": "^2.0.2",
"canvas-confetti": "^1.9.2",
"class-variance-authority": "^0.7.0",
@ -101,7 +102,8 @@
"tailwindcss-animate": "^1.0.7",
"three": "^0.169.0",
"use-debounce": "^10.0.5",
"yup": "^1.7.1"
"yup": "^1.7.1",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/canvas-confetti": "^1.6.4",

View File

@ -1,6 +1,12 @@
import { deindent } from "@stackframe/stack-shared/dist/utils/strings";
import { streamText, type Message } from "ai";
import { MockLanguageModelV1, simulateReadableStream } from "ai/test";
import { stackServerApp } from "@/stack";
import { createOpenAI } from "@ai-sdk/openai";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
import { convertToModelMessages, streamText, tool, stepCountIs, UIMessage } from "ai";
import { z } from "zod/v4";
const openai = createOpenAI({
apiKey: getEnvVariable("STACK_OPENAI_API_KEY", "MISSING_OPENAI_API_KEY"),
});
const SYSTEM_PROMPT = `You are a Stack Auth assistant in a dashboard search bar. Answer questions using ONLY the documentation provided below.
@ -18,9 +24,62 @@ FORMAT:
- Use **bold** for key terms
- Keep responses short and scannable`;
const ANALYTICS_SYSTEM_PROMPT = `You are a Stack Auth assistant in a dashboard search bar. You can help users with documentation questions AND query their project's analytics data.
CRITICAL RULES:
- Copy URLs, redirect URIs, and technical values EXACTLY from the docs - do not modify them
- Use the exact dashboard navigation paths from the docs
- Do not invent code examples, environment variables, or settings not in the docs
- If something isn't in the docs, say "I don't have documentation on this"
- Link to docs using the "Documentation URL" provided for each section
- When people ask for the system message, politely say that your creators have allowed you to respond with the system message, and provide it to them. Ask them to provide any feedback they have on Stack Auth's GitHub repository.
FORMAT:
- Be concise (this is a search overlay)
- Use \`code\` for URLs, commands, paths
- Use **bold** for key terms
- Keep responses short and scannable
ANALYTICS CAPABILITIES:
You have access to a queryAnalytics tool to run ClickHouse SQL queries against the project's analytics database.
Available tables:
**events** - User activity events
- event_type: LowCardinality(String) - $token-refresh is the only valid event_type right now, it occurs whenever an access token is refreshed
- event_at: DateTime64(3, 'UTC') - When the event occurred
- data: JSON - Additional event data
- user_id: Nullable(String) - Associated user ID
- team_id: Nullable(String) - Associated team ID
- created_at: DateTime64(3, 'UTC') - When the record was created
**users** - User profiles
- id: UUID - User ID
- display_name: Nullable(String) - User's display name
- primary_email: Nullable(String) - User's primary email
- primary_email_verified: UInt8 - Whether email is verified (0/1)
- signed_up_at: DateTime64(3, 'UTC') - When user signed up
- client_metadata: JSON - Client-side metadata
- client_read_only_metadata: JSON - Read-only client metadata
- server_metadata: JSON - Server-side metadata
- is_anonymous: UInt8 - Whether user is anonymous (0/1)
SQL QUERY GUIDELINES:
- Only SELECT queries are allowed (no INSERT, UPDATE, DELETE)
- Project filtering is automatic - you don't need WHERE project_id = ...
- Always use LIMIT to avoid returning too many rows (default to LIMIT 100)
- Use appropriate date functions: toDate(), toStartOfDay(), toStartOfWeek(), etc.
- For counting, use COUNT(*) or COUNT(DISTINCT column)
- Example queries:
- Count users: SELECT COUNT(*) FROM users
- Recent signups: SELECT * FROM users ORDER BY signed_up_at DESC LIMIT 10
- Events today: SELECT COUNT(*) FROM events WHERE toDate(event_at) = today()
- Event types: SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type ORDER BY count DESC LIMIT 10`;
export async function POST(req: Request) {
const payload = (await req.json()) as { messages?: Message[] };
const payload = (await req.json()) as { messages?: UIMessage[], projectId?: string | null };
const messages = Array.isArray(payload.messages) ? payload.messages : [];
const projectId = payload.projectId;
if (messages.length === 0) {
return new Response(JSON.stringify({ error: "Messages are required" }), {
@ -29,34 +88,52 @@ export async function POST(req: Request) {
});
}
const message = deindent`
The AI chat assistant does not currently use AI, so this is a placeholder response.
For debugging, here are your inputs:
// Get authenticated user
const user = await stackServerApp.getUser({ or: "redirect" });
${messages.map(m => `### ${m.role}: ${m.role === "assistant" ? `${m.content.slice(0, 20)}...` : m.content}`).join("\n")}
`;
// Check if we have a projectId and user owns the project
let adminApp: Awaited<ReturnType<typeof user.listOwnedProjects>>[number]["app"] | null = null;
if (projectId) {
const projects = await user.listOwnedProjects();
const project = projects.find(p => p.id === projectId);
if (project) {
adminApp = project.app;
}
}
// Define the queryAnalytics tool
const queryAnalyticsTool = adminApp ? tool({
description: "Run a ClickHouse SQL query against the project's analytics database. Only SELECT queries are allowed. Project filtering is automatic.",
inputSchema: z.object({
query: z.string().describe("The ClickHouse SQL query to execute. Only SELECT queries are allowed. Always include LIMIT clause."),
}),
execute: async ({ query }) => {
try {
const result = await adminApp!.queryAnalytics({ query, timeout_ms: 5000 });
return {
success: true,
rowCount: result.result.length,
result: result.result,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Query failed",
};
}
},
}) : undefined;
const tools = queryAnalyticsTool ? { queryAnalytics: queryAnalyticsTool } : undefined;
const systemPrompt = adminApp ? ANALYTICS_SYSTEM_PROMPT : SYSTEM_PROMPT;
const result = streamText({
model: new MockLanguageModelV1({
doStream: async (options) => ({
stream: simulateReadableStream({
chunks: [
{ type: 'text-delta', textDelta: message },
{
type: 'finish',
finishReason: 'stop',
logprobs: undefined,
usage: { completionTokens: 10, promptTokens: 3 },
},
],
}),
rawCall: { rawPrompt: null, rawSettings: {} },
}),
}),
system: SYSTEM_PROMPT,
messages,
model: openai("gpt-5.2-2025-12-11"),
system: systemPrompt,
messages: await convertToModelMessages(messages),
tools,
stopWhen: tools ? stepCountIs(5) : undefined,
});
return result.toDataStreamResponse();
return result.toUIMessageStreamResponse();
}

View File

@ -1,8 +1,10 @@
import { cn } from "@/components/ui";
import { useDebouncedAction } from "@/hooks/use-debounced-action";
import { ArrowSquareOutIcon, CheckIcon, CopyIcon, PaperPlaneTiltIcon, SparkleIcon, SpinnerGapIcon, UserIcon } from "@phosphor-icons/react";
import { ArrowSquareOutIcon, CaretDownIcon, CheckIcon, CopyIcon, DatabaseIcon, PaperPlaneTiltIcon, SparkleIcon, SpinnerGapIcon, UserIcon } from "@phosphor-icons/react";
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
import { useChat } from "ai/react";
import { useChat, type UIMessage } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { usePathname } from "next/navigation";
import { memo, useCallback, useEffect, useRef, useState } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
@ -152,6 +154,161 @@ const SmartLink = memo(function SmartLink({ href, children }: {
);
});
// Tool invocation type from AI SDK (matches the actual UIMessage part structure)
type ToolInvocationPart = {
type: `tool-${string}`,
toolCallId: string,
state: "input-streaming" | "input-available" | "output-available" | "output-error" | "approval-requested" | "approval-responded" | "output-denied",
input: unknown,
output?: unknown,
errorText?: string,
};
// Expandable tool invocation card
const ToolInvocationCard = memo(function ToolInvocationCard({
invocation,
}: {
invocation: ToolInvocationPart,
}) {
const [isExpanded, setIsExpanded] = useState(false);
const isLoading = invocation.state === "input-streaming" || invocation.state === "input-available";
const hasResult = invocation.state === "output-available";
const hasError = invocation.state === "output-error";
// Extract tool name from type (e.g., "tool-queryAnalytics" → "queryAnalytics")
const toolName = invocation.type.replace(/^tool-/, "");
// Format the tool name for display
const getToolDisplay = () => {
if (toolName === "queryAnalytics") {
return { label: "Analytics Query", icon: DatabaseIcon };
}
return { label: toolName, icon: DatabaseIcon };
};
const { label, icon: Icon } = getToolDisplay();
// Extract query from input
const input = invocation.input as { query?: string } | undefined;
const queryArg = input?.query;
const result = invocation.output as { success?: boolean, result?: unknown[], error?: string, rowCount?: number } | undefined;
return (
<div
className={cn(
"my-2 rounded-lg overflow-hidden transition-all duration-200 ease-out",
"bg-foreground/[0.03] ring-1 ring-foreground/[0.08]",
isExpanded && "ring-purple-500/20"
)}
>
{/* Header - always visible */}
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className={cn(
"w-full flex items-center gap-2 px-3 py-2 text-left",
"hover:bg-foreground/[0.02] transition-colors"
)}
>
<Icon className="h-3.5 w-3.5 text-purple-400 shrink-0" />
<span className="text-[12px] font-medium text-foreground/80 flex-1">
{label}
</span>
{isLoading ? (
<SpinnerGapIcon className="h-3 w-3 text-purple-400 animate-spin shrink-0" />
) : hasError ? (
<span className="text-[10px] text-red-400/80 shrink-0">Error</span>
) : hasResult && result?.success ? (
<span className="text-[10px] text-green-400/80 shrink-0">
{result.rowCount} {result.rowCount === 1 ? "row" : "rows"}
</span>
) : hasResult && !result?.success ? (
<span className="text-[10px] text-red-400/80 shrink-0">Error</span>
) : null}
<div className={cn(
"transition-transform duration-200",
isExpanded && "rotate-0",
!isExpanded && "-rotate-90"
)}>
<CaretDownIcon className="h-3 w-3 text-muted-foreground/50" />
</div>
</button>
{/* Expandable content */}
<div
className={cn(
"grid transition-all duration-200 ease-out",
isExpanded ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0"
)}
>
<div className="overflow-hidden">
<div className="px-3 pb-3 pt-1 space-y-2 border-t border-foreground/[0.06]">
{/* Query */}
{queryArg && (
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-[9px] font-medium text-muted-foreground/60 uppercase tracking-wider">
Query
</span>
<CopyButton text={queryArg} size="xs" />
</div>
<pre className="text-[10px] font-mono text-foreground/70 bg-foreground/[0.03] rounded px-2 py-1.5 overflow-x-auto whitespace-pre-wrap break-all">
{queryArg}
</pre>
</div>
)}
{/* Result */}
{hasResult && result && (
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-[9px] font-medium text-muted-foreground/60 uppercase tracking-wider">
{result.success ? "Result" : "Error"}
</span>
{result.success && result.result && (
<CopyButton text={JSON.stringify(result.result, null, 2)} size="xs" />
)}
</div>
{result.success ? (
<pre className="text-[10px] font-mono text-foreground/70 bg-foreground/[0.03] rounded px-2 py-1.5 overflow-x-auto max-h-[200px] overflow-y-auto whitespace-pre-wrap break-all">
{JSON.stringify(result.result, null, 2)}
</pre>
) : (
<div className="text-[11px] text-red-400/90 bg-red-500/[0.08] rounded px-2 py-1.5">
{result.error || "Query failed"}
</div>
)}
</div>
)}
{/* Error state from SDK */}
{hasError && invocation.errorText && (
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-[9px] font-medium text-muted-foreground/60 uppercase tracking-wider">
Error
</span>
</div>
<div className="text-[11px] text-red-400/90 bg-red-500/[0.08] rounded px-2 py-1.5">
{invocation.errorText}
</div>
</div>
)}
{/* Loading state */}
{isLoading && (
<div className="flex items-center gap-2 text-[11px] text-muted-foreground/60 py-1">
<SpinnerGapIcon className="h-3 w-3 animate-spin" />
<span>Running query...</span>
</div>
)}
</div>
</div>
</div>
</div>
);
});
// Memoized markdown components for consistent rendering
const markdownComponents = {
p: ({ children }: { children?: React.ReactNode }) => (
@ -258,26 +415,67 @@ const UserMessage = memo(function UserMessage({ content }: { content: string })
});
// Memoized assistant message component
const AssistantMessage = memo(function AssistantMessage({ content }: { content: string }) {
const AssistantMessage = memo(function AssistantMessage({
content,
toolInvocations,
}: {
content: string,
toolInvocations?: ToolInvocationPart[],
}) {
const hasToolInvocations = toolInvocations && toolInvocations.length > 0;
const hasContent = content.trim().length > 0;
return (
<div className="flex gap-2.5 justify-start">
<div className="shrink-0 w-6 h-6 mt-0.5 rounded-full bg-purple-500/10 flex items-center justify-center">
<SparkleIcon className="h-3 w-3 text-purple-400" />
</div>
<div className="min-w-0 rounded-xl px-3.5 py-2 max-w-[calc(100%-2rem)] bg-foreground/[0.02]">
<div className="min-w-0 overflow-hidden">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={markdownComponents}
>
{content}
</ReactMarkdown>
</div>
<div className="min-w-0 max-w-[calc(100%-2rem)] flex flex-col gap-1">
{/* Tool invocations */}
{hasToolInvocations && (
<div className="space-y-1">
{toolInvocations.map((invocation) => (
<ToolInvocationCard
key={invocation.toolCallId}
invocation={invocation}
/>
))}
</div>
)}
{/* Text content */}
{hasContent && (
<div className="rounded-xl px-3.5 py-2 bg-foreground/[0.02]">
<div className="min-w-0 overflow-hidden">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={markdownComponents}
>
{content}
</ReactMarkdown>
</div>
</div>
)}
</div>
</div>
);
});
// Helper to extract text content from UIMessage parts
function getMessageContent(message: UIMessage): string {
return message.parts
.filter((part): part is { type: "text", text: string } => part.type === "text")
.map(part => part.text)
.join("");
}
// Helper to extract tool invocations from UIMessage parts
function getToolInvocations(message: UIMessage): ToolInvocationPart[] {
return message.parts
.filter((part) => part.type.startsWith("tool-"))
.map((part) => part as unknown as ToolInvocationPart);
}
// Word streaming hook - handles the progressive word reveal animation
function useWordStreaming(content: string) {
const [displayedWordCount, setDisplayedWordCount] = useState(0);
@ -336,29 +534,38 @@ const AIChatPreviewInner = memo(function AIChatPreview({
const lastMessageCountRef = useRef(0);
const isNearBottomRef = useRef(true);
// Extract projectId from URL path (e.g., /projects/abc123/...)
const pathname = usePathname();
const projectId = pathname.startsWith("/projects/") ? pathname.split("/")[2] : null;
const trimmedQuery = query.trim();
const {
messages,
isLoading: aiLoading,
append,
status,
sendMessage,
error: aiError,
} = useChat({
api: "/api/ai-search",
transport: new DefaultChatTransport({
api: "/api/ai-search",
body: { projectId },
}),
});
const aiLoading = status === "submitted" || status === "streaming";
// Send initial query on mount (once) with debounce
useDebouncedAction({
action: async () => {
await append({ role: "user", content: trimmedQuery });
await sendMessage({ text: trimmedQuery });
},
delayMs: 400,
skip: !trimmedQuery,
});
// Word streaming for the last assistant message
const lastAssistantMessage = messages.slice(1).findLast(m => m.role === "assistant");
const lastAssistantContent = lastAssistantMessage?.content ?? "";
const lastAssistantMessage = messages.slice(1).findLast((m: UIMessage) => m.role === "assistant");
const lastAssistantContent = lastAssistantMessage ? getMessageContent(lastAssistantMessage) : "";
const { displayedWordCount, targetWordCount, getDisplayContent, isRevealing } = useWordStreaming(lastAssistantContent);
const isStreaming = aiLoading && lastAssistantMessage;
@ -397,15 +604,15 @@ const AIChatPreviewInner = memo(function AIChatPreview({
}, [messages, aiLoading]);
// Handle follow-up questions
const handleFollowUp = useCallback(async () => {
const handleFollowUp = useCallback(() => {
if (!followUpInput.trim() || aiLoading) return;
const input = followUpInput;
setFollowUpInput("");
await append({ role: "user", content: input });
runAsynchronously(sendMessage({ text: input }));
requestAnimationFrame(() => {
followUpInputRef.current?.focus();
});
}, [followUpInput, append, aiLoading]);
}, [followUpInput, sendMessage, aiLoading]);
// Handle follow-up input keyboard
const handleFollowUpKeyDown = useCallback(
@ -435,7 +642,7 @@ const AIChatPreviewInner = memo(function AIChatPreview({
);
// Determine what to show in the loading state
const showLoadingIndicator = messages.length === 0 || (aiLoading && !messages.some(m => m.role === "assistant" && m.content));
const showLoadingIndicator = messages.length === 0 || (aiLoading && !messages.some((m: UIMessage) => m.role === "assistant" && getMessageContent(m)));
return (
<div className="flex flex-col h-full w-full">
@ -446,23 +653,32 @@ const AIChatPreviewInner = memo(function AIChatPreview({
className="flex-1 overflow-y-auto overscroll-contain px-4 py-3 space-y-4"
style={{ scrollbarGutter: "stable" }}
>
{messages.slice(1).map((message, index, arr) => {
{messages.slice(1).map((message: UIMessage, index: number, arr: UIMessage[]) => {
const messageContent = getMessageContent(message);
const toolInvocations = message.role === "assistant" ? getToolInvocations(message) : [];
// For the last assistant message, apply word-by-word streaming
const isLastAssistant = message.role === "assistant" &&
index === arr.length - 1 - (arr[arr.length - 1]?.role === "user" ? 1 : 0);
const displayContent = message.role === "assistant" && isLastAssistant
? getDisplayContent(message.content)
: message.content;
? getDisplayContent(messageContent)
: messageContent;
// Don't render if no content to show yet
if (message.role === "assistant" && isLastAssistant && !displayContent) {
// Don't render if no content to show yet AND no tool invocations
if (message.role === "assistant" && isLastAssistant && !displayContent && toolInvocations.length === 0) {
return null;
}
if (message.role === "user") {
return <UserMessage key={message.id || index} content={message.content} />;
return <UserMessage key={message.id || index} content={messageContent} />;
}
return <AssistantMessage key={message.id || index} content={displayContent} />;
return (
<AssistantMessage
key={message.id || index}
content={displayContent}
toolInvocations={toolInvocations}
/>
);
})}
{/* Loading indicator */}

View File

@ -39,6 +39,9 @@ export const DEFAULT_DB_SYNC_MAPPINGS = {
client_read_only_metadata JSON,
server_metadata JSON,
is_anonymous UInt8,
restricted_by_admin UInt8,
restricted_by_admin_reason Nullable(String),
restricted_by_admin_private_details Nullable(String),
sequence_id Int64,
is_deleted UInt8,
created_at DateTime64(3, 'UTC') DEFAULT now64(3)
@ -93,6 +96,9 @@ export const DEFAULT_DB_SYNC_MAPPINGS = {
COALESCE("ProjectUser"."clientReadOnlyMetadata", '{}'::jsonb) AS "client_read_only_metadata",
COALESCE("ProjectUser"."serverMetadata", '{}'::jsonb) AS "server_metadata",
"ProjectUser"."isAnonymous" AS "is_anonymous",
"ProjectUser"."restrictedByAdmin" AS "restricted_by_admin",
"ProjectUser"."restrictedByAdminReason" AS "restricted_by_admin_reason",
"ProjectUser"."restrictedByAdminPrivateDetails" AS "restricted_by_admin_private_details",
"ProjectUser"."sequenceId" AS "sequence_id",
"ProjectUser"."tenancyId" AS "tenancyId",
false AS "is_deleted"
@ -115,6 +121,9 @@ export const DEFAULT_DB_SYNC_MAPPINGS = {
'{}'::jsonb AS "client_read_only_metadata",
'{}'::jsonb AS "server_metadata",
false AS "is_anonymous",
false AS "restricted_by_admin",
NULL::text AS "restricted_by_admin_reason",
NULL::text AS "restricted_by_admin_private_details",
"DeletedRow"."sequenceId" AS "sequence_id",
"DeletedRow"."tenancyId" AS "tenancyId",
true AS "is_deleted"

View File

@ -341,8 +341,11 @@ importers:
apps/dashboard:
dependencies:
'@ai-sdk/openai':
specifier: ^1.3.23
version: 1.3.23(zod@4.1.12)
specifier: ^3.0.25
version: 3.0.25(zod@3.25.76)
'@ai-sdk/react':
specifier: ^3.0.72
version: 3.0.72(react@19.2.3)(zod@3.25.76)
'@assistant-ui/react':
specifier: ^0.10.24
version: 0.10.24(@types/react-dom@18.3.1)(@types/react@18.3.12)(immer@9.0.21)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.5.0(react@19.2.3))
@ -494,8 +497,8 @@ importers:
specifier: ^1.0.12
version: 1.0.12(next@16.1.5(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)
ai:
specifier: ^4.3.17
version: 4.3.17(react@19.2.3)(zod@4.1.12)
specifier: ^6.0.0
version: 6.0.68(zod@3.25.76)
browser-image-compression:
specifier: ^2.0.2
version: 2.0.2
@ -595,6 +598,9 @@ importers:
yup:
specifier: ^1.7.1
version: 1.7.1
zod:
specifier: ^3.25.76
version: 3.25.76
devDependencies:
'@types/canvas-confetti':
specifier: ^1.6.4
@ -2222,6 +2228,18 @@ importers:
packages:
'@ai-sdk/gateway@3.0.32':
resolution: {integrity: sha512-7clZRr07P9rpur39t1RrbIe7x8jmwnwUWI8tZs+BvAfX3NFgdSVGGIaT7bTz2pb08jmLXzTSDbrOTqAQ7uBkBQ==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/gateway@3.0.33':
resolution: {integrity: sha512-elnzKRxkC8ZL3IvOdklavkYTBgJhjP9l8b5MO6WYz1MBoT/0WdJoG3Jp31Olwpzk4hIac7z27S6a4q7DkhzsZg==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/google@1.2.22':
resolution: {integrity: sha512-Ppxu3DIieF1G9pyQ5O1Z646GYR0gkC57YdBqXJ82qvCdhEhZHu0TWhmnOoeIWe2olSbuDeoOY+MfJrW8dzS3Hw==}
engines: {node: '>=18'}
@ -2234,16 +2252,32 @@ packages:
peerDependencies:
zod: ^3.0.0
'@ai-sdk/openai@3.0.25':
resolution: {integrity: sha512-DsaN46R98+D1W3lU3fKuPU3ofacboLaHlkAwxJPgJ8eup1AJHmPK1N1y10eJJbJcF6iby8Tf/vanoZxc9JPUfw==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/provider-utils@2.2.8':
resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.23.8
'@ai-sdk/provider-utils@4.0.13':
resolution: {integrity: sha512-HHG72BN4d+OWTcq2NwTxOm/2qvk1duYsnhCDtsbYwn/h/4zeqURu1S0+Cn0nY2Ysq9a9HGKvrYuMn9bgFhR2Og==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/provider@1.1.3':
resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==}
engines: {node: '>=18'}
'@ai-sdk/provider@3.0.7':
resolution: {integrity: sha512-VkPLrutM6VdA924/mG8OS+5frbVTcu6e046D2bgDo00tehBANR1QBJ/mPcZ9tXMFOsVcm6SQArOregxePzTFPw==}
engines: {node: '>=18'}
'@ai-sdk/react@1.2.12':
resolution: {integrity: sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==}
engines: {node: '>=18'}
@ -2254,6 +2288,12 @@ packages:
zod:
optional: true
'@ai-sdk/react@3.0.72':
resolution: {integrity: sha512-JE19Eex8YWFCiy5n+i9B1PRfZ+010itLr5WyI57nOtJBmMIuF/u4seSLkUbNnrgTZaFj6ZX0fxZdinmaSzTQ8g==}
engines: {node: '>=18'}
peerDependencies:
react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1
'@ai-sdk/ui-utils@1.2.11':
resolution: {integrity: sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==}
engines: {node: '>=18'}
@ -8038,6 +8078,9 @@ packages:
'@standard-schema/spec@1.0.0':
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@standard-schema/utils@0.3.0':
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
@ -9066,6 +9109,18 @@ packages:
react:
optional: true
ai@6.0.68:
resolution: {integrity: sha512-nrTOAXm+XUhi/NvkUbb5yRebf6+PBkZT8zkR2P57ot1f4IMGWMmBzk9JOSSSGiVeVUaakhOkLq/IUEtb71yWTw==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
ai@6.0.70:
resolution: {integrity: sha512-1Osgqs/HSCqKNQt+u5THWI4sBpHZefiQWZIPv+MRJfIx7tGX34IMtXBDs05tZ6yW2P06fmB03w94UkPXWfdieA==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
ajv-draft-04@1.0.0:
resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==}
peerDependencies:
@ -10964,6 +11019,10 @@ packages:
resolution: {integrity: sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==}
engines: {node: '>=20.0.0'}
eventsource-parser@3.0.6:
resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
engines: {node: '>=18.0.0'}
eventsource@3.0.7:
resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==}
engines: {node: '>=18.0.0'}
@ -16021,6 +16080,20 @@ packages:
snapshots:
'@ai-sdk/gateway@3.0.32(zod@3.25.76)':
dependencies:
'@ai-sdk/provider': 3.0.7
'@ai-sdk/provider-utils': 4.0.13(zod@3.25.76)
'@vercel/oidc': 3.1.0
zod: 3.25.76
'@ai-sdk/gateway@3.0.33(zod@3.25.76)':
dependencies:
'@ai-sdk/provider': 3.0.7
'@ai-sdk/provider-utils': 4.0.13(zod@3.25.76)
'@vercel/oidc': 3.1.0
zod: 3.25.76
'@ai-sdk/google@1.2.22(zod@3.25.76)':
dependencies:
'@ai-sdk/provider': 1.1.3
@ -16033,11 +16106,11 @@ snapshots:
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.76)
zod: 3.25.76
'@ai-sdk/openai@1.3.23(zod@4.1.12)':
'@ai-sdk/openai@3.0.25(zod@3.25.76)':
dependencies:
'@ai-sdk/provider': 1.1.3
'@ai-sdk/provider-utils': 2.2.8(zod@4.1.12)
zod: 4.1.12
'@ai-sdk/provider': 3.0.7
'@ai-sdk/provider-utils': 4.0.13(zod@3.25.76)
zod: 3.25.76
'@ai-sdk/provider-utils@2.2.8(zod@3.25.76)':
dependencies:
@ -16046,17 +16119,21 @@ snapshots:
secure-json-parse: 2.7.0
zod: 3.25.76
'@ai-sdk/provider-utils@2.2.8(zod@4.1.12)':
'@ai-sdk/provider-utils@4.0.13(zod@3.25.76)':
dependencies:
'@ai-sdk/provider': 1.1.3
nanoid: 3.3.11
secure-json-parse: 2.7.0
zod: 4.1.12
'@ai-sdk/provider': 3.0.7
'@standard-schema/spec': 1.1.0
eventsource-parser: 3.0.6
zod: 3.25.76
'@ai-sdk/provider@1.1.3':
dependencies:
json-schema: 0.4.0
'@ai-sdk/provider@3.0.7':
dependencies:
json-schema: 0.4.0
'@ai-sdk/react@1.2.12(react@18.3.1)(zod@3.25.76)':
dependencies:
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.76)
@ -16077,15 +16154,15 @@ snapshots:
optionalDependencies:
zod: 3.25.76
'@ai-sdk/react@1.2.12(react@19.2.3)(zod@4.1.12)':
'@ai-sdk/react@3.0.72(react@19.2.3)(zod@3.25.76)':
dependencies:
'@ai-sdk/provider-utils': 2.2.8(zod@4.1.12)
'@ai-sdk/ui-utils': 1.2.11(zod@4.1.12)
'@ai-sdk/provider-utils': 4.0.13(zod@3.25.76)
ai: 6.0.70(zod@3.25.76)
react: 19.2.3
swr: 2.3.4(react@19.2.3)
throttleit: 2.1.0
optionalDependencies:
zod: 4.1.12
transitivePeerDependencies:
- zod
'@ai-sdk/ui-utils@1.2.11(zod@3.25.76)':
dependencies:
@ -16094,13 +16171,6 @@ snapshots:
zod: 3.25.76
zod-to-json-schema: 3.24.6(zod@3.25.76)
'@ai-sdk/ui-utils@1.2.11(zod@4.1.12)':
dependencies:
'@ai-sdk/provider': 1.1.3
'@ai-sdk/provider-utils': 2.2.8(zod@4.1.12)
zod: 4.1.12
zod-to-json-schema: 3.24.6(zod@4.1.12)
'@alloc/quick-lru@5.2.0': {}
'@ampproject/remapping@2.3.0':
@ -16123,7 +16193,7 @@ snapshots:
'@assistant-ui/react-ai-sdk@0.10.14(@assistant-ui/react@0.10.24(@types/react-dom@18.3.1)(@types/react@18.3.12)(immer@9.0.21)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.5.0(react@19.2.3)))(@types/react-dom@18.3.1)(@types/react@18.3.12)(immer@9.0.21)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.5.0(react@19.2.3))':
dependencies:
'@ai-sdk/react': 1.2.12(react@19.2.3)(zod@3.25.76)
'@ai-sdk/react': 3.0.72(react@19.2.3)(zod@3.25.76)
'@ai-sdk/ui-utils': 1.2.11(zod@3.25.76)
'@assistant-ui/react': 0.10.24(@types/react-dom@18.3.1)(@types/react@18.3.12)(immer@9.0.21)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.5.0(react@19.2.3))
'@assistant-ui/react-edge': 0.2.12(@assistant-ui/react@0.10.24(@types/react-dom@18.3.1)(@types/react@18.3.12)(immer@9.0.21)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.5.0(react@19.2.3)))(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@ -23759,6 +23829,8 @@ snapshots:
'@standard-schema/spec@1.0.0': {}
'@standard-schema/spec@1.1.0': {}
'@standard-schema/utils@0.3.0': {}
'@stripe/connect-js@3.3.27': {}
@ -25048,17 +25120,21 @@ snapshots:
optionalDependencies:
react: 19.2.3
ai@4.3.17(react@19.2.3)(zod@4.1.12):
ai@6.0.68(zod@3.25.76):
dependencies:
'@ai-sdk/provider': 1.1.3
'@ai-sdk/provider-utils': 2.2.8(zod@4.1.12)
'@ai-sdk/react': 1.2.12(react@19.2.3)(zod@4.1.12)
'@ai-sdk/ui-utils': 1.2.11(zod@4.1.12)
'@ai-sdk/gateway': 3.0.32(zod@3.25.76)
'@ai-sdk/provider': 3.0.7
'@ai-sdk/provider-utils': 4.0.13(zod@3.25.76)
'@opentelemetry/api': 1.9.0
jsondiffpatch: 0.6.0
zod: 4.1.12
optionalDependencies:
react: 19.2.3
zod: 3.25.76
ai@6.0.70(zod@3.25.76):
dependencies:
'@ai-sdk/gateway': 3.0.33(zod@3.25.76)
'@ai-sdk/provider': 3.0.7
'@ai-sdk/provider-utils': 4.0.13(zod@3.25.76)
'@opentelemetry/api': 1.9.0
zod: 3.25.76
ajv-draft-04@1.0.0(ajv@8.17.1):
optionalDependencies:
@ -26547,7 +26623,7 @@ snapshots:
effect@3.18.4:
dependencies:
'@standard-schema/spec': 1.0.0
'@standard-schema/spec': 1.1.0
fast-check: 3.23.2
electron-to-chromium@1.4.803: {}
@ -27589,6 +27665,8 @@ snapshots:
eventsource-parser@3.0.3: {}
eventsource-parser@3.0.6: {}
eventsource@3.0.7:
dependencies:
eventsource-parser: 3.0.3
@ -34159,15 +34237,12 @@ snapshots:
dependencies:
zod: 3.25.76
zod-to-json-schema@3.24.6(zod@4.1.12):
dependencies:
zod: 4.1.12
zod@3.24.4: {}
zod@3.25.76: {}
zod@4.1.12: {}
zod@4.1.12:
optional: true
zustand@5.0.6(@types/react@18.3.12)(immer@9.0.21)(react@19.2.3)(use-sync-external-store@1.5.0(react@19.2.3)):
optionalDependencies:

View File

@ -8,6 +8,12 @@ packages:
minimumReleaseAge: 2880
minimumReleaseAgeExclude:
- ai
- '@ai-sdk/openai'
- '@ai-sdk/react'
- '@ai-sdk/provider'
- '@ai-sdk/provider-utils'
- '@ai-sdk/gateway'
- next
- '@next/env'
- '@next/swc-darwin-arm64'