import { useState, useEffect, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { BuildMonitor } from '../components/BuildMonitor'; import { ChipSummary } from '../components/ChipSummary'; import { BillingModal } from '../components/BillingModal'; import { fetchEventSource } from '@microsoft/fetch-event-source'; import { api, API_BASE } from '../api'; type Phase = 'prompt' | 'building' | 'done'; interface BuildEvent { type: string; state: string; message: string; step: number; total_steps: number; timestamp: number; status?: string; // present on stream_end events } interface StageSchemaItem { state: string; label: string; icon: string; } function slugify(text: string): string { return text .toLowerCase() .replace(/[^a-z0-9\s_]/g, '') .trim() .split(/\s+/) .slice(0, 4) .join('_') .substring(0, 48); } export const DesignStudio = () => { const [phase, setPhase] = useState('prompt'); const [prompt, setPrompt] = useState(''); const [designName, setDesignName] = useState(''); const [jobId, setJobId] = useState(''); const [events, setEvents] = useState([]); const [jobStatus, setJobStatus] = useState<'queued' | 'running' | 'done' | 'failed' | 'cancelled' | 'cancelling'>('queued'); const [result, setResult] = useState(null); const [error, setError] = useState(''); // Billing / Profile State const [profile, setProfile] = useState<{ auth_enabled: boolean, plan: string, successful_builds: number, has_byok_key: boolean } | null>(null); const [showBillingModal, setShowBillingModal] = useState(false); // Build Options const [skipOpenlane, setSkipOpenlane] = useState(false); const [skipCoverage, setSkipCoverage] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false); const [fullSignoff, setFullSignoff] = useState(false); const [maxRetries, setMaxRetries] = useState(5); const [showThinking, setShowThinking] = useState(false); const [minCoverage, setMinCoverage] = useState(80.0); const [strictGates, setStrictGates] = useState(false); const [pdkProfile, setPdkProfile] = useState("sky130"); const [maxPivots, setMaxPivots] = useState(2); const [congestionThreshold, setCongestionThreshold] = useState(10.0); const [hierarchical, setHierarchical] = useState("auto"); const [tbGateMode, setTbGateMode] = useState("strict"); const [tbMaxRetries, setTbMaxRetries] = useState(3); const [tbFallbackTemplate, setTbFallbackTemplate] = useState("uvm_lite"); const [coverageBackend, setCoverageBackend] = useState("auto"); const [coverageFallbackPolicy, setCoverageFallbackPolicy] = useState("fail_closed"); const [coverageProfile, setCoverageProfile] = useState("balanced"); const [stageSchema, setStageSchema] = useState([]); const abortCtrlRef = useRef(null); // Auto-generate design name from prompt useEffect(() => { if (prompt.length > 8) { setDesignName(slugify(prompt)); } }, [prompt]); // Fetch Profile Limits useEffect(() => { api.get('/profile') .then(res => setProfile(res.data)) .catch(() => setProfile(null)); // Ignored explicitly if no auth }, []); const handleLaunch = async () => { if (!prompt.trim()) return; setError(''); // Billing Guard: enforce 2 free successful builds if (profile?.auth_enabled) { const { plan, successful_builds, has_byok_key } = profile; if (plan === 'free' && successful_builds >= 2 && !has_byok_key) { setShowBillingModal(true); return; } } try { const res = await api.post(`/build`, { design_name: designName || slugify(prompt), description: prompt, skip_openlane: skipOpenlane, skip_coverage: skipCoverage, full_signoff: fullSignoff, max_retries: maxRetries, show_thinking: showThinking, min_coverage: minCoverage, strict_gates: strictGates, pdk_profile: pdkProfile, max_pivots: maxPivots, congestion_threshold: congestionThreshold, hierarchical: hierarchical, tb_gate_mode: tbGateMode, tb_max_retries: tbMaxRetries, tb_fallback_template: tbFallbackTemplate, coverage_backend: coverageBackend, coverage_fallback_policy: coverageFallbackPolicy, coverage_profile: coverageProfile }); const { job_id } = res.data; setJobId(job_id); setPhase('building'); startStreaming(job_id); } catch (e: any) { if (e?.code === 'ERR_NETWORK' || !e?.response) { setError('Backend is offline. Start the server with: uvicorn server.api:app --port 7860'); } else { setError(e?.response?.data?.detail || 'Build failed. Check the backend logs.'); } } }; const startStreaming = (jid: string) => { if (abortCtrlRef.current) abortCtrlRef.current.abort(); const ctrl = new AbortController(); abortCtrlRef.current = ctrl; // Clear previous events on reconnect to prevent duplicates // (server replays all events from the beginning on each connection) setEvents([]); fetchEventSource(`${API_BASE}/build/stream/${jid}`, { method: 'GET', headers: { 'ngrok-skip-browser-warning': 'true', 'Accept': 'text/event-stream', }, signal: ctrl.signal, onmessage(evt) { try { const data: BuildEvent = JSON.parse(evt.data); if (data.type === 'ping') return; if (data.type === 'stream_end') { ctrl.abort(); fetchResult(jid, data.status as any); return; } setEvents(prev => { // Deduplicate: skip if last event has same message + type const last = prev[prev.length - 1]; if (last && last.message === data.message && last.type === data.type) { return prev; } return [...prev, data]; }); setJobStatus(data.type === 'error' ? 'failed' : 'running'); } catch { /* ignore parse errors */ } }, onerror(err) { ctrl.abort(); throw err; } }); }; const fetchResult = async (jid: string, status: string) => { setJobStatus(status === 'done' ? 'done' : 'failed'); try { const res = await api.get(`/build/result/${jid}`); setResult(res.data.result); } catch { /* result might not exist if failed early */ } setPhase('done'); // Browser notification if ('Notification' in window && Notification.permission === 'granted') { new Notification('AgentIC — Chip Build Complete 🎉', { body: `Your chip "${designName}" has finished ${status === 'done' ? 'successfully!' : 'with errors.'}`, icon: '/chip-icon.png', }); } }; const handleReset = () => { abortCtrlRef.current?.abort(); setPhase('prompt'); setEvents([]); setResult(null); setJobId(''); setJobStatus('queued'); setError(''); setPrompt(''); }; // Request notification permission on mount useEffect(() => { if ('Notification' in window && Notification.permission === 'default') { Notification.requestPermission(); } api.get(`/pipeline/schema`) .then(res => setStageSchema(res.data?.stages || [])) .catch(() => setStageSchema([])); return () => abortCtrlRef.current?.abort(); }, []); return (
{/* ── PHASE A: Prompt ──────────────────────────── */} {phase === 'prompt' && (
⚙️

Design Your Chip

Describe any digital chip in plain English.
AgentIC will autonomously write RTL, verify, and harden it to silicon.

{['8-bit RISC CPU with Harvard architecture', 'AXI4 DMA engine with 4 channels', 'UART controller at 115200 baud'].map(ex => ( ))}