+ {/* Title, description & API Key */}
+
+
+
+
+ AI Chart Generation
+
+
+ Describe the chart you want in natural language. Your API key is stored in your browser
+ and only used by your selected provider. We never see it.
+
+
+
+
+
+
+
+
+
+
+ {PROVIDER_OPTIONS.map((p) => (
+
+ {getProviderLabel(p)}
+
+ ))}
+
+
+
+
+ setApiKeys((prev) => ({ ...prev, [provider]: e.target.value }))}
+ data-ph-no-capture
+ autoComplete="off"
+ />
+ setShowKey((s) => !s)}
+ aria-label={showKey ? 'Hide API key' : 'Show API key'}
+ >
+ {showKey ? : }
+
+
+
+
+
+
+
+ {/* Loading state */}
+ {isLoading && (
+
+
+
+
+
+
+
+ )}
+
+ {/* Error state */}
+ {error && (
+
+
+
+
+
Error
+
{error}
+
+ Try Again
+
+
+
+
+ )}
+
+ {/* Result */}
+ {result &&
}
+
+ {/* Example prompts (shown when no result) */}
+ {!result && !isLoading && !error && (
+
+
Example prompts
+
+ {EXAMPLE_PROMPTS.map((example, i) => (
+ handleExampleClick(example, i)}
+ >
+ {example}
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/packages/app/src/components/ai-chart/AiChartResult.tsx b/packages/app/src/components/ai-chart/AiChartResult.tsx
new file mode 100644
index 0000000..87e2639
--- /dev/null
+++ b/packages/app/src/components/ai-chart/AiChartResult.tsx
@@ -0,0 +1,246 @@
+'use client';
+
+import { useMemo } from 'react';
+
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import type { InferenceData } from '@/components/inference/types';
+import { D3Chart } from '@/lib/d3-chart/D3Chart';
+import type {
+ BarLayerConfig,
+ ScatterLayerConfig,
+ TooltipConfig,
+ ScaleConfig,
+ AxisConfig,
+} from '@/lib/d3-chart/D3Chart';
+
+import DOMPurify from 'dompurify';
+
+import type { AiChartBarPoint, AiChartSpec } from './types';
+import type { AiSingleChartResult } from '@/hooks/api/use-ai-chart';
+
+/** Sanitize tooltip HTML that may contain LLM-generated strings. */
+function sanitize(html: string): string {
+ return DOMPurify.sanitize(html, {
+ ALLOWED_TAGS: ['div', 'span', 'strong', 'br'],
+ ALLOWED_ATTR: ['style'],
+ });
+}
+
+const CHART_INSTRUCTIONS = 'Hover for details';
+
+interface AiChartResultProps {
+ charts: AiSingleChartResult[];
+ summary: string | null;
+}
+
+function BarChart({ data, spec }: { data: AiChartBarPoint[]; spec: AiChartSpec }) {
+ const xScale = useMemo