add tool genui components to web/src/components/gen-ui (#159)

This commit is contained in:
Dylan Boudro 2025-06-13 10:28:21 -07:00 • committed by GitHub
parent 0bfbf99db9
commit eadf05ed76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 946 additions and 0 deletions

View file

@ -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 (
<div className="h-3.5 w-3.5 rounded-full border border-gray-300" />
);
case "generating":
return <Loader2 className="h-3.5 w-3.5 animate-spin text-gray-500" />;
case "done":
return props.success ? (
<CheckCircle className="h-3.5 w-3.5 text-green-500" />
) : (
<XCircle className="h-3.5 w-3.5 text-red-500" />
);
}
};
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 <Loader2 className="mr-2 h-3.5 w-3.5 text-gray-500" />;
}
return props.actionType === "shell" ? (
<Terminal className="mr-2 h-3.5 w-3.5 text-gray-500" />
) : (
<FileCode className="mr-2 h-3.5 w-3.5 text-gray-500" />
);
};
// Render the header content based on action type
const renderHeaderContent = () => {
if (props.status === "loading" || !("actionType" in props)) {
return (
<span className="text-xs font-normal text-gray-800">
Preparing action...
</span>
);
}
if (props.actionType === "shell") {
return (
<div className="flex-1">
{props.workdir && (
<div className="mb-0.5 text-xs font-normal text-gray-500">
{props.workdir}
</div>
)}
<code className="text-xs font-normal text-gray-800">
{props.command}
</code>
</div>
);
} else {
return (
<code className="flex-1 text-xs font-normal text-gray-800">
{props.file}
</code>
);
}
};
// 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 (
<div className="overflow-x-auto bg-gray-900 p-2 text-gray-200">
<pre className="text-xs font-normal whitespace-pre-wrap">
{props.output}
</pre>
{props.errorCode !== undefined && !props.success && (
<div className="mt-1 text-xs text-red-400">
Exit code: {props.errorCode}
</div>
)}
</div>
);
} else if (props.actionType === "apply-patch" && props.diff) {
return (
<div className="overflow-x-auto bg-gray-900 p-2">
<pre
className="text-xs font-normal whitespace-pre-wrap text-gray-200"
dangerouslySetInnerHTML={{ __html: formatDiff(props.diff) }}
/>
{!props.success && props.errorMessage && (
<div className="mt-2 rounded border border-red-700/30 bg-red-900/30 p-2 text-xs text-red-400">
{props.errorMessage}
</div>
)}
{!props.success && props.fixedDiff && (
<div className="mt-2">
<div className="mb-1 text-xs text-gray-400">Fixed diff:</div>
<pre
className="text-xs font-normal whitespace-pre-wrap text-gray-200"
dangerouslySetInnerHTML={{
__html: formatDiff(props.fixedDiff),
}}
/>
</div>
)}
</div>
);
}
return null;
};
return (
<div className="overflow-hidden rounded-md border border-gray-200">
{props.reasoningText && (
<div className="border-b border-blue-100 bg-blue-50 p-2">
<button
onClick={() => setShowReasoning(!showReasoning)}
className="flex items-center gap-1 text-xs font-normal text-blue-700 hover:text-blue-800"
>
<MessageSquare className="h-3 w-3" />
{showReasoning ? "Hide reasoning" : "Show reasoning"}
</button>
{showReasoning && (
<p className="mt-1 text-xs font-normal text-blue-800">
{props.reasoningText}
</p>
)}
</div>
)}
<div className="flex items-center border-b border-gray-200 bg-gray-50 p-2">
{renderHeaderIcon()}
{renderHeaderContent()}
<div className="flex items-center gap-2">
<span className="text-xs font-normal text-gray-500">
{getStatusText()}
</span>
{getStatusIcon()}
{shouldShowToggle() && (
<button
onClick={() => setExpanded(!expanded)}
className="text-gray-500 hover:text-gray-700"
>
{expanded ? (
<ChevronUp className="h-3.5 w-3.5" />
) : (
<ChevronDown className="h-3.5 w-3.5" />
)}
</button>
)}
</div>
</div>
{renderContent()}
{props.summaryText && props.status === "done" && (
<div className="border-t border-green-100 bg-green-50 p-2">
<button
onClick={() => setShowSummary(!showSummary)}
className="flex items-center gap-1 text-xs font-normal text-green-700 hover:text-green-800"
>
<FileText className="h-3 w-3" />
{showSummary ? "Hide summary" : "Show summary"}
</button>
{showSummary && (
<p className="mt-1 text-xs font-normal text-green-800">
{props.summaryText}
</p>
)}
</div>
)}
</div>
);
}
function formatDiff(diff: string) {
return diff
.split("\n")
.map((line) => {
if (line.startsWith("+")) {
return `<span class="text-green-400">${line}</span>`;
} else if (line.startsWith("-")) {
return `<span class="text-red-400">${line}</span>`;
} else if (line.startsWith("@")) {
return `<span class="text-blue-400">${line}</span>`;
}
return line;
})
.join("\n");
}

