mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 13:53:15 +00:00
add tool genui components to web/src/components/gen-ui (#159)
This commit is contained in:
parent
0bfbf99db9
commit
eadf05ed76
7 changed files with 946 additions and 0 deletions
281
apps/web/src/components/gen-ui/action-step.tsx
Normal file
281
apps/web/src/components/gen-ui/action-step.tsx
Normal 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");
|
||||
}
|
||||
122
apps/web/src/components/gen-ui/diagnose-error.tsx
Normal file
122
apps/web/src/components/gen-ui/diagnose-error.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
15
apps/web/src/components/gen-ui/index.tsx
Normal file
15
apps/web/src/components/gen-ui/index.tsx
Normal 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,
|
||||
};
|
||||
145
apps/web/src/components/gen-ui/initialize-step.tsx
Normal file
145
apps/web/src/components/gen-ui/initialize-step.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
163
apps/web/src/components/gen-ui/push-changes.tsx
Normal file
163
apps/web/src/components/gen-ui/push-changes.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
96
apps/web/src/components/gen-ui/replanning-step.tsx
Normal file
96
apps/web/src/components/gen-ui/replanning-step.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
124
apps/web/src/components/gen-ui/task-summary.tsx
Normal file
124
apps/web/src/components/gen-ui/task-summary.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue