diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/use-ai-query-chat.test.ts b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/use-ai-table-filter-chat.test.ts similarity index 97% rename from apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/use-ai-query-chat.test.ts rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/use-ai-table-filter-chat.test.ts index f941edc63..a785379fd 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/use-ai-query-chat.test.ts +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/use-ai-table-filter-chat.test.ts @@ -1,6 +1,6 @@ import type { ThreadMessage } from "@assistant-ui/react"; import { describe, expect, it } from "vitest"; -import { extractLatestQuery } from "./use-ai-query-chat"; +import { extractLatestQuery } from "./use-ai-table-filter-chat"; const fixture = (messages: Array<{ id: string, diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/use-ai-query-chat.ts b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/use-ai-table-filter-chat.ts similarity index 62% rename from apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/use-ai-query-chat.ts rename to apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/use-ai-table-filter-chat.ts index 7f45a5d67..8513caf16 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/use-ai-query-chat.ts +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/use-ai-table-filter-chat.ts @@ -1,8 +1,8 @@ "use client"; -import { createAnalyticsQueryChatAdapter } from "@/components/vibe-coding"; +import { createAnalyticsTableFilterChatAdapter } from "@/components/vibe-coding"; import { getPublicEnvVar } from "@/lib/env"; -import { useLocalThreadRuntime, type AssistantRuntime, type ThreadMessage, type ToolCallContentPart } from "@assistant-ui/react"; +import { useLocalThreadRuntime, type ThreadMessage, type ToolCallContentPart } from "@assistant-ui/react"; import { useUser } from "@hexclave/next"; import { throwErr } from "@hexclave/shared/dist/utils/errors"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -58,20 +58,50 @@ export function extractLatestQuery(messages: readonly ThreadMessage[]): { return null; } -export type AiQueryChat = { - runtime: AssistantRuntime, +/** + * Extracts the trailing text of the last assistant message, so the UI can + * surface what the AI said when it answered without committing a query + * (e.g. "that's an aggregation, which this search can't display"). + */ +function extractLastAssistantText(messages: readonly ThreadMessage[]): string | null { + for (let i = messages.length - 1; i >= 0; i--) { + const msg = messages[i]!; + if (msg.role !== "assistant") continue; + for (let j = msg.content.length - 1; j >= 0; j--) { + const part = msg.content[j]!; + if (part.type === "text" && part.text.trim().length > 0) { + return part.text.trim(); + } + } + } + return null; +} + +export type AiTableFilterChat = { messages: readonly ThreadMessage[], isResponding: boolean, error: Error | null, sendMessage: (input: { text: string }) => void, clearMessages: () => void, - stop: () => void, + /** The last query the AI committed via the queryAnalytics tool (unvalidated). */ latestQuery: string | null, - queryGeneration: number, - rewindToQuery: (query: string) => void, + /** + * Set when the last run finished WITHOUT committing a new query — holds the + * assistant's text reply (or null if there was none / a query was + * committed). Cleared on the next send/clear or via `dismissAssistantNote`. + */ + assistantNote: string | null, + dismissAssistantNote: () => void, }; -export function useAiQueryChat(): AiQueryChat { +/** + * AI chat thread backing the analytics table search bar's AI fallback. Uses + * the constrained `filter-analytics-table` system prompt scoped to the given + * table, so the AI only produces `SELECT * FROM