View file

@ -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 (
<div className="h-3.5 w-3.5 rounded-full border border-gray-300" />
);
case "generating":
return <Loader2 className="h-3.5 w-3.5 animate-spin text-gray-500" />;
case "done":
return <CheckCircle className="h-3.5 w-3.5 text-green-500" />;
}
};
const getStatusText = () => {
switch (status) {
case "loading":
return "Preparing error analysis...";
case "generating":
return "Diagnosing errors...";
case "done":
return "Error diagnosis complete";
}
};
return (
<div className="overflow-hidden rounded-md border border-gray-200">
{reasoningText && (
<div className="border-b border-blue-100 bg-blue-50 p-2">
<button
onClick={() => setShowReasoning(!showReasoning)}
className="flex items-center gap-1 text-xs font-normal text-blue-700 hover:text-blue-800"
>
<MessageSquare className="h-3 w-3" />
{showReasoning ? "Hide reasoning" : "Show reasoning"}
</button>
{showReasoning && (
<p className="mt-1 text-xs font-normal text-blue-800">
{reasoningText}
</p>
)}
</div>
)}
<div className="flex items-center border-b border-gray-200 bg-gray-50 p-2">
<AlertTriangle className="mr-2 h-3.5 w-3.5 text-amber-500" />
<span className="flex-1 text-xs font-normal text-gray-800">
{getStatusText()}
</span>
{getStatusIcon()}
</div>
{status === "done" && diagnosis && (
<div className="p-2">
<div className="mb-2">
<h3 className="mb-1 text-xs font-normal text-gray-500">
Diagnosis
</h3>
<p className="text-xs font-normal text-gray-800">{diagnosis}</p>
</div>
{recommendation && (
<div>
<h3 className="mb-1 text-xs font-normal text-gray-500">
Recommendation
</h3>
<p className="text-xs font-normal text-gray-800">
{recommendation}
</p>
</div>
)}
</div>
)}
{summaryText && status === "done" && (
<div className="border-t border-green-100 bg-green-50 p-2">
<button
onClick={() => setShowSummary(!showSummary)}
className="flex items-center gap-1 text-xs font-normal text-green-700 hover:text-green-800"
>
<FileText className="h-3 w-3" />
{showSummary ? "Hide summary" : "Show summary"}
</button>
{showSummary && (
<p className="mt-1 text-xs font-normal text-green-800">
{summaryText}
</p>
)}
</div>
)}
</div>
);
}

View file

@ -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,
};

View file

