diff --git a/apps/web/src/components/gen-ui/action-step.tsx b/apps/web/src/components/gen-ui/action-step.tsx new file mode 100644 index 00000000..15644544 --- /dev/null +++ b/apps/web/src/components/gen-ui/action-step.tsx @@ -0,0 +1,281 @@ +"use client"; + +import "../app/globals.css"; +import { useState } from "react"; +import { + Terminal, + FileCode, + Loader2, + CheckCircle, + XCircle, + ChevronDown, + ChevronUp, + MessageSquare, + FileText, +} from "lucide-react"; + +// Common props for all action types +type BaseActionProps = { + status: "loading" | "generating" | "done"; + success?: boolean; + reasoningText?: string; + summaryText?: string; +}; + +// Shell command specific props +type ShellActionProps = BaseActionProps & { + actionType: "shell"; + command: string; + workdir?: string; + output?: string; + errorCode?: number; +}; + +// Apply patch specific props +type PatchActionProps = BaseActionProps & { + actionType: "apply-patch"; + file: string; + diff?: string; + errorMessage?: string; + fixedDiff?: string; +}; + +// Union type for all possible action props +type ActionStepProps = + | (BaseActionProps & { status: "loading" }) + | ShellActionProps + | PatchActionProps; + +export function ActionStep(props: ActionStepProps) { + const [expanded, setExpanded] = useState(props.status === "done"); + const [showReasoning, setShowReasoning] = useState(false); + const [showSummary, setShowSummary] = useState(false); + + const getStatusIcon = () => { + switch (props.status) { + case "loading": + return ( +
+ ); + case "generating": + return ; + case "done": + return props.success ? ( + + ) : ( + + ); + } + }; + + const getStatusText = () => { + if (props.status === "loading") { + return "Preparing action..."; + } + + if (props.status === "generating") { + return props.actionType === "shell" + ? "Executing..." + : "Applying patch..."; + } + + if (props.status === "done") { + if (props.actionType === "shell") { + return props.success ? "Command completed" : "Command failed"; + } else if (props.actionType === "apply-patch") { + return props.success ? "Patch applied" : "Patch failed"; + } + } + + return ""; + }; + + // Determine if we should show the content toggle button + const shouldShowToggle = () => { + if (props.status !== "done") return false; + + if (props.actionType === "shell") { + return !!props.output; + } else if (props.actionType === "apply-patch") { + return !!props.diff; + } + + return false; + }; + + // Render the header icon based on action type + const renderHeaderIcon = () => { + if (props.status === "loading" || !("actionType" in props)) { + // In loading state, we don't know the type yet, use a generic icon + return ; + } + + return props.actionType === "shell" ? ( + + ) : ( + + ); + }; + + // Render the header content based on action type + const renderHeaderContent = () => { + if (props.status === "loading" || !("actionType" in props)) { + return ( + + Preparing action... + + ); + } + + if (props.actionType === "shell") { + return ( +
+ {props.workdir && ( +
+ {props.workdir} +
+ )} + + {props.command} + +
+ ); + } else { + return ( + + {props.file} + + ); + } + }; + + // Render the content based on action type + const renderContent = () => { + if (props.status !== "done" || !("actionType" in props)) return null; + + if (!expanded) return null; + + if (props.actionType === "shell" && props.output) { + return ( +
+
+            {props.output}
+          
+ {props.errorCode !== undefined && !props.success && ( +
+ Exit code: {props.errorCode} +
+ )} +
+ ); + } else if (props.actionType === "apply-patch" && props.diff) { + return ( +
+
+
+          {!props.success && props.errorMessage && (
+            
+ {props.errorMessage} +
+ )} + + {!props.success && props.fixedDiff && ( +
+
Fixed diff:
+
+            
+ )} +
+ ); + } + + return null; + }; + + return ( +
+ {props.reasoningText && ( +
+ + {showReasoning && ( +

+ {props.reasoningText} +

+ )} +
+ )} + +
+ {renderHeaderIcon()} + {renderHeaderContent()} +
+ + {getStatusText()} + + {getStatusIcon()} + {shouldShowToggle() && ( + + )} +
+
+ + {renderContent()} + + {props.summaryText && props.status === "done" && ( +
+ + {showSummary && ( +

+ {props.summaryText} +

+ )} +
+ )} +
+ ); +} + +function formatDiff(diff: string) { + return diff + .split("\n") + .map((line) => { + if (line.startsWith("+")) { + return `${line}`; + } else if (line.startsWith("-")) { + return `${line}`; + } else if (line.startsWith("@")) { + return `${line}`; + } + return line; + }) + .join("\n"); +} diff --git a/apps/web/src/components/gen-ui/diagnose-error.tsx b/apps/web/src/components/gen-ui/diagnose-error.tsx new file mode 100644 index 00000000..7013f3cf --- /dev/null +++ b/apps/web/src/components/gen-ui/diagnose-error.tsx @@ -0,0 +1,122 @@ +"use client"; + +import "../app/globals.css"; +import { + AlertTriangle, + Loader2, + CheckCircle, + MessageSquare, + FileText, +} from "lucide-react"; +import { useState } from "react"; + +type DiagnoseErrorProps = { + status: "loading" | "generating" | "done"; + diagnosis?: string; + recommendation?: string; + reasoningText?: string; + summaryText?: string; +}; + +export function DiagnoseError({ + status, + diagnosis, + recommendation, + reasoningText, + summaryText, +}: DiagnoseErrorProps) { + const [showReasoning, setShowReasoning] = useState(false); + const [showSummary, setShowSummary] = useState(false); + + const getStatusIcon = () => { + switch (status) { + case "loading": + return ( +
+ ); + case "generating": + return ; + case "done": + return ; + } + }; + + const getStatusText = () => { + switch (status) { + case "loading": + return "Preparing error analysis..."; + case "generating": + return "Diagnosing errors..."; + case "done": + return "Error diagnosis complete"; + } + }; + + return ( +
+ {reasoningText && ( +
+ + {showReasoning && ( +

+ {reasoningText} +

+ )} +
+ )} + +
+ + + {getStatusText()} + + {getStatusIcon()} +
+ + {status === "done" && diagnosis && ( +
+
+

+ Diagnosis +

+

{diagnosis}

+
+ + {recommendation && ( +
+

+ Recommendation +

+

+ {recommendation} +

+
+ )} +
+ )} + + {summaryText && status === "done" && ( +
+ + {showSummary && ( +

+ {summaryText} +

+ )} +
+ )} +
+ ); +} diff --git a/apps/web/src/components/gen-ui/index.tsx b/apps/web/src/components/gen-ui/index.tsx new file mode 100644 index 00000000..ebf0d372 --- /dev/null +++ b/apps/web/src/components/gen-ui/index.tsx @@ -0,0 +1,15 @@ +import { ActionStep } from "./action-step"; +import { DiagnoseError } from "./diagnose-error"; +import { InitializeStep } from "./initialize-step"; +import { PushChanges } from "./push-changes"; +import { ReplanningStep } from "./replanning-step"; +import { TaskSummary } from "./task-summary"; + +export default { + "action-step": ActionStep, + "diagnose-error": DiagnoseError, + "initialize-step": InitializeStep, + "push-changes": PushChanges, + "replanning-step": ReplanningStep, + "task-summary": TaskSummary, +}; diff --git a/apps/web/src/components/gen-ui/initialize-step.tsx b/apps/web/src/components/gen-ui/initialize-step.tsx new file mode 100644 index 00000000..eed5448a --- /dev/null +++ b/apps/web/src/components/gen-ui/initialize-step.tsx @@ -0,0 +1,145 @@ +"use client"; + +import "../app/globals.css"; +import { + Loader2, + CheckCircle, + XCircle, + GitBranch, + MessageSquare, + FileText, +} from "lucide-react"; +import { useState } from "react"; + +type Step = { + name: string; + status: "waiting" | "generating" | "success" | "error"; + error?: string; +}; + +type InitializeStepProps = { + status: "loading" | "generating" | "done"; + success?: boolean; + steps?: Step[]; + reasoningText?: string; + summaryText?: string; +}; + +export function InitializeStep({ + status, + success, + steps, + reasoningText, + summaryText, +}: InitializeStepProps) { + const [showReasoning, setShowReasoning] = useState(false); + const [showSummary, setShowSummary] = useState(false); + + const stepStatusIcon = { + waiting: ( +
+ ), + generating: , + success: , + error: , + }; + + const getStatusIcon = () => { + switch (status) { + case "loading": + return ( +
+ ); + case "generating": + return ; + case "done": + return success ? ( + + ) : ( + + ); + } + }; + + const getStatusText = () => { + switch (status) { + case "loading": + return "Preparing environment..."; + case "generating": + return "Initializing environment..."; + case "done": + return success ? "Environment ready" : "Initialization failed"; + } + }; + + return ( +
+ {reasoningText && ( +
+ + {showReasoning && ( +

+ {reasoningText} +

+ )} +
+ )} + +
+ + + {getStatusText()} + + {getStatusIcon()} +
+ + {steps && (status === "generating" || status === "done") && ( +
+
    + {steps.map((step, index) => ( +
  • + {stepStatusIcon[step.status]} + + {step.name} + + {step.error && ( + + ({step.error}) + + )} +
  • + ))} +
+
+ )} + + {summaryText && status === "done" && ( +
+ + {showSummary && ( +

+ {summaryText} +

+ )} +
+ )} +
+ ); +} diff --git a/apps/web/src/components/gen-ui/push-changes.tsx b/apps/web/src/components/gen-ui/push-changes.tsx new file mode 100644 index 00000000..daf613e0 --- /dev/null +++ b/apps/web/src/components/gen-ui/push-changes.tsx @@ -0,0 +1,163 @@ +"use client"; + +import "../app/globals.css"; +import { useState } from "react"; +import { + GitCommit, + Loader2, + CheckCircle, + XCircle, + ChevronDown, + ChevronUp, + MessageSquare, + FileText, +} from "lucide-react"; + +type PushChangesProps = { + status: "loading" | "generating" | "done"; + success?: boolean; + gitStatus?: string; + commitMessage?: string; + errorMessage?: string; + reasoningText?: string; + summaryText?: string; +}; + +export function PushChanges({ + status, + success, + gitStatus, + commitMessage, + errorMessage, + reasoningText, + summaryText, +}: PushChangesProps) { + const [expanded, setExpanded] = useState( + Boolean(status === "done" && gitStatus), + ); + const [showReasoning, setShowReasoning] = useState(false); + const [showSummary, setShowSummary] = useState(false); + + const getStatusIcon = () => { + switch (status) { + case "loading": + return ( +
+ ); + case "generating": + return ; + case "done": + return success ? ( + + ) : ( + + ); + } + }; + + const getStatusText = () => { + switch (status) { + case "loading": + return "Preparing changes..."; + case "generating": + return "Pushing changes..."; + case "done": + return success + ? "Changes pushed successfully" + : "Failed to push changes"; + } + }; + + return ( +
+ {reasoningText && ( +
+ + {showReasoning && ( +

+ {reasoningText} +

+ )} +
+ )} + +
+ + + {getStatusText()} + +
+ {getStatusIcon()} + {gitStatus && status === "done" && ( + + )} +
+
+ + {expanded && gitStatus && status === "done" && ( +
+
+

+ Git Status +

+
+              {gitStatus}
+            
+
+ + {commitMessage && success && ( +
+

+ Commit Message +

+
+ {commitMessage} +
+
+ )} + + {errorMessage && !success && ( +
+

Error

+
+ {errorMessage} +
+
+ )} +
+ )} + + {summaryText && status === "done" && ( +
+ + {showSummary && ( +

+ {summaryText} +

+ )} +
+ )} +
+ ); +} diff --git a/apps/web/src/components/gen-ui/replanning-step.tsx b/apps/web/src/components/gen-ui/replanning-step.tsx new file mode 100644 index 00000000..70571b63 --- /dev/null +++ b/apps/web/src/components/gen-ui/replanning-step.tsx @@ -0,0 +1,96 @@ +"use client"; + +import "../app/globals.css"; +import { + RefreshCw, + Loader2, + CheckCircle, + MessageSquare, + FileText, +} from "lucide-react"; +import { useState } from "react"; + +type ReplanningStepProps = { + status: "loading" | "generating" | "done"; + reasoningText?: string; + summaryText?: string; +}; + +export function ReplanningStep({ + status, + reasoningText, + summaryText, +}: ReplanningStepProps) { + const [showReasoning, setShowReasoning] = useState(false); + const [showSummary, setShowSummary] = useState(false); + + const getStatusIcon = () => { + switch (status) { + case "loading": + return ( +
+ ); + case "generating": + return ; + case "done": + return ; + } + }; + + const getStatusText = () => { + switch (status) { + case "loading": + return "Preparing to update plan..."; + case "generating": + return "Updating plan..."; + case "done": + return "Plan updated"; + } + }; + + return ( +
+ {reasoningText && ( +
+ + {showReasoning && ( +

+ {reasoningText} +

+ )} +
+ )} + +
+ + + {getStatusText()} + + {getStatusIcon()} +
+ + {summaryText && status === "done" && ( +
+ + {showSummary && ( +

+ {summaryText} +

+ )} +
+ )} +
+ ); +} diff --git a/apps/web/src/components/gen-ui/task-summary.tsx b/apps/web/src/components/gen-ui/task-summary.tsx new file mode 100644 index 00000000..5fc2a57e --- /dev/null +++ b/apps/web/src/components/gen-ui/task-summary.tsx @@ -0,0 +1,124 @@ +"use client"; + +import "../app/globals.css"; +import { useState } from "react"; +import { + CheckCircle, + Loader2, + ChevronDown, + ChevronUp, + MessageSquare, + FileText, +} from "lucide-react"; + +type TaskSummaryProps = { + status: "loading" | "generating" | "done"; + summary?: string; + reasoningText?: string; + summaryText?: string; +}; + +export function TaskSummary({ + status, + summary, + reasoningText, + summaryText, +}: TaskSummaryProps) { + const [expanded, setExpanded] = useState(false); + const [showReasoning, setShowReasoning] = useState(false); + const [showSummary, setShowSummary] = useState(false); + + const getStatusIcon = () => { + switch (status) { + case "loading": + return ( +
+ ); + case "generating": + return ; + case "done": + return ; + } + }; + + const getStatusText = () => { + switch (status) { + case "loading": + return "Preparing task summary..."; + case "generating": + return "Generating summary..."; + case "done": + return "Task completed"; + } + }; + + return ( +
+ {reasoningText && ( +
+ + {showReasoning && ( +

+ {reasoningText} +

+ )} +
+ )} + +
setExpanded(!expanded) + : undefined + } + > + {getStatusIcon()} + + {getStatusText()} + + {status === "done" && summary && ( + + )} +
+ + {expanded && summary && status === "done" && ( +
+

+ Task Summary +

+

{summary}

+
+ )} + + {summaryText && status === "done" && ( +
+ + {showSummary && ( +

+ {summaryText} +

+ )} +
+ )} +
+ ); +}