diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/ai-query-bar.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/ai-query-bar.tsx deleted file mode 100644 index 5c628c8fe..000000000 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/ai-query-bar.tsx +++ /dev/null @@ -1,134 +0,0 @@ -"use client"; - -import { SimpleTooltip } from "@/components/ui/simple-tooltip"; -import { cn } from "@/lib/utils"; -import { - EyeIcon, - PaperPlaneTiltIcon, - SparkleIcon, - SpinnerGapIcon, - XIcon, -} from "@phosphor-icons/react"; -import { runAsynchronously } from "@hexclave/shared/dist/utils/promises"; -import { useCallback, useState, type KeyboardEvent } from "react"; -import type { AiQueryChat } from "./use-ai-query-chat"; - -type AiQueryBarProps = { - chat: AiQueryChat, - /** Whether the AI has committed a query (drives the purple accent). */ - isActive: boolean, - /** Invoked when the user clicks the eye button. */ - onOpenDialog: () => void, - /** Invoked when the user clicks the reset (clear) button. */ - onReset: () => void, -}; - -/** - * AI-powered search bar for the analytics tables page. Drops into the - * DataGridToolbar in place of the built-in quick-search input. Typing - * a message and pressing Enter sends a prompt to the shared AI chat; - * the AI responds by calling the `queryAnalytics` tool, and the - * extracted query drives the grid (via the parent component). - */ -export function AiQueryBar({ - chat, - isActive, - onOpenDialog, - onReset, -}: AiQueryBarProps) { - const [input, setInput] = useState(""); - - const handleSubmit = useCallback(() => { - const text = input.trim(); - if (!text || chat.isResponding) return; - setInput(""); - runAsynchronously(chat.sendMessage({ text })); - }, [input, chat]); - - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - handleSubmit(); - } - }, - [handleSubmit], - ); - - return ( -
-
- - setInput(e.target.value)} - onKeyDown={handleKeyDown} - placeholder={ - isActive ? "Refine with AI…" : "Ask about your analytics…" - } - className={cn( - "min-w-0 flex-1 bg-transparent text-xs outline-none", - "placeholder:text-muted-foreground/40", - )} - disabled={chat.isResponding} - /> - {chat.isResponding && ( - - )} - {!chat.isResponding && input.trim().length > 0 && ( - - )} - - - -
- - {isActive && ( - - - - )} -
- ); -} diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/ai-query-dialog.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/ai-query-dialog.tsx deleted file mode 100644 index 6273cc4ee..000000000 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/ai-query-dialog.tsx +++ /dev/null @@ -1,483 +0,0 @@ -"use client"; - -import { MarkdownText } from "@/components/assistant-ui/markdown-text"; -import { Thread } from "@/components/assistant-ui/thread"; -import { ToolFallback } from "@/components/assistant-ui/tool-fallback"; -import type { CmdKPreviewProps } from "@/components/cmdk-commands"; -import { Button } from "@/components/ui"; -import { - Dialog, - DialogBody, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { SimpleTooltip } from "@/components/ui/simple-tooltip"; -import { Textarea } from "@/components/ui/textarea"; -import { CreateDashboardPreview } from "@/components/commands/create-dashboard/create-dashboard-preview"; -import { useUpdateConfig } from "@/components/config-update"; -import { AssistantRuntimeProvider, type ToolCallContentPartProps } from "@assistant-ui/react"; -import { - ArrowClockwiseIcon, - CheckIcon, - CopyIcon, - FloppyDiskIcon, - LayoutIcon, - SparkleIcon, - TrashIcon, - XIcon, -} from "@phosphor-icons/react"; -import { generateSecureRandomString } from "@hexclave/shared/dist/utils/crypto"; -import { - runAsynchronously, - runAsynchronouslyWithAlert, -} from "@hexclave/shared/dist/utils/promises"; -import { - useCallback, - useEffect, - useMemo, - useState, -} from "react"; -import { useAdminApp } from "../../use-admin-app"; -import type { AiQueryChat } from "./use-ai-query-chat"; - -function AnalyticsQueryToolCall( - props: ToolCallContentPartProps & { - currentQuery: string | null, - onApplyQuery: (query: string) => void, - }, -) { - const query = (props.args as { query?: unknown } | undefined)?.query; - const queryString = typeof query === "string" ? query : ""; - const result = props.result as { success?: unknown } | null | undefined; - const isSuccessful = props.status.type === "complete" && result?.success !== false; - const canApply = isSuccessful && queryString.trim().length > 0 && queryString !== props.currentQuery; - - return ( - e.stopPropagation()}> - - - - - ) : undefined} - /> - ); -} - -function AiQueryWelcome() { - return ( -
-
-
- -
-

- Build an analytics query -

-

- Ask for a table, segment, trend, or funnel. Try “daily signups over the last 30 days” or “top 10 users by event count this week”. -

-
-
- ); -} - -// ─── Save query sub-dialog ────────────────────────────────────────── - -function SaveQueryInlineDialog({ - open, - onOpenChange, - sqlQuery, -}: { - open: boolean, - onOpenChange: (open: boolean) => void, - sqlQuery: string, -}) { - const adminApp = useAdminApp(); - const project = adminApp.useProject(); - const config = project.useConfig(); - const updateConfig = useUpdateConfig(); - - const [displayName, setDisplayName] = useState(""); - const [saving, setSaving] = useState(false); - - useEffect(() => { - if (!open) { - setDisplayName(""); - setSaving(false); - } - }, [open]); - - const handleSave = useCallback(async () => { - if (!displayName.trim() || !sqlQuery.trim()) return; - setSaving(true); - try { - // Reuse an existing folder if available, otherwise create an - // "AI Queries" bucket on the fly so the save flow never stalls - // on folder management. - const existingFolders = Object.entries(config.analytics.queryFolders); - let folderId: string; - if (existingFolders.length > 0) { - folderId = existingFolders[0]![0]; - } else { - folderId = generateSecureRandomString(); - await updateConfig({ - adminApp, - configUpdate: { - [`analytics.queryFolders.${folderId}`]: { - displayName: "AI Queries", - sortOrder: 0, - queries: {}, - }, - }, - pushable: false, - }); - } - - const queryId = generateSecureRandomString(); - await updateConfig({ - adminApp, - configUpdate: { - [`analytics.queryFolders.${folderId}.queries.${queryId}`]: { - displayName: displayName.trim(), - sqlQuery, - }, - }, - pushable: false, - }); - onOpenChange(false); - } finally { - setSaving(false); - } - }, [displayName, sqlQuery, config, updateConfig, adminApp, onOpenChange]); - - return ( - - - - Save query - - -
-
- - setDisplayName(e.target.value)} - placeholder="Recent signups" - onKeyDown={(e) => { - if (e.key === "Enter") { - runAsynchronouslyWithAlert(handleSave); - } - }} - /> -
-
-
-                {sqlQuery}
-              
-
-
-
- - - - -
-
- ); -} - -// ─── Build dashboard sub-dialog ───────────────────────────────────── - -/** - * Reuses the existing `CreateDashboardPreview` component (the same - * one the Cmd+K command center uses) so the dashboard-builder - * experience is identical whether you enter it from the command - * palette or from the analytics AI query builder. Most of - * `CmdKPreviewProps` are unused by `CreateDashboardPreview` internally, - * so we pass no-op stubs for them. - */ -function BuildDashboardDialog({ - open, - onOpenChange, - sqlQuery, -}: { - open: boolean, - onOpenChange: (open: boolean) => void, - sqlQuery: string, -}) { - // Synthesize a prompt that pre-seeds the SQL query as context so - // the dashboard the AI generates visualizes exactly these results. - const dashboardPrompt = useMemo( - () => - `Build a dashboard that visualizes the results of this ClickHouse query:\n\n\`\`\`sql\n${sqlQuery}\n\`\`\``, - [sqlQuery], - ); - - const stubProps: Omit = { - isSelected: true, - registerOnFocus: () => { - // no-op - }, - unregisterOnFocus: () => { - // no-op - }, - onBlur: () => { - // no-op - }, - registerNestedCommands: () => { - // no-op - }, - navigateToNested: () => { - // no-op - }, - depth: 0, - pathname: "", - }; - - return ( - - - - - - Build dashboard - - -
- {open && ( - onOpenChange(false)} - {...stubProps} - /> - )} -
-
-
- ); -} - -// ─── Main dialog ──────────────────────────────────────────────────── - -type AiQueryDialogProps = { - open: boolean, - onOpenChange: (open: boolean) => void, - chat: AiQueryChat, - /** The query currently driving the data grid (may be `null` if none yet). */ - currentQuery: string | null, -}; - -export function AiQueryDialog({ - open, - onOpenChange, - chat, - currentQuery, -}: AiQueryDialogProps) { - const [copied, setCopied] = useState(false); - const [saveOpen, setSaveOpen] = useState(false); - const [buildOpen, setBuildOpen] = useState(false); - const [currentQueryDraft, setCurrentQueryDraft] = useState(currentQuery ?? ""); - const assistantContentComponents = useMemo(() => ({ - Text: MarkdownText, - tools: { - Fallback: ToolFallback, - by_name: { - queryAnalytics: (props: ToolCallContentPartProps) => ( - - ), - }, - }, - }), [chat.rewindToQuery, currentQuery]); - - useEffect(() => { - setCurrentQueryDraft(currentQuery ?? ""); - }, [currentQuery]); - - const applyCurrentQueryDraft = useCallback(() => { - if (currentQueryDraft.trim().length === 0 || currentQueryDraft === currentQuery) return; - chat.rewindToQuery(currentQueryDraft); - }, [chat, currentQuery, currentQueryDraft]); - - const handleCopy = useCallback(async () => { - if (!currentQueryDraft) return; - await navigator.clipboard.writeText(currentQueryDraft); - setCopied(true); - setTimeout(() => setCopied(false), 1500); - }, [currentQueryDraft]); - - const canActOnQuery = currentQueryDraft.trim().length > 0; - const hasUnappliedCurrentQueryEdits = currentQueryDraft.trim().length > 0 && currentQueryDraft !== (currentQuery ?? ""); - - return ( - <> - - - -
- - - AI query builder - -
- {chat.messages.length > 0 && ( - - - - )} - -
-
-
- -
-
- -
- {hasUnappliedCurrentQueryEdits && ( - - - - )} - {canActOnQuery && ( - - - - )} -
-
-
-