@ -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: (
<div className="h-3.5 w-3.5 rounded-full border border-gray-300" />
),
generating: <Loader2 className="h-3.5 w-3.5 animate-spin text-gray-500" />,
success: <CheckCircle className="h-3.5 w-3.5 text-green-500" />,
error: <XCircle className="h-3.5 w-3.5 text-red-500" />,
};
const getStatusIcon = () => {
switch (status) {
case "loading":
return (
<div className="h-3.5 w-3.5 rounded-full border border-gray-300" />
);
case "generating":
return <Loader2 className="h-3.5 w-3.5 animate-spin text-gray-500" />;
case "done":
return success ? (
<CheckCircle className="h-3.5 w-3.5 text-green-500" />
) : (
<XCircle className="h-3.5 w-3.5 text-red-500" />
);
}
};
const getStatusText = () => {
switch (status) {
case "loading":
return "Preparing environment...";
case "generating":
return "Initializing environment...";
case "done":
return success ? "Environment ready" : "Initialization failed";
}
};
return (
<div className="overflow-hidden rounded-md border border-gray-200">
{reasoningText && (
<div className="border-b border-blue-100 bg-blue-50 p-2">
<button
onClick={() => setShowReasoning(!showReasoning)}
className="flex items-center gap-1 text-xs font-normal text-blue-700 hover:text-blue-800"
>
<MessageSquare className="h-3 w-3" />
{showReasoning ? "Hide reasoning" : "Show reasoning"}
</button>
{showReasoning && (
<p className="mt-1 text-xs font-normal text-blue-800">
{reasoningText}
</p>
)}
</div>
)}
<div className="flex items-center border-b border-gray-200 bg-gray-50 p-2">
<GitBranch className="mr-2 h-3.5 w-3.5 text-gray-500" />
<span className="flex-1 text-xs font-normal text-gray-800">
{getStatusText()}
</span>
{getStatusIcon()}
</div>
{steps && (status === "generating" || status === "done") && (
<div className="p-2">
<ul className="space-y-2">
{steps.map((step, index) => (
<li
key={index}
className="flex items-center text-xs"
>
<span className="mr-2">{stepStatusIcon[step.status]}</span>
<span
className={`font-normal ${step.status === "error" ? "text-red-500" : "text-gray-800"}`}
>
{step.name}
</span>
{step.error && (
<span className="ml-2 text-xs text-red-500">
({step.error})
</span>
)}
</li>
))}
</ul>
</div>
)}
{summaryText && status === "done" && (
<div className="border-t border-green-100 bg-green-50 p-2">
<button
onClick={() => setShowSummary(!showSummary)}
className="flex items-center gap-1 text-xs font-normal text-green-700 hover:text-green-800"
>
<FileText className="h-3 w-3" />
{showSummary ? "Hide summary" : "Show summary"}
</button>
{showSummary && (
<p className="mt-1 text-xs font-normal text-green-800">
{summaryText}
</p>
)}
</div>
)}
</div>
);
}

View file

@ -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 (
<div className="h-3.5 w-3.5 rounded-full border border-gray-300" />
);
case "generating":
return <Loader2 className="h-3.5 w-3.5 animate-spin text-gray-500" />;
case "done":
return success ? (
<CheckCircle className="h-3.5 w-3.5 text-green-500" />
) : (
<XCircle className="h-3.5 w-3.5 text-red-500" />
);
}
};
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 (
<div className="overflow-hidden rounded-md border border-gray-200">
{reasoningText && (
<div className="border-b border-blue-100 bg-blue-50 p-2">
<button
onClick={() => setShowReasoning(!showReasoning)}
className="flex items-center gap-1 text-xs font-normal text-blue-700 hover:text-blue-800"
>
<MessageSquare className="h-3 w-3" />
{showReasoning ? "Hide reasoning" : "Show reasoning"}
</button>
{showReasoning && (
<p className="mt-1 text-xs font-normal text-blue-800">
{reasoningText}
</p>
)}
</div>
)}
<div className="flex items-center border-b border-gray-200 bg-gray-50 p-2">
<GitCommit className="mr-2 h-3.5 w-3.5 text-gray-500" />
<span className="flex-1 text-xs font-normal text-gray-800">
{getStatusText()}
</span>
<div className="flex items-center gap-2">
{getStatusIcon()}
{gitStatus && status === "done" && (
<button
onClick={() => setExpanded(!expanded)}
className="text-gray-500 hover:text-gray-700"
>
{expanded ? (
<ChevronUp className="h-3.5 w-3.5" />
) : (
<ChevronDown className="h-3.5 w-3.5" />
)}
</button>
)}
</div>
</div>
{expanded && gitStatus && status === "done" && (
<div className="p-2">
<div className="mb-2">
<h3 className="mb-1 text-xs font-normal text-gray-500">
Git Status
</h3>
<pre className="rounded border border-gray-200 bg-gray-50 p-1.5 text-xs font-normal whitespace-pre-wrap">
{gitStatus}
</pre>
</div>
{commitMessage && success && (
<div>
<h3 className="mb-1 text-xs font-normal text-gray-500">
Commit Message
</h3>
<div className="rounded border border-gray-200 bg-gray-50 p-1.5 text-xs font-normal">
{commitMessage}
</div>
</div>
)}
{errorMessage && !success && (
<div>
<h3 className="mb-1 text-xs font-normal text-gray-500">Error</h3>
<div className="rounded border border-red-100 bg-red-50 p-1.5 text-xs font-normal text-red-500">
{errorMessage}
</div>
</div>
)}
</div>
)}
{summaryText && status === "done" && (
<div className="border-t border-green-100 bg-green-50 p-2">
<button
onClick={() => setShowSummary(!showSummary)}
className="flex items-center gap-1 text-xs font-normal text-green-700 hover:text-green-800"
>
<FileText className="h-3 w-3" />
{showSummary ? "Hide summary" : "Show summary"}
</button>
{showSummary && (
<p className="mt-1 text-xs font-normal text-green-800">
{summaryText}
</p>
)}
</div>
)}
</div>
);
}

