diff --git a/apps/arc-web/app/layouts/app-shell.tsx b/apps/arc-web/app/layouts/app-shell.tsx
index f191a79b7..b01dab917 100644
--- a/apps/arc-web/app/layouts/app-shell.tsx
+++ b/apps/arc-web/app/layouts/app-shell.tsx
@@ -9,7 +9,10 @@ import {
} from "@headlessui/react";
import {
Bars3Icon,
+ ChartBarIcon,
+ CheckBadgeIcon,
Cog6ToothIcon,
+ LightBulbIcon,
PlayIcon,
RectangleStackIcon,
SparklesIcon,
@@ -28,6 +31,9 @@ const navigation = [
{ name: "Start", href: "/start", icon: SparklesIcon },
{ name: "Workflows", href: "/workflows", icon: RectangleStackIcon },
{ name: "Runs", href: "/runs", icon: PlayIcon },
+ { name: "Verifications", href: "/verifications", icon: CheckBadgeIcon },
+ { name: "Retros", href: "/retros", icon: LightBulbIcon },
+ { name: "Insights", href: "/insights", icon: ChartBarIcon },
{ name: "Settings", href: "/settings", icon: Cog6ToothIcon },
];
@@ -43,10 +49,11 @@ export default function AppShell() {
const currentNav = navigation.find((item) => pathname.startsWith(item.href));
const title = currentNav?.name ?? "";
const lastMatch = matches[matches.length - 1];
- const handle = lastMatch?.handle as { headerExtra?: React.ReactNode; wide?: boolean } | undefined;
+ const handle = lastMatch?.handle as { headerExtra?: React.ReactNode } | undefined;
const headerExtra = handle?.headerExtra;
const hideHeader = matches.some((m) => (m.handle as { hideHeader?: boolean } | undefined)?.hideHeader);
- const maxWidth = handle?.wide ? "" : "max-w-5xl";
+ const wide = matches.some((m) => (m.handle as { wide?: boolean } | undefined)?.wide);
+ const maxWidth = wide ? "" : "max-w-5xl";
return (
diff --git a/apps/arc-web/app/routes.ts b/apps/arc-web/app/routes.ts
index 00baa467f..db3d7241e 100644
--- a/apps/arc-web/app/routes.ts
+++ b/apps/arc-web/app/routes.ts
@@ -25,7 +25,12 @@ export default [
route("files", "routes/run-files-changed.tsx"),
route("usage", "routes/run-usage.tsx"),
]),
- route("insights", "routes/insights.tsx"),
+ route("verifications", "routes/verifications.tsx"),
+ route("retros", "routes/retros.tsx"),
+ route("insights", "routes/insights.tsx", [
+ index("routes/insights-editor.tsx"),
+ route("new", "routes/insights-new.tsx"),
+ ]),
route("settings", "routes/settings.tsx"),
]),
] satisfies RouteConfig;
diff --git a/apps/arc-web/app/routes/insights-editor.tsx b/apps/arc-web/app/routes/insights-editor.tsx
new file mode 100644
index 000000000..65d1f7f62
--- /dev/null
+++ b/apps/arc-web/app/routes/insights-editor.tsx
@@ -0,0 +1,654 @@
+import { useState, useRef, useEffect, useCallback } from "react";
+import { useLocation } from "react-router";
+import {
+ Dialog,
+ DialogPanel,
+ DialogTitle,
+} from "@headlessui/react";
+import {
+ PlayIcon,
+ BookmarkIcon,
+ SparklesIcon,
+ TableCellsIcon,
+ ChartBarIcon,
+ XMarkIcon,
+ ArrowPathIcon,
+ PencilIcon,
+} from "@heroicons/react/24/outline";
+
+// ── Types ──
+
+interface QueryResult {
+ columns: string[];
+ rows: Array
>;
+ elapsed: number;
+ rowsRead: number;
+ bytesRead: number;
+ rowsReturned: number;
+}
+
+type ResultView = "chart" | "table";
+
+// ── Mock data ──
+
+function generateMockResult(sql: string): QueryResult {
+ const lowerSql = sql.toLowerCase();
+
+ if (lowerSql.includes("workflow_name") && lowerSql.includes("avg")) {
+ return {
+ columns: ["workflow_name", "avg_duration", "run_count"],
+ rows: [
+ { workflow_name: "Expand Product", avg_duration: 342.5, run_count: 48 },
+ { workflow_name: "Implement Feature", avg_duration: 287.3, run_count: 156 },
+ { workflow_name: "Security Scan", avg_duration: 198.1, run_count: 312 },
+ { workflow_name: "Fix Build", avg_duration: 145.7, run_count: 482 },
+ { workflow_name: "Sync Drift", avg_duration: 89.2, run_count: 94 },
+ { workflow_name: "Dependency Audit", avg_duration: 67.4, run_count: 201 },
+ ],
+ elapsed: 0.531,
+ rowsRead: 5182366,
+ bytesRead: 357780000,
+ rowsReturned: 6,
+ };
+ }
+
+ if (lowerSql.includes("failure_rate") || lowerSql.includes("failed")) {
+ return {
+ columns: ["day", "failures", "total", "failure_rate"],
+ rows: Array.from({ length: 14 }, (_, i) => {
+ const d = new Date();
+ d.setDate(d.getDate() - i);
+ const total = 80 + Math.floor(Math.random() * 60);
+ const failures = Math.floor(Math.random() * 15);
+ return {
+ day: d.toISOString().slice(0, 10),
+ failures,
+ total,
+ failure_rate: Math.round((1000 * failures) / total) / 10,
+ };
+ }),
+ elapsed: 0.287,
+ rowsRead: 2841092,
+ bytesRead: 198400000,
+ rowsReturned: 14,
+ };
+ }
+
+ return {
+ columns: ["repo", "runs", "total_additions", "total_deletions"],
+ rows: [
+ { repo: "arc-engine", runs: 482, total_additions: 28450, total_deletions: 12300 },
+ { repo: "arc-web", runs: 356, total_additions: 19200, total_deletions: 8900 },
+ { repo: "arc-cli", runs: 198, total_additions: 8700, total_deletions: 4200 },
+ { repo: "arc-docs", runs: 145, total_additions: 12100, total_deletions: 3400 },
+ { repo: "arc-sdk", runs: 89, total_additions: 5600, total_deletions: 2100 },
+ { repo: "arc-infra", runs: 67, total_additions: 3200, total_deletions: 1800 },
+ { repo: "arc-actions", runs: 42, total_additions: 2100, total_deletions: 980 },
+ { repo: "arc-proto", runs: 28, total_additions: 1400, total_deletions: 650 },
+ ],
+ elapsed: 0.148,
+ rowsRead: 1204588,
+ bytesRead: 89200000,
+ rowsReturned: 8,
+ };
+}
+
+// ── Formatting helpers ──
+
+function formatBytes(bytes: number): string {
+ if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(2)} GB`;
+ if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(2)} MB`;
+ if (bytes >= 1e3) return `${(bytes / 1e3).toFixed(2)} KB`;
+ return `${bytes} B`;
+}
+
+function formatNumber(n: number): string {
+ return n.toLocaleString();
+}
+
+// ── Chart component ──
+
+const BAR_COLORS = [
+ "rgba(90, 200, 168, 0.85)",
+ "rgba(103, 178, 215, 0.85)",
+ "rgba(181, 221, 239, 0.65)",
+ "rgba(240, 164, 91, 0.75)",
+];
+
+function BarChart({ result }: { result: QueryResult }) {
+ const containerRef = useRef(null);
+ const [containerWidth, setContainerWidth] = useState(0);
+
+ useEffect(() => {
+ const el = containerRef.current;
+ if (!el) return;
+
+ const observer = new ResizeObserver((entries) => {
+ const entry = entries[0];
+ if (entry) {
+ setContainerWidth(entry.contentRect.width);
+ }
+ });
+ observer.observe(el);
+ return () => observer.disconnect();
+ }, []);
+
+ const labelCol = result.columns[0];
+ const valueCols = result.columns.slice(1).filter((col) => {
+ const firstVal = result.rows[0]?.[col];
+ return typeof firstVal === "number";
+ });
+
+ if (valueCols.length === 0 || result.rows.length === 0) {
+ return (
+
+ No numeric columns to chart
+
+ );
+ }
+
+ const valueCol = valueCols[0];
+ const maxVal = Math.max(...result.rows.map((r) => {
+ const v = r[valueCol];
+ return typeof v === "number" ? v : 0;
+ }));
+
+ const chartHeight = 260;
+ const yAxisWidth = 52;
+ const padding = { top: 12, bottom: 48, right: 16 };
+ const plotHeight = chartHeight - padding.top - padding.bottom;
+ const plotWidth = containerWidth - yAxisWidth - padding.right;
+ const barCount = result.rows.length;
+ const gap = Math.max(8, Math.min(16, plotWidth / barCount * 0.3));
+ const barWidth = Math.max(12, (plotWidth - gap * (barCount + 1)) / barCount);
+
+ const tickCount = 5;
+ const yTicks = Array.from({ length: tickCount }, (_, i) =>
+ Math.round((maxVal * (tickCount - 1 - i)) / (tickCount - 1)),
+ );
+
+ return (
+
+ {containerWidth > 0 && (
+
+ )}
+
+
+ {valueCol.replace(/_/g, " ")}
+
+
+
+ );
+}
+
+// ── Table component ──
+
+function ResultTable({ result }: { result: QueryResult }) {
+ return (
+
+
+
+
+ {result.columns.map((col) => (
+ |
+ {col}
+ |
+ ))}
+
+
+
+ {result.rows.map((row, i) => (
+
+ {result.columns.map((col) => {
+ const val = row[col];
+ const isNum = typeof val === "number";
+ return (
+ |
+ {isNum ? formatNumber(val) : String(val)}
+ |
+ );
+ })}
+
+ ))}
+
+
+
+ );
+}
+
+// ── SQL Editor with line numbers ──
+
+function SqlEditor({
+ value,
+ onChange,
+ onRun,
+}: {
+ value: string;
+ onChange: (v: string) => void;
+ onRun: () => void;
+}) {
+ const textareaRef = useRef(null);
+ const lineNumbersRef = useRef(null);
+ const lineCount = value.split("\n").length;
+
+ const syncScroll = useCallback(() => {
+ if (textareaRef.current && lineNumbersRef.current) {
+ lineNumbersRef.current.scrollTop = textareaRef.current.scrollTop;
+ }
+ }, []);
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ // Ctrl/Cmd + Enter to run
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
+ e.preventDefault();
+ onRun();
+ return;
+ }
+ // Tab inserts spaces
+ if (e.key === "Tab") {
+ e.preventDefault();
+ const textarea = e.currentTarget;
+ const start = textarea.selectionStart;
+ const end = textarea.selectionEnd;
+ const newValue = value.slice(0, start) + " " + value.slice(end);
+ onChange(newValue);
+ requestAnimationFrame(() => {
+ textarea.selectionStart = start + 2;
+ textarea.selectionEnd = start + 2;
+ });
+ }
+ };
+
+ return (
+
+ {/* Line numbers */}
+
+ {Array.from({ length: lineCount }, (_, i) => (
+
+ {i + 1}
+
+ ))}
+
+ {/* Textarea */}
+
+ );
+}
+
+// ── Main page ──
+
+const DEFAULT_SQL =
+ "SELECT workflow_name, AVG(duration_seconds) as avg_duration,\n COUNT(*) as run_count\nFROM runs\nGROUP BY workflow_name\nORDER BY avg_duration DESC\nLIMIT 20";
+
+export default function InsightsEditor() {
+ const location = useLocation();
+ const navState = location.state as { sql?: string; name?: string } | null;
+
+ const [sql, setSql] = useState(navState?.sql ?? DEFAULT_SQL);
+ const [result, setResult] = useState(null);
+ const [resultView, setResultView] = useState("chart");
+ const [isRunning, setIsRunning] = useState(false);
+ const [queryName, setQueryName] = useState(navState?.name ?? "Run duration by workflow");
+ const [isEditingName, setIsEditingName] = useState(false);
+ const nameInputRef = useRef(null);
+ const [showAiDialog, setShowAiDialog] = useState(false);
+ const [aiPrompt, setAiPrompt] = useState("");
+
+ const runQuery = useCallback(() => {
+ setIsRunning(true);
+ const delay = 200 + Math.random() * 400;
+ setTimeout(() => {
+ setResult(generateMockResult(sql));
+ setIsRunning(false);
+ }, delay);
+ }, [sql]);
+
+ // Load query from navigation state
+ useEffect(() => {
+ if (navState?.sql) {
+ setSql(navState.sql);
+ if (navState.name) {
+ setQueryName(navState.name);
+ }
+ }
+ }, [navState]);
+
+ // Run default query on mount
+ const hasRun = useRef(false);
+ useEffect(() => {
+ if (!hasRun.current) {
+ hasRun.current = true;
+ runQuery();
+ }
+ }, [runQuery]);
+
+ return (
+
+ {/* ── Toolbar + Editor ── */}
+
+ {/* Toolbar */}
+
+ {/* Query name */}
+ {isEditingName ? (
+
setQueryName(e.target.value)}
+ onBlur={() => setIsEditingName(false)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === "Escape") {
+ setIsEditingName(false);
+ }
+ }}
+ placeholder="Untitled query"
+ className="min-w-0 max-w-xs rounded border border-teal-500/40 bg-navy-950/60 px-2 py-0.5 text-sm font-medium text-ice-100 placeholder-navy-600 outline-none"
+ />
+ ) : (
+
+
+ {queryName || "Untitled query"}
+
+
+
+ )}
+
+ {/* Push buttons to the right */}
+
+
+ {/* SQL AI */}
+
+
+ {/* Save */}
+
+
+ {/* Run */}
+
+
+
+
+
+
+ {/* ── Results bar + content ── */}
+ {result && (
+ <>
+ {/* Results bar */}
+
+ {/* Query stats */}
+
+
+ Elapsed:{" "}
+
+ {result.elapsed.toFixed(3)}s
+
+
+
+ Read:{" "}
+
+ {formatNumber(result.rowsRead)} rows
+ {" "}
+ ({formatBytes(result.bytesRead)})
+
+
+ Returned:{" "}
+
+ {formatNumber(result.rowsReturned)} rows
+
+
+
+
+ {/* View toggle */}
+
+
+
+
+
+
+ {/* Results content */}
+
+ {resultView === "chart" ? (
+
+ ) : (
+
+ )}
+
+ >
+ )}
+
+ {/* ── Running overlay ── */}
+ {isRunning && !result && (
+
+ )}
+
+ {/* ── AI Dialog ── */}
+
+
+ );
+}
diff --git a/apps/arc-web/app/routes/insights-new.tsx b/apps/arc-web/app/routes/insights-new.tsx
new file mode 100644
index 000000000..f6ee45c3a
--- /dev/null
+++ b/apps/arc-web/app/routes/insights-new.tsx
@@ -0,0 +1,99 @@
+import { Link, useNavigate } from "react-router";
+import {
+ SparklesIcon,
+ ClockIcon,
+ ExclamationTriangleIcon,
+ ServerStackIcon,
+ CommandLineIcon,
+} from "@heroicons/react/24/outline";
+
+const templateQueries = [
+ {
+ title: "Run duration by workflow",
+ description: "Average execution time and run count per workflow",
+ icon: ClockIcon,
+ sql: "SELECT workflow_name, AVG(duration_seconds) as avg_duration,\n COUNT(*) as run_count\nFROM runs\nGROUP BY workflow_name\nORDER BY avg_duration DESC\nLIMIT 20",
+ },
+ {
+ title: "Daily failure rate",
+ description: "Failures, totals, and failure percentage by day",
+ icon: ExclamationTriangleIcon,
+ sql: "SELECT date_trunc('day', created_at) as day,\n COUNT(*) FILTER (WHERE status = 'failed') as failures,\n COUNT(*) as total,\n ROUND(100.0 * COUNT(*) FILTER (WHERE status = 'failed') / COUNT(*), 1) as failure_rate\nFROM runs\nGROUP BY 1\nORDER BY 1 DESC\nLIMIT 30",
+ },
+ {
+ title: "Top repos by activity",
+ description: "Run count and code churn per repository",
+ icon: ServerStackIcon,
+ sql: "SELECT repo, COUNT(*) as runs, SUM(additions) as total_additions,\n SUM(deletions) as total_deletions\nFROM runs\nGROUP BY repo\nORDER BY runs DESC",
+ },
+];
+
+export default function InsightsNew() {
+ const navigate = useNavigate();
+
+ return (
+
+ {/* LLM input */}
+
+
+
+
New Query
+
+
+
+
+
+
+
+ {/* Template cards */}
+
+
+ Start from a template
+
+
+ {templateQueries.map((tpl) => (
+
+ ))}
+
+
+
+ {/* SQL link */}
+
+
+
+ Write my own report with SQL
+
+
+
+ );
+}
diff --git a/apps/arc-web/app/routes/insights.tsx b/apps/arc-web/app/routes/insights.tsx
index a5a386db1..75a9da032 100644
--- a/apps/arc-web/app/routes/insights.tsx
+++ b/apps/arc-web/app/routes/insights.tsx
@@ -1,20 +1,5 @@
-import { useState, useRef, useEffect, useCallback } from "react";
-import {
- Dialog,
- DialogPanel,
- DialogTitle,
-} from "@headlessui/react";
-import {
- PlayIcon,
- BookmarkIcon,
- SparklesIcon,
- TableCellsIcon,
- ChartBarIcon,
- XMarkIcon,
- ArrowPathIcon,
- PencilIcon,
- PlusIcon,
-} from "@heroicons/react/24/outline";
+import { Link, Outlet, useNavigate } from "react-router";
+import { PlusIcon } from "@heroicons/react/24/outline";
import type { Route } from "./+types/insights";
export function meta({}: Route.MetaArgs) {
@@ -27,22 +12,13 @@ export const handle = {
// ── Types ──
-interface QueryResult {
- columns: string[];
- rows: Array>;
- elapsed: number;
- rowsRead: number;
- bytesRead: number;
- rowsReturned: number;
-}
-
-interface SavedQuery {
+export interface SavedQuery {
id: string;
name: string;
sql: string;
}
-interface HistoryEntry {
+export interface HistoryEntry {
id: string;
sql: string;
timestamp: string;
@@ -50,11 +26,9 @@ interface HistoryEntry {
rowsReturned: number;
}
-type ResultView = "chart" | "table";
-
// ── Mock data ──
-const savedQueries: SavedQuery[] = [
+export const savedQueries: SavedQuery[] = [
{
id: "1",
name: "Run duration by workflow",
@@ -72,7 +46,7 @@ const savedQueries: SavedQuery[] = [
},
];
-const historyEntries: HistoryEntry[] = [
+export const historyEntries: HistoryEntry[] = [
{ id: "h1", sql: "SELECT workflow_name, COUNT(*) FROM runs GROUP BY 1", timestamp: "2 min ago", elapsed: 0.342, rowsReturned: 6 },
{ id: "h2", sql: "SELECT * FROM runs WHERE status = 'failed' LIMIT 100", timestamp: "8 min ago", elapsed: 0.127, rowsReturned: 23 },
{ id: "h3", sql: "SELECT date_trunc('day', created_at) as d, COUNT(*) FROM runs GROUP BY 1 ORDER BY 1", timestamp: "15 min ago", elapsed: 0.531, rowsReturned: 30 },
@@ -80,404 +54,21 @@ const historyEntries: HistoryEntry[] = [
{ id: "h5", sql: "DESCRIBE runs", timestamp: "1 hr ago", elapsed: 0.003, rowsReturned: 18 },
];
-function generateMockResult(sql: string): QueryResult {
- const lowerSql = sql.toLowerCase();
-
- if (lowerSql.includes("workflow_name") && lowerSql.includes("avg")) {
- return {
- columns: ["workflow_name", "avg_duration", "run_count"],
- rows: [
- { workflow_name: "Expand Product", avg_duration: 342.5, run_count: 48 },
- { workflow_name: "Implement Feature", avg_duration: 287.3, run_count: 156 },
- { workflow_name: "Security Scan", avg_duration: 198.1, run_count: 312 },
- { workflow_name: "Fix Build", avg_duration: 145.7, run_count: 482 },
- { workflow_name: "Sync Drift", avg_duration: 89.2, run_count: 94 },
- { workflow_name: "Dependency Audit", avg_duration: 67.4, run_count: 201 },
- ],
- elapsed: 0.531,
- rowsRead: 5182366,
- bytesRead: 357780000,
- rowsReturned: 6,
- };
- }
-
- if (lowerSql.includes("failure_rate") || lowerSql.includes("failed")) {
- return {
- columns: ["day", "failures", "total", "failure_rate"],
- rows: Array.from({ length: 14 }, (_, i) => {
- const d = new Date();
- d.setDate(d.getDate() - i);
- const total = 80 + Math.floor(Math.random() * 60);
- const failures = Math.floor(Math.random() * 15);
- return {
- day: d.toISOString().slice(0, 10),
- failures,
- total,
- failure_rate: Math.round((1000 * failures) / total) / 10,
- };
- }),
- elapsed: 0.287,
- rowsRead: 2841092,
- bytesRead: 198400000,
- rowsReturned: 14,
- };
- }
-
- return {
- columns: ["repo", "runs", "total_additions", "total_deletions"],
- rows: [
- { repo: "arc-engine", runs: 482, total_additions: 28450, total_deletions: 12300 },
- { repo: "arc-web", runs: 356, total_additions: 19200, total_deletions: 8900 },
- { repo: "arc-cli", runs: 198, total_additions: 8700, total_deletions: 4200 },
- { repo: "arc-docs", runs: 145, total_additions: 12100, total_deletions: 3400 },
- { repo: "arc-sdk", runs: 89, total_additions: 5600, total_deletions: 2100 },
- { repo: "arc-infra", runs: 67, total_additions: 3200, total_deletions: 1800 },
- { repo: "arc-actions", runs: 42, total_additions: 2100, total_deletions: 980 },
- { repo: "arc-proto", runs: 28, total_additions: 1400, total_deletions: 650 },
- ],
- elapsed: 0.148,
- rowsRead: 1204588,
- bytesRead: 89200000,
- rowsReturned: 8,
- };
-}
-
-// ── Formatting helpers ──
-
-function formatBytes(bytes: number): string {
- if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(2)} GB`;
- if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(2)} MB`;
- if (bytes >= 1e3) return `${(bytes / 1e3).toFixed(2)} KB`;
- return `${bytes} B`;
-}
-
-function formatNumber(n: number): string {
- return n.toLocaleString();
-}
-
-// ── Chart component ──
-
-const BAR_COLORS = [
- "rgba(90, 200, 168, 0.85)",
- "rgba(103, 178, 215, 0.85)",
- "rgba(181, 221, 239, 0.65)",
- "rgba(240, 164, 91, 0.75)",
-];
-
-function BarChart({ result }: { result: QueryResult }) {
- const containerRef = useRef(null);
- const [containerWidth, setContainerWidth] = useState(0);
-
- useEffect(() => {
- const el = containerRef.current;
- if (!el) return;
-
- const observer = new ResizeObserver((entries) => {
- const entry = entries[0];
- if (entry) {
- setContainerWidth(entry.contentRect.width);
- }
- });
- observer.observe(el);
- return () => observer.disconnect();
- }, []);
-
- const labelCol = result.columns[0];
- const valueCols = result.columns.slice(1).filter((col) => {
- const firstVal = result.rows[0]?.[col];
- return typeof firstVal === "number";
- });
-
- if (valueCols.length === 0 || result.rows.length === 0) {
- return (
-
- No numeric columns to chart
-
- );
- }
-
- const valueCol = valueCols[0];
- const maxVal = Math.max(...result.rows.map((r) => {
- const v = r[valueCol];
- return typeof v === "number" ? v : 0;
- }));
-
- const chartHeight = 260;
- const yAxisWidth = 52;
- const padding = { top: 12, bottom: 48, right: 16 };
- const plotHeight = chartHeight - padding.top - padding.bottom;
- const plotWidth = containerWidth - yAxisWidth - padding.right;
- const barCount = result.rows.length;
- const gap = Math.max(8, Math.min(16, plotWidth / barCount * 0.3));
- const barWidth = Math.max(12, (plotWidth - gap * (barCount + 1)) / barCount);
-
- const tickCount = 5;
- const yTicks = Array.from({ length: tickCount }, (_, i) =>
- Math.round((maxVal * (tickCount - 1 - i)) / (tickCount - 1)),
- );
-
- return (
-
- {containerWidth > 0 && (
-
- )}
-
-
- {valueCol.replace(/_/g, " ")}
-
-
-
- );
-}
-
-// ── Table component ──
-
-function ResultTable({ result }: { result: QueryResult }) {
- return (
-
-
-
-
- {result.columns.map((col) => (
- |
- {col}
- |
- ))}
-
-
-
- {result.rows.map((row, i) => (
-
- {result.columns.map((col) => {
- const val = row[col];
- const isNum = typeof val === "number";
- return (
- |
- {isNum ? formatNumber(val) : String(val)}
- |
- );
- })}
-
- ))}
-
-
-
- );
-}
-
-// ── SQL Editor with line numbers ──
-
-function SqlEditor({
- value,
- onChange,
- onRun,
-}: {
- value: string;
- onChange: (v: string) => void;
- onRun: () => void;
-}) {
- const textareaRef = useRef(null);
- const lineNumbersRef = useRef(null);
- const lineCount = value.split("\n").length;
-
- const syncScroll = useCallback(() => {
- if (textareaRef.current && lineNumbersRef.current) {
- lineNumbersRef.current.scrollTop = textareaRef.current.scrollTop;
- }
- }, []);
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- // Ctrl/Cmd + Enter to run
- if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
- e.preventDefault();
- onRun();
- return;
- }
- // Tab inserts spaces
- if (e.key === "Tab") {
- e.preventDefault();
- const textarea = e.currentTarget;
- const start = textarea.selectionStart;
- const end = textarea.selectionEnd;
- const newValue = value.slice(0, start) + " " + value.slice(end);
- onChange(newValue);
- requestAnimationFrame(() => {
- textarea.selectionStart = start + 2;
- textarea.selectionEnd = start + 2;
- });
- }
- };
-
- return (
-
- {/* Line numbers */}
-
- {Array.from({ length: lineCount }, (_, i) => (
-
- {i + 1}
-
- ))}
-
- {/* Textarea */}
-
- );
-}
-
-// ── Main page ──
-
-export default function Insights() {
- const defaultSql =
- "SELECT workflow_name, AVG(duration_seconds) as avg_duration,\n COUNT(*) as run_count\nFROM runs\nGROUP BY workflow_name\nORDER BY avg_duration DESC\nLIMIT 20";
-
- const [sql, setSql] = useState(defaultSql);
- const [result, setResult] = useState(null);
- const [resultView, setResultView] = useState("chart");
- const [isRunning, setIsRunning] = useState(false);
- const [queryName, setQueryName] = useState("Run duration by workflow");
- const [isEditingName, setIsEditingName] = useState(false);
- const nameInputRef = useRef(null);
- const [showAiDialog, setShowAiDialog] = useState(false);
- const [aiPrompt, setAiPrompt] = useState("");
-
- const runQuery = useCallback(() => {
- setIsRunning(true);
- // Simulate query execution
- const delay = 200 + Math.random() * 400;
- setTimeout(() => {
- setResult(generateMockResult(sql));
- setIsRunning(false);
- }, delay);
- }, [sql]);
-
- // Run default query on mount
- const hasRun = useRef(false);
- useEffect(() => {
- if (!hasRun.current) {
- hasRun.current = true;
- runQuery();
- }
- }, [runQuery]);
-
- const loadSavedQuery = (query: SavedQuery) => {
- setSql(query.sql);
- };
-
- const loadHistoryEntry = (entry: HistoryEntry) => {
- setSql(entry.sql);
- };
+export default function InsightsLayout() {
+ const navigate = useNavigate();
return (
{/* ── Sidebar ── */}
-
+
@@ -489,14 +80,9 @@ export default function Insights() {
key={q.id}
type="button"
onClick={() => {
- loadSavedQuery(q);
- setQueryName(q.name);
+ navigate("/insights", { state: { sql: q.sql, name: q.name } });
}}
- className={`flex w-full flex-col gap-0.5 rounded-md px-2.5 py-2 text-left transition-colors hover:bg-white/[0.05] ${
- queryName === q.name
- ? "bg-white/[0.05] text-white"
- : ""
- }`}
+ className="flex w-full flex-col gap-0.5 rounded-md px-2.5 py-2 text-left transition-colors hover:bg-white/[0.05]"
>
{q.name}
@@ -518,7 +104,9 @@ export default function Insights() {
{/* ── Main content ── */}
-
- {/* ── Toolbar + Editor ── */}
-
- {/* Toolbar */}
-
- {/* Query name */}
- {isEditingName ? (
-
setQueryName(e.target.value)}
- onBlur={() => setIsEditingName(false)}
- onKeyDown={(e) => {
- if (e.key === "Enter" || e.key === "Escape") {
- setIsEditingName(false);
- }
- }}
- placeholder="Untitled query"
- className="min-w-0 max-w-xs rounded border border-teal-500/40 bg-navy-950/60 px-2 py-0.5 text-sm font-medium text-ice-100 placeholder-navy-600 outline-none"
- />
- ) : (
-
-
- {queryName || "Untitled query"}
-
-
-
- )}
-
- {/* Push buttons to the right */}
-
-
- {/* SQL AI */}
-
-
- {/* Save */}
-
-
- {/* Run */}
-
-
-
-
+
+
-
- {/* ── Results bar + content ── */}
- {result && (
- <>
- {/* Results bar */}
-
- {/* Query stats */}
-
-
- Elapsed:{" "}
-
- {result.elapsed.toFixed(3)}s
-
-
-
- Read:{" "}
-
- {formatNumber(result.rowsRead)} rows
- {" "}
- ({formatBytes(result.bytesRead)})
-
-
- Returned:{" "}
-
- {formatNumber(result.rowsReturned)} rows
-
-
-
-
- {/* View toggle */}
-
-
-
-
-
-
- {/* Results content */}
-
- {resultView === "chart" ? (
-
- ) : (
-
- )}
-
- >
- )}
-
- {/* ── Running overlay ── */}
- {isRunning && !result && (
-
-
-
- Executing query\u2026
-
-
- )}
-
- {/* ── AI Dialog ── */}
-
-
{/* end main content */}
);
}