diff --git a/apps/webapp/app/assets/icons/AbacusIcon.tsx b/apps/webapp/app/assets/icons/AbacusIcon.tsx new file mode 100644 index 0000000000..f0b7bfdf7b --- /dev/null +++ b/apps/webapp/app/assets/icons/AbacusIcon.tsx @@ -0,0 +1,71 @@ +export function AbacusIcon({ className }: { className?: string }) { + return ( + + ); +} diff --git a/apps/webapp/app/assets/icons/ArrowTopRightBottomLeftIcon.tsx b/apps/webapp/app/assets/icons/ArrowTopRightBottomLeftIcon.tsx new file mode 100644 index 0000000000..c49aa8cb0c --- /dev/null +++ b/apps/webapp/app/assets/icons/ArrowTopRightBottomLeftIcon.tsx @@ -0,0 +1,22 @@ +export function ArrowTopRightBottomLeftIcon({ className }: { className?: string }) { + return ( + + ); +} diff --git a/apps/webapp/app/components/code/AIQueryInput.tsx b/apps/webapp/app/components/code/AIQueryInput.tsx index e7624ba5b1..38d0c9b21b 100644 --- a/apps/webapp/app/components/code/AIQueryInput.tsx +++ b/apps/webapp/app/components/code/AIQueryInput.tsx @@ -18,18 +18,22 @@ import { Spinner } from "~/components/primitives/Spinner"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; +import type { AITimeFilter } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/types"; import { cn } from "~/utils/cn"; type StreamEventType = | { type: "thinking"; content: string } | { type: "tool_call"; tool: string; args: unknown } - | { type: "result"; success: true; query: string } + | { type: "time_filter"; filter: AITimeFilter } + | { type: "result"; success: true; query: string; timeFilter?: AITimeFilter } | { type: "result"; success: false; error: string }; export type AIQueryMode = "new" | "edit"; interface AIQueryInputProps { onQueryGenerated: (query: string) => void; + /** Called when the AI sets a time filter - updates URL search params */ + onTimeFilterChange?: (filter: AITimeFilter) => void; /** Set this to a prompt to auto-populate and immediately submit */ autoSubmitPrompt?: string; /** Change this to force re-submission even if prompt is the same */ @@ -40,6 +44,7 @@ interface AIQueryInputProps { export function AIQueryInput({ onQueryGenerated, + onTimeFilterChange, autoSubmitPrompt, autoSubmitKey, getCurrentQuery, @@ -174,10 +179,32 @@ export function AIQueryInput({ setThinking((prev) => prev + event.content); break; case "tool_call": - setThinking((prev) => prev + `\nValidating query...\n`); + if (event.tool === "setTimeFilter") { + setThinking((prev) => { + if (prev.trimEnd().endsWith("Setting time filter...")) { + return prev; + } + return prev + `\nSetting time filter...\n`; + }); + } else { + setThinking((prev) => { + if (prev.trimEnd().endsWith("Validating query...")) { + return prev; + } + return prev + `\nValidating query...\n`; + }); + } + break; + case "time_filter": + // Apply time filter immediately when the AI sets it + onTimeFilterChange?.(event.filter); break; case "result": if (event.success) { + // Apply time filter if included in result (backup in case time_filter event was missed) + if (event.timeFilter) { + onTimeFilterChange?.(event.timeFilter); + } onQueryGenerated(event.query); setPrompt(""); setLastResult("success"); @@ -189,7 +216,7 @@ export function AIQueryInput({ break; } }, - [onQueryGenerated] + [onQueryGenerated, onTimeFilterChange] ); const handleSubmit = useCallback( @@ -359,10 +386,10 @@ export function AIQueryInput({ {isLoading ? "AI is thinking..." : lastResult === "success" - ? "Query generated" - : lastResult === "error" - ? "Generation failed" - : "AI response"} + ? "Query generated" + : lastResult === "error" + ? "Generation failed" + : "AI response"} {isLoading ? ( diff --git a/apps/webapp/app/components/code/ChartConfigPanel.tsx b/apps/webapp/app/components/code/ChartConfigPanel.tsx index 7c4e9d672a..7d563e781d 100644 --- a/apps/webapp/app/components/code/ChartConfigPanel.tsx +++ b/apps/webapp/app/components/code/ChartConfigPanel.tsx @@ -1,5 +1,5 @@ import type { OutputColumnMetadata } from "@internal/clickhouse"; -import { BarChart, LineChart } from "lucide-react"; +import { BarChart, LineChart, Plus, XIcon } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef } from "react"; import { cn } from "~/utils/cn"; import { Header3 } from "../primitives/Headers"; @@ -239,9 +239,9 @@ export function ChartConfigPanel({ columns, config, onChange, className }: Chart } return ( -
|
+ header.column.resetSize()}
+ onMouseDown={header.getResizeHandler()}
+ onTouchStart={header.getResizeHandler()}
+ className={cn(
+ "absolute right-0 top-0 h-full w-0.5 cursor-col-resize touch-none select-none",
+ "opacity-0 group-hover/header:opacity-100",
+ "bg-charcoal-600 hover:bg-indigo-500",
+ header.column.getIsResizing() && "bg-indigo-500 opacity-100"
+ )}
+ />
+ |
+ );
+ })}
+
|---|
|
+ |
+
|
+ header.column.resetSize()}
+ onMouseDown={header.getResizeHandler()}
+ onTouchStart={header.getResizeHandler()}
+ className={cn(
+ "absolute right-0 top-0 h-full w-1 cursor-col-resize touch-none select-none",
+ "opacity-0 group-hover/header:opacity-100",
+ "bg-charcoal-600 hover:bg-indigo-500",
+ header.column.getIsResizing() && "bg-indigo-500 opacity-100"
+ )}
+ />
+ |
+ );
+ })}
+
|---|