266 lines
9.5 KiB
TypeScript
266 lines
9.5 KiB
TypeScript
'use client';
|
|
|
|
import { memo } from 'react';
|
|
import {
|
|
IconDeviceFloppy,
|
|
IconX,
|
|
IconLoader2,
|
|
IconRobot,
|
|
IconUser,
|
|
IconCopy,
|
|
IconRefresh,
|
|
IconTrash,
|
|
IconSend,
|
|
} from '@tabler/icons-react';
|
|
import { GridSpinner } from '@/components/grid-spinner';
|
|
import { Button } from '@kandev/ui/button';
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@kandev/ui/tooltip';
|
|
import { cn, formatRelativeTime } from '@/lib/utils';
|
|
import type { TaskPlan } from '@/lib/types/http';
|
|
|
|
type TaskPlanToolbarProps = {
|
|
plan: TaskPlan | null;
|
|
hasUnsavedChanges: boolean;
|
|
hasDraftContent: boolean;
|
|
isSaving: boolean;
|
|
isAgentBusy: boolean;
|
|
isReanalyzing: boolean;
|
|
hasActiveSession: boolean;
|
|
// Comments props
|
|
commentCount?: number;
|
|
isSubmittingComments?: boolean;
|
|
onSubmitComments?: () => void;
|
|
onClearComments?: () => void;
|
|
// Existing handlers
|
|
onSave: () => void;
|
|
onDiscard: () => void;
|
|
onCopy: () => void;
|
|
onReanalyze: () => void;
|
|
onClear: () => void;
|
|
};
|
|
|
|
export const TaskPlanToolbar = memo(function TaskPlanToolbar({
|
|
plan,
|
|
hasUnsavedChanges,
|
|
hasDraftContent,
|
|
isSaving,
|
|
isAgentBusy,
|
|
isReanalyzing,
|
|
hasActiveSession,
|
|
commentCount = 0,
|
|
isSubmittingComments = false,
|
|
onSubmitComments,
|
|
onClearComments,
|
|
onSave,
|
|
onDiscard,
|
|
onCopy,
|
|
onReanalyze,
|
|
onClear,
|
|
}: TaskPlanToolbarProps) {
|
|
const hasComments = commentCount > 0;
|
|
return (
|
|
<div className="mt-2">
|
|
<div
|
|
className={cn(
|
|
'rounded-2xl border border-border bg-background shadow-sm overflow-hidden relative',
|
|
hasUnsavedChanges && 'border-amber-500/50 border-dashed'
|
|
)}
|
|
>
|
|
{/* Animated progress bar when agent is working */}
|
|
{isAgentBusy && (
|
|
<div className="absolute bottom-0 left-0 right-0 h-0.5 overflow-hidden">
|
|
<div className="h-full w-full bg-gradient-to-r from-transparent via-primary/60 to-transparent animate-shimmer" />
|
|
</div>
|
|
)}
|
|
<div className="flex items-center gap-1 px-2 py-2">
|
|
{/* Left: Status info */}
|
|
<div className="flex items-center gap-1 flex-1">
|
|
{plan && (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<div className="flex items-center gap-1.5 px-2 py-1 rounded-md text-xs text-muted-foreground hover:bg-muted/40 cursor-default">
|
|
{plan.created_by === 'agent' ? (
|
|
<IconRobot className="h-3.5 w-3.5" />
|
|
) : (
|
|
<IconUser className="h-3.5 w-3.5" />
|
|
)}
|
|
<span className="capitalize">{plan.created_by}</span>
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Created by {plan.created_by}</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
{isAgentBusy ? (
|
|
<span className="flex items-center gap-2 text-xs text-muted-foreground px-2">
|
|
<span>Agent is working</span>
|
|
<GridSpinner className="h-3 w-3" />
|
|
</span>
|
|
) : hasUnsavedChanges ? (
|
|
<span className="text-xs text-amber-500 px-2">
|
|
Unsaved changes <span className="text-amber-500/60">· ⌘S to save</span>
|
|
</span>
|
|
) : plan?.updated_at ? (
|
|
<span className="text-xs text-muted-foreground/60 px-2">
|
|
Saved {formatRelativeTime(plan.updated_at)}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
|
|
{/* Right: Actions */}
|
|
<div className="flex items-center gap-0.5 shrink-0">
|
|
{hasUnsavedChanges && (
|
|
<>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-7 gap-1.5 px-2 cursor-pointer hover:bg-muted/40 text-muted-foreground hover:text-destructive"
|
|
onClick={onDiscard}
|
|
disabled={isSaving}
|
|
>
|
|
<IconX className="h-4 w-4" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Discard changes</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-7 gap-1.5 px-2 cursor-pointer hover:bg-muted/40"
|
|
onClick={onSave}
|
|
disabled={isSaving}
|
|
>
|
|
{isSaving ? (
|
|
<IconLoader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<IconDeviceFloppy className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Save plan</TooltipContent>
|
|
</Tooltip>
|
|
</>
|
|
)}
|
|
|
|
{/* Separator before utility actions */}
|
|
{(hasUnsavedChanges || plan || hasDraftContent) && (
|
|
<div className="h-4 w-px bg-border mx-1" />
|
|
)}
|
|
|
|
{/* Copy to clipboard */}
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-7 gap-1.5 px-2 cursor-pointer hover:bg-muted/40 text-muted-foreground"
|
|
onClick={onCopy}
|
|
disabled={!hasDraftContent}
|
|
>
|
|
<IconCopy className="h-4 w-4" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Copy to clipboard</TooltipContent>
|
|
</Tooltip>
|
|
|
|
{/* Re-analyze with agent */}
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-7 gap-1.5 px-2 cursor-pointer hover:bg-muted/40 text-muted-foreground"
|
|
onClick={onReanalyze}
|
|
disabled={isAgentBusy || !hasActiveSession || isReanalyzing}
|
|
>
|
|
{isReanalyzing ? (
|
|
<IconLoader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<IconRefresh className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Ask agent to review plan</TooltipContent>
|
|
</Tooltip>
|
|
|
|
{/* Clear/Delete plan */}
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-7 gap-1.5 px-2 cursor-pointer hover:bg-muted/40 text-muted-foreground hover:text-destructive"
|
|
onClick={onClear}
|
|
disabled={!plan && !hasDraftContent}
|
|
>
|
|
<IconTrash className="h-4 w-4" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Clear plan</TooltipContent>
|
|
</Tooltip>
|
|
|
|
{/* Separator before comments */}
|
|
{hasComments && <div className="h-4 w-px bg-border mx-1" />}
|
|
|
|
{/* Comments section */}
|
|
{hasComments && (
|
|
<div className="flex items-center gap-1">
|
|
{/* Clear comments */}
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-7 w-7 p-0 cursor-pointer hover:bg-muted/40 text-muted-foreground hover:text-destructive"
|
|
onClick={onClearComments}
|
|
disabled={isSubmittingComments}
|
|
>
|
|
<IconX className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Clear all comments</TooltipContent>
|
|
</Tooltip>
|
|
|
|
{/* Submit comments button - round with count inside */}
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
size="icon"
|
|
className="h-7 w-7 rounded-full cursor-pointer bg-primary hover:bg-primary/90 text-primary-foreground relative"
|
|
onClick={onSubmitComments}
|
|
disabled={isSubmittingComments || isAgentBusy}
|
|
>
|
|
{isSubmittingComments ? (
|
|
<IconLoader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<>
|
|
<IconSend className="h-3.5 w-3.5" />
|
|
<span className="absolute -top-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-primary-foreground text-primary text-[10px] font-semibold">
|
|
{commentCount}
|
|
</span>
|
|
</>
|
|
)}
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Send {commentCount} comment{commentCount !== 1 ? 's' : ''} to agent</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|
|
|