import { useState, useCallback, useEffect, useRef } from "react"; import type { Task, PlanningQuestion, PlanningSummary } from "@kb/core"; import { startPlanning, respondToPlanning, cancelPlanning, createTaskFromPlanning, type PlanningSession, } from "../api"; import { Lightbulb, X, Loader2, CheckCircle, ArrowLeft, ArrowRight, Sparkles } from "lucide-react"; interface PlanningModeModalProps { isOpen: boolean; onClose: () => void; onTaskCreated: (task: Task) => void; tasks: Task[]; initialPlan?: string; } interface QuestionResponse { [key: string]: unknown; } type ViewState = | { type: "initial" } | { type: "question"; session: PlanningSession } | { type: "summary"; session: PlanningSession; summary: PlanningSummary } | { type: "loading" }; const EXAMPLE_PLANS = [ "Build a user authentication system with login and signup", "Add dark mode support to the dashboard", "Create an API endpoint for exporting tasks as CSV", "Refactor the task card component for better performance", ]; export function PlanningModeModal({ isOpen, onClose, onTaskCreated, tasks, initialPlan: initialPlanProp }: PlanningModeModalProps) { const [initialPlan, setInitialPlan] = useState(""); const [view, setView] = useState({ type: "initial" }); const [error, setError] = useState(null); const [responseHistory, setResponseHistory] = useState([]); const [editedSummary, setEditedSummary] = useState(null); const [hasAutoStarted, setHasAutoStarted] = useState(false); const textareaRef = useRef(null); // Focus textarea when opening useEffect(() => { if (isOpen && view.type === "initial") { textareaRef.current?.focus(); } }, [isOpen, view.type]); // Auto-start planning when initialPlan prop is provided useEffect(() => { if (isOpen && initialPlanProp && !hasAutoStarted && view.type === "initial") { setInitialPlan(initialPlanProp); setHasAutoStarted(true); // Use a small timeout to allow state update to propagate before starting const timer = setTimeout(() => { handleStartPlanningWithPlan(initialPlanProp); }, 0); return () => clearTimeout(timer); } }, [isOpen, initialPlanProp, hasAutoStarted, view.type]); // Reset hasAutoStarted when modal closes useEffect(() => { if (!isOpen) { setHasAutoStarted(false); } }, [isOpen]); // Handle browser unload during active session useEffect(() => { if (!isOpen) return; const handleBeforeUnload = (e: BeforeUnloadEvent) => { if (view.type === "question" || view.type === "summary") { e.preventDefault(); e.returnValue = ""; } }; window.addEventListener("beforeunload", handleBeforeUnload); return () => window.removeEventListener("beforeunload", handleBeforeUnload); }, [isOpen, view]); // Handle escape key to close useEffect(() => { if (!isOpen) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { if (view.type === "question" || view.type === "summary") { if (confirm("Are you sure you want to close? Your planning progress will be lost.")) { handleCancel(); } } else { handleCancel(); } } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [isOpen, view]); const handleStartPlanning = useCallback(async () => { if (!initialPlan.trim()) return; setError(null); setView({ type: "loading" }); try { const session = await startPlanning(initialPlan.trim()); if (session.currentQuestion) { setView({ type: "question", session }); } else if (session.summary) { setView({ type: "summary", session, summary: session.summary }); setEditedSummary(session.summary); } setResponseHistory([]); } catch (err: any) { setError(err.message || "Failed to start planning session"); setView({ type: "initial" }); } }, [initialPlan]); // Helper for auto-start with a specific plan (from prop) const handleStartPlanningWithPlan = useCallback(async (plan: string) => { if (!plan.trim()) return; setError(null); setView({ type: "loading" }); try { const session = await startPlanning(plan.trim()); if (session.currentQuestion) { setView({ type: "question", session }); } else if (session.summary) { setView({ type: "summary", session, summary: session.summary }); setEditedSummary(session.summary); } setResponseHistory([]); } catch (err: any) { setError(err.message || "Failed to start planning session"); setView({ type: "initial" }); } }, []); const handleSubmitResponse = useCallback( async (responses: QuestionResponse) => { if (view.type !== "question") return; const { session } = view; setError(null); setView({ type: "loading" }); try { const updatedSession = await respondToPlanning(session.sessionId, responses); setResponseHistory((prev) => [...prev, responses]); if (updatedSession.summary) { setView({ type: "summary", session: updatedSession, summary: updatedSession.summary }); setEditedSummary(updatedSession.summary); } else if (updatedSession.currentQuestion) { setView({ type: "question", session: updatedSession }); } } catch (err: any) { setError(err.message || "Failed to submit response"); setView({ type: "question", session }); } }, [view] ); const handleCancel = useCallback(async () => { if (view.type === "question" || view.type === "summary") { try { await cancelPlanning(view.session.sessionId); } catch { // Ignore errors on cancel } } setInitialPlan(""); setView({ type: "initial" }); setError(null); setResponseHistory([]); setEditedSummary(null); onClose(); }, [view, onClose]); const handleCreateTask = useCallback(async () => { if (view.type !== "summary") return; setError(null); setView({ type: "loading" }); try { const task = await createTaskFromPlanning(view.session.sessionId); onTaskCreated(task); handleCancel(); } catch (err: any) { setError(err.message || "Failed to create task"); setView({ type: "summary", session: view.session, summary: view.summary }); } }, [view, onTaskCreated, handleCancel]); const handleBack = useCallback(() => { if (view.type === "question" && responseHistory.length > 0) { // Remove last response and go back const previousResponses = responseHistory.slice(0, -1); setResponseHistory(previousResponses); // Note: We don't actually have a way to go back in the backend, // so we just reset to the question from the initial session setView({ type: "question", session: view.session }); } }, [view, responseHistory]); const getProgress = () => { if (view.type === "question") { return Math.min(responseHistory.length + 1, 3); } return 3; }; if (!isOpen) return null; return (
e.target === e.currentTarget && handleCancel()}>

Planning Mode

{error &&
{error}
} {view.type === "initial" && (

Transform your idea into a detailed task

Describe what you want to build in plain language. The AI will ask clarifying questions and help you structure a well-defined task.