import { useCallback, useEffect, useRef, useState } from "react"; import { API } from "../config"; /* ─── Types ─── */ type Step = 0 | 1 | 2 | 3 | 4; type ClarifyMsg = { role: "user" | "assistant"; content: string }; type PMResult = { summary: string; functional: string[]; nonfunctional: string[]; acceptance: string[]; edge_cases: string[]; }; type TestCase = { id: string; name: string; precondition: string; steps: string; expected: string; }; type TestResult = { passed: number; failed: number; errors: number; total: number; output: string; }; /* ─── API helpers ─── */ const BASE = API.devops; async function apiPost(path: string, body?: unknown): Promise { const res = await fetch(`${BASE}${path}`, { method: "POST", headers: body !== undefined ? { "Content-Type": "application/json" } : undefined, body: body !== undefined ? JSON.stringify(body) : undefined, }); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); return res.json(); } async function readSSE(path: string, onChunk: (text: string) => void, signal?: AbortSignal) { const res = await fetch(`${BASE}${path}`, { signal }); if (!res.ok) throw new Error(`${res.status}`); if (!res.body) return; const reader = res.body.getReader(); const dec = new TextDecoder(); let buf = ""; try { while (true) { const { done, value } = await reader.read(); if (done) break; buf += dec.decode(value, { stream: true }); // process SSE lines let idx = buf.indexOf("\n"); while (idx >= 0) { const line = buf.slice(0, idx).trim(); buf = buf.slice(idx + 1); if (line.startsWith("data:")) { const payload = line.slice(5).trim(); if (payload === "[DONE]") return; try { const evt = JSON.parse(payload); if (evt.text) onChunk(evt.text); else if (evt.message) onChunk(evt.message); else if (typeof evt === "string") onChunk(evt); } catch { onChunk(payload); } } idx = buf.indexOf("\n"); } } } finally { reader.releaseLock(); } } /* ── Step labels ── */ const STEPS: { title: string; icon: string }[] = [ { title: "Clarification", icon: "💬" }, { title: "PM Analysis", icon: "📋" }, { title: "QA Cases", icon: "🧪" }, { title: "Dev Output", icon: "💻" }, { title: "Test Run", icon: "▶" }, ]; /* ━━━━━━━━━━━━━━━━━━━━━ Component ━━━━━━━━━━━━━━━━━━━━━ */ export default function DevOpsAgent() { const [step, setStep] = useState(0); const [sessionId, setSessionId] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); // Step 0 — Requirement const [requirement, setRequirement] = useState(""); const [rawReq, setRawReq] = useState(""); const [status, setStatus] = useState(""); const [clarifyHistory, setClarifyHistory] = useState([]); const [clarifyInput, setClarifyInput] = useState(""); // Step 1 — PM analysis const [pmStream, setPmStream] = useState(""); const [pmResult, setPmResult] = useState(null); const [pmFeedback, setPmFeedback] = useState(""); // Step 2 — QA test cases const [qaStream, setQaStream] = useState(""); const [testCases, setTestCases] = useState([]); const [qaFeedback, setQaFeedback] = useState(""); // Step 3 — Dev code const [devStream, setDevStream] = useState(""); const [codeTab, setCodeTab] = useState<"code" | "test" | "notes">("code"); const [devCode, setDevCode] = useState(""); const [devTest, setDevTest] = useState(""); const [devNotes, setDevNotes] = useState(""); // Step 4 — Test results const [testResult, setTestResult] = useState(null); const abortRef = useRef(null); const bottomRef = useRef(null); useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [pmStream, qaStream, devStream]); useEffect(() => { return () => abortRef.current?.abort(); }, []); /* ── Step 0: Start session ── */ const handleStart = useCallback(async () => { if (!requirement.trim() || loading) return; setLoading(true); setError(""); try { const data: any = await apiPost("/session/start", { requirement: requirement.trim() }); setSessionId(data.session_id); setRawReq(requirement.trim()); setStatus(data.status || "clarifying"); if (data.clarify_questions) { setClarifyHistory([{ role: "assistant", content: data.clarify_questions }]); } } catch (e) { setError((e as Error).message); } finally { setLoading(false); } }, [requirement, loading]); /* ── Step 0: Clarify ── */ const handleClarify = useCallback(async () => { if (!clarifyInput.trim() || loading) return; setClarifyHistory((h) => [...h, { role: "user", content: clarifyInput.trim() }]); setLoading(true); setError(""); try { const data: any = await apiPost(`/session/${sessionId}/clarify`, { message: clarifyInput.trim() }); setClarifyInput(""); setStatus(data.status || status); if (data.clarify_questions) { setClarifyHistory((h) => [...h, { role: "assistant", content: data.clarify_questions }]); } } catch (e) { setError((e as Error).message); } finally { setLoading(false); } }, [clarifyInput, sessionId, loading, status]); /* ── Step 1: PM Run ── */ const handlePmRun = useCallback(async () => { setStep(1); setLoading(true); setError(""); setPmStream(""); try { await apiPost(`/session/${sessionId}/pm/run`); const ctrl = new AbortController(); abortRef.current = ctrl; let full = ""; await readSSE(`/session/${sessionId}/pm/stream`, (text) => { full += text; setPmStream(full); }, ctrl.signal); // Try to parse structured result try { const json = JSON.parse(full); setPmResult(json); } catch { setPmResult({ summary: full, functional: [], nonfunctional: [], acceptance: [], edge_cases: [] }); } } catch (e) { setError((e as Error).message); } finally { setLoading(false); } }, [sessionId]); /* ── Step 1: PM Refine ── */ const handlePmRefine = useCallback(async () => { if (!pmFeedback.trim()) return; setLoading(true); setPmStream(""); try { const ctrl = new AbortController(); abortRef.current = ctrl; let full = ""; await readSSE( `/session/${sessionId}/pm/refine/stream?feedback=${encodeURIComponent(pmFeedback.trim())}`, (text) => { full += text; setPmStream(full); }, ctrl.signal, ); setPmFeedback(""); } catch (e) { setError((e as Error).message); } finally { setLoading(false); } }, [sessionId, pmFeedback]); /* ── Step 2: QA Run ── */ const handleQaRun = useCallback(async () => { setStep(2); setLoading(true); setError(""); setQaStream(""); try { await apiPost(`/session/${sessionId}/qa/run`); const ctrl = new AbortController(); abortRef.current = ctrl; let full = ""; await readSSE(`/session/${sessionId}/qa/stream`, (text) => { full += text; setQaStream(full); }, ctrl.signal); // Try parse try { const json = JSON.parse(full); if (json.test_cases) setTestCases(json.test_cases); } catch { /* raw display */ } } catch (e) { setError((e as Error).message); } finally { setLoading(false); } }, [sessionId]); /* ── Step 3: Dev Run ── */ const handleDevRun = useCallback(async () => { setStep(3); setLoading(true); setError(""); setDevStream(""); try { await apiPost(`/session/${sessionId}/dev/run`); const ctrl = new AbortController(); abortRef.current = ctrl; let full = ""; await readSSE(`/session/${sessionId}/dev/stream`, (text) => { full += text; setDevStream(full); }, ctrl.signal); // Parse code blocks try { const json = JSON.parse(full); setDevCode(json.code || json.implementation || full); setDevTest(json.tests || json.test_code || ""); setDevNotes(json.notes || json.implementation_notes || ""); } catch { setDevCode(full); } } catch (e) { setError((e as Error).message); } finally { setLoading(false); } }, [sessionId]); /* ── Step 4: Test Run ── */ const handleTestRun = useCallback(async () => { setStep(4); setLoading(true); setError(""); try { const data: any = await apiPost(`/session/${sessionId}/test/run`); setTestResult({ passed: data.passed ?? 0, failed: data.failed ?? 0, errors: data.errors ?? 0, total: data.total ?? 0, output: data.output || data.detail || JSON.stringify(data, null, 2), }); } catch (e) { setError((e as Error).message); } finally { setLoading(false); } }, [sessionId]); /* ── Test Fix ── */ const handleTestFix = useCallback(async () => { setLoading(true); try { const ctrl = new AbortController(); abortRef.current = ctrl; let full = ""; await readSSE(`/session/${sessionId}/test/fix/stream`, (text) => { full += text; setDevStream(full); }, ctrl.signal); // Re-run test await handleTestRun(); } catch (e) { setError((e as Error).message); } finally { setLoading(false); } }, [sessionId, handleTestRun]); return (
{/* ── Step bar ── */}
{STEPS.map((s, i) => (
{i < STEPS.length - 1 && }
))}
{/* Error banner */} {error && (
{error}
)} {/* ── Content ── */}
{/* ═══ Step 0: Requirement ═══ */} {step === 0 && !sessionId && (

📋 Describe Product Requirement

Enter your product requirement, the system will automatically perform requirement clarification and analysis