mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
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:
parent
43c1f157d0
commit
6b893706c8
@ -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;
|
||||
|
||||
@ -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"),
|
||||
};
|
||||
});
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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 */}
|
||||
|
||||
@ -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"
|
||||
|
||||
157
pnpm-lock.yaml
157
pnpm-lock.yaml
@ -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:
|
||||
|
||||
@ -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'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user