View file

@ -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 (
<div className="h-3.5 w-3.5 rounded-full border border-gray-300" />
);
case "generating":
return <Loader2 className="h-3.5 w-3.5 animate-spin text-gray-500" />;
case "done":
return <CheckCircle className="h-3.5 w-3.5 text-green-500" />;
}
};
const getStatusText = () => {
switch (status) {
case "loading":
return "Preparing to update plan...";
case "generating":
return "Updating plan...";
case "done":
return "Plan updated";
}
};
return (
<div className="overflow-hidden rounded-md border border-gray-200">
{reasoningText && (
<div className="border-b border-blue-100 bg-blue-50 p-2">
<button
onClick={() => setShowReasoning(!showReasoning)}
className="flex items-center gap-1 text-xs font-normal text-blue-700 hover:text-blue-800"
>
<MessageSquare className="h-3 w-3" />
{showReasoning ? "Hide reasoning" : "Show reasoning"}
</button>
{showReasoning && (
<p className="mt-1 text-xs font-normal text-blue-800">
{reasoningText}
</p>
)}
</div>
)}
<div className="flex items-center bg-gray-50 p-2">
<RefreshCw className="mr-2 h-3.5 w-3.5 text-blue-500" />
<span className="flex-1 text-xs font-normal text-gray-800">
{getStatusText()}
</span>
{getStatusIcon()}
</div>
{summaryText && status === "done" && (
<div className="border-t border-green-100 bg-green-50 p-2">
<button
onClick={() => setShowSummary(!showSummary)}
className="flex items-center gap-1 text-xs font-normal text-green-700 hover:text-green-800"
>
<FileText className="h-3 w-3" />
{showSummary ? "Hide summary" : "Show summary"}
</button>
{showSummary && (
<p className="mt-1 text-xs font-normal text-green-800">
{summaryText}
</p>
)}
</div>
)}
</div>
);
}

View file

@ -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 (
<div className="h-3.5 w-3.5 rounded-full border border-gray-300" />
);
case "generating":
return <Loader2 className="h-3.5 w-3.5 animate-spin text-gray-500" />;
case "done":
return <CheckCircle className="h-3.5 w-3.5 text-green-500" />;
}
};
const getStatusText = () => {
switch (status) {
case "loading":
return "Preparing task summary...";
case "generating":
return "Generating summary...";
case "done":
return "Task completed";
}
};
return (
<div className="overflow-hidden rounded-md border border-gray-200">
{reasoningText && (
<div className="border-b border-blue-100 bg-blue-50 p-2">
<button
onClick={() => setShowReasoning(!showReasoning)}
className="flex items-center gap-1 text-xs font-normal text-blue-700 hover:text-blue-800"
>
<MessageSquare className="h-3 w-3" />
{showReasoning ? "Hide reasoning" : "Show reasoning"}
</button>
{showReasoning && (
<p className="mt-1 text-xs font-normal text-blue-800">
{reasoningText}
</p>
)}
</div>
)}
<div
className={`flex items-center bg-gray-50 p-2 ${status === "done" && summary ? "cursor-pointer" : ""}`}
onClick={
status === "done" && summary
? () => setExpanded(!expanded)
: undefined
}
>
{getStatusIcon()}
<span className="ml-2 flex-1 text-xs font-normal text-gray-800">
{getStatusText()}
</span>
{status === "done" && summary && (
<button className="text-gray-500 hover:text-gray-700">
{expanded ? (
<ChevronUp className="h-3.5 w-3.5" />
) : (
<ChevronDown className="h-3.5 w-3.5" />
)}
</button>
)}
</div>
{expanded && summary && status === "done" && (
<div className="border-t border-gray-200 p-2">
<h3 className="mb-1 text-xs font-normal text-gray-500">
Task Summary
</h3>
<p className="text-xs font-normal text-gray-800">{summary}</p>
</div>
)}
{summaryText && status === "done" && (
<div className="border-t border-green-100 bg-green-50 p-2">
<button
onClick={() => setShowSummary(!showSummary)}
className="flex items-center gap-1 text-xs font-normal text-green-700 hover:text-green-800"
>
<FileText className="h-3 w-3" />
{showSummary ? "Hide summary" : "Show summary"}
</button>
{showSummary && (
<p className="mt-1 text-xs font-normal text-green-800">
{summaryText}
</p>
)}
</div>
)}
</div>
);
}