mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 12:43:16 +00:00
221 lines
5.1 KiB
TypeScript
221 lines
5.1 KiB
TypeScript
import {
|
|
XIcon,
|
|
SendHorizontal,
|
|
RefreshCcw,
|
|
Pencil,
|
|
Copy,
|
|
CopyCheck,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
} from "lucide-react";
|
|
import { TooltipIconButton } from "../tooltip-icon-button";
|
|
import { AnimatePresence, motion } from "framer-motion";
|
|
import { useState } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
function ContentCopyable({
|
|
content,
|
|
disabled,
|
|
}: {
|
|
content: string;
|
|
disabled: boolean;
|
|
}) {
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
const handleCopy = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
|
e.stopPropagation();
|
|
navigator.clipboard.writeText(content);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
};
|
|
|
|
return (
|
|
<TooltipIconButton
|
|
onClick={(e) => handleCopy(e)}
|
|
variant="ghost"
|
|
tooltip="Copy content"
|
|
disabled={disabled}
|
|
>
|
|
<AnimatePresence
|
|
mode="wait"
|
|
initial={false}
|
|
>
|
|
{copied ? (
|
|
<motion.div
|
|
key="check"
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
exit={{ opacity: 0, scale: 0.8 }}
|
|
transition={{ duration: 0.15 }}
|
|
>
|
|
<CopyCheck className="text-green-500" />
|
|
</motion.div>
|
|
) : (
|
|
<motion.div
|
|
key="copy"
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
exit={{ opacity: 0, scale: 0.8 }}
|
|
transition={{ duration: 0.15 }}
|
|
>
|
|
<Copy />
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</TooltipIconButton>
|
|
);
|
|
}
|
|
|
|
export function BranchSwitcher({
|
|
branch,
|
|
branchOptions,
|
|
onSelect,
|
|
isLoading,
|
|
}: {
|
|
branch: string | undefined;
|
|
branchOptions: string[] | undefined;
|
|
onSelect: (branch: string) => void;
|
|
isLoading: boolean;
|
|
}) {
|
|
if (!branchOptions || !branch) return null;
|
|
const index = branchOptions.indexOf(branch);
|
|
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="size-6 p-1"
|
|
onClick={() => {
|
|
const prevBranch = branchOptions[index - 1];
|
|
if (!prevBranch) return;
|
|
onSelect(prevBranch);
|
|
}}
|
|
disabled={isLoading}
|
|
>
|
|
<ChevronLeft />
|
|
</Button>
|
|
<span className="text-sm">
|
|
{index + 1} / {branchOptions.length}
|
|
</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="size-6 p-1"
|
|
onClick={() => {
|
|
const nextBranch = branchOptions[index + 1];
|
|
if (!nextBranch) return;
|
|
onSelect(nextBranch);
|
|
}}
|
|
disabled={isLoading}
|
|
>
|
|
<ChevronRight />
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function CommandBar({
|
|
content,
|
|
isHumanMessage,
|
|
isAiMessage,
|
|
isEditing,
|
|
setIsEditing,
|
|
handleSubmitEdit,
|
|
handleRegenerate,
|
|
isLoading,
|
|
}: {
|
|
content: string;
|
|
isHumanMessage?: boolean;
|
|
isAiMessage?: boolean;
|
|
isEditing?: boolean;
|
|
setIsEditing?: React.Dispatch<React.SetStateAction<boolean>>;
|
|
handleSubmitEdit?: () => void;
|
|
handleRegenerate?: () => void;
|
|
isLoading: boolean;
|
|
}) {
|
|
if (isHumanMessage && isAiMessage) {
|
|
throw new Error(
|
|
"Can only set one of isHumanMessage or isAiMessage to true, not both.",
|
|
);
|
|
}
|
|
|
|
if (!isHumanMessage && !isAiMessage) {
|
|
throw new Error(
|
|
"One of isHumanMessage or isAiMessage must be set to true.",
|
|
);
|
|
}
|
|
|
|
if (
|
|
isHumanMessage &&
|
|
(isEditing === undefined ||
|
|
setIsEditing === undefined ||
|
|
handleSubmitEdit === undefined)
|
|
) {
|
|
throw new Error(
|
|
"If isHumanMessage is true, all of isEditing, setIsEditing, and handleSubmitEdit must be set.",
|
|
);
|
|
}
|
|
|
|
const showEdit =
|
|
isHumanMessage &&
|
|
isEditing !== undefined &&
|
|
!!setIsEditing &&
|
|
!!handleSubmitEdit;
|
|
|
|
if (isHumanMessage && isEditing && !!setIsEditing && !!handleSubmitEdit) {
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<TooltipIconButton
|
|
disabled={isLoading}
|
|
tooltip="Cancel edit"
|
|
variant="ghost"
|
|
onClick={() => {
|
|
setIsEditing(false);
|
|
}}
|
|
>
|
|
<XIcon />
|
|
</TooltipIconButton>
|
|
<TooltipIconButton
|
|
disabled={isLoading}
|
|
tooltip="Submit"
|
|
variant="secondary"
|
|
onClick={handleSubmitEdit}
|
|
>
|
|
<SendHorizontal />
|
|
</TooltipIconButton>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<ContentCopyable
|
|
content={content}
|
|
disabled={isLoading}
|
|
/>
|
|
{isAiMessage && !!handleRegenerate && (
|
|
<TooltipIconButton
|
|
disabled={isLoading}
|
|
tooltip="Refresh"
|
|
variant="ghost"
|
|
onClick={handleRegenerate}
|
|
>
|
|
<RefreshCcw />
|
|
</TooltipIconButton>
|
|
)}
|
|
{showEdit && (
|
|
<TooltipIconButton
|
|
disabled={isLoading}
|
|
tooltip="Edit"
|
|
variant="ghost"
|
|
onClick={() => {
|
|
setIsEditing?.(true);
|
|
}}
|
|
>
|
|
<Pencil />
|
|
</TooltipIconButton>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|