diff --git a/packages/app/package.json b/packages/app/package.json index 722ab67..ec6ea7a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -46,6 +46,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "d3": "^7.9.0", + "dompurify": "^3.3.3", "gray-matter": "^4.0.3", "iwanthue": "^2.0.0", "lodash-es": "^4.17.23", diff --git a/packages/app/src/app/(dashboard)/ai-chart/page.tsx b/packages/app/src/app/(dashboard)/ai-chart/page.tsx new file mode 100644 index 0000000..b299e8a --- /dev/null +++ b/packages/app/src/app/(dashboard)/ai-chart/page.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from 'next'; + +import AiChartDisplay from '@/components/ai-chart/AiChartDisplay'; +import { tabMetadata } from '@/lib/tab-meta'; + +export const metadata: Metadata = tabMetadata('ai-chart'); + +export default function AiChartPage() { + return ; +} diff --git a/packages/app/src/components/ai-chart/AiChartDisplay.tsx b/packages/app/src/components/ai-chart/AiChartDisplay.tsx new file mode 100644 index 0000000..c1d30c1 --- /dev/null +++ b/packages/app/src/components/ai-chart/AiChartDisplay.tsx @@ -0,0 +1,191 @@ +'use client'; + +import { useCallback, useState } from 'react'; +import { AlertCircle, Eye, EyeOff, Sparkles } from 'lucide-react'; + +import { track } from '@/lib/analytics'; +import { PROVIDER_OPTIONS, getProviderLabel } from '@/lib/ai-providers'; +import { useAiChart } from '@/hooks/api/use-ai-chart'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Textarea } from '@/components/ui/textarea'; + +import type { AiProvider } from './types'; +import { EXAMPLE_PROMPTS } from './example-prompts'; +import AiChartResult from './AiChartResult'; + +export default function AiChartDisplay() { + const [provider, setProvider] = useState('openai'); + const [apiKeys, setApiKeys] = useState>({ + openai: '', + anthropic: '', + xai: '', + google: '', + }); + const [prompt, setPrompt] = useState(''); + const [showKey, setShowKey] = useState(false); + const { result, isLoading, error, generate, reset } = useAiChart(); + + const apiKey = apiKeys[provider]; + + const handleProviderChange = useCallback((value: string) => { + const newProvider = value as AiProvider; + setProvider(newProvider); + track('ai_chart_provider_changed', { provider: newProvider }); + }, []); + + const handleSubmit = useCallback(() => { + if (!apiKey.trim() || !prompt.trim()) return; + track('ai_chart_prompt_submitted', { provider, prompt_length: prompt.length }); + generate(prompt, provider, apiKey); + }, [apiKey, prompt, provider, generate]); + + const handleExampleClick = useCallback((example: string, index: number) => { + setPrompt(example); + track('ai_chart_example_clicked', { example_index: index }); + }, []); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + handleSubmit(); + } + }, + [handleSubmit], + ); + + return ( +
+ {/* 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. + + + +
+
+ +
+
+ setApiKeys((prev) => ({ ...prev, [provider]: e.target.value }))} + data-ph-no-capture + autoComplete="off" + /> + +
+
+
+