12ed7123ab
* fix: support markdown preview comments * fix: address markdown preview review feedback * fix: address markdown preview comment scope * fix: address markdown preview follow-up feedback * fix: preserve markdown preview run action in file tabs * fix: ignore svg clicks in markdown preview controls * fix: ignore svg markdown preview selection targets
183 lines
6.0 KiB
TypeScript
183 lines
6.0 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useCallback, useRef, useEffect } from "react";
|
|
import { IconPlus, IconGripHorizontal, IconPlayerPlay } from "@tabler/icons-react";
|
|
import { Button } from "@kandev/ui/button";
|
|
import { Textarea } from "@kandev/ui/textarea";
|
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@kandev/ui/tooltip";
|
|
import { cn } from "@/lib/utils";
|
|
import { useDraggablePopover, usePopoverDismiss } from "@/components/task/use-draggable-popover";
|
|
|
|
type EditorCommentPopoverProps = {
|
|
selectedText: string;
|
|
lineRange: { start: number; end: number };
|
|
position: { x: number; y: number };
|
|
onSubmit: (comment: string) => void;
|
|
onSubmitAndRun?: (comment: string) => void;
|
|
onClose: () => void;
|
|
};
|
|
|
|
function getPopoverWidth() {
|
|
if (typeof window === "undefined") return 384;
|
|
return Math.min(384, Math.max(280, window.innerWidth - 32));
|
|
}
|
|
|
|
function PopoverBody({
|
|
selectedText,
|
|
comment,
|
|
setComment,
|
|
handleSubmit,
|
|
handleSubmitAndRun,
|
|
textareaRef,
|
|
}: {
|
|
selectedText: string;
|
|
comment: string;
|
|
setComment: (v: string) => void;
|
|
handleSubmit: () => void;
|
|
handleSubmitAndRun?: () => void;
|
|
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
|
}) {
|
|
const previewText =
|
|
selectedText.length > 100 ? selectedText.slice(0, 100).trim() + "…" : selectedText;
|
|
const isDisabled = !comment.trim();
|
|
const isMac = typeof navigator !== "undefined" && navigator.platform?.includes("Mac");
|
|
const modKey = isMac ? "\u2318" : "Ctrl";
|
|
|
|
const handleKeyDown = useCallback(
|
|
(e: React.KeyboardEvent) => {
|
|
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
e.preventDefault();
|
|
if (e.shiftKey && handleSubmitAndRun) {
|
|
handleSubmitAndRun();
|
|
} else {
|
|
handleSubmit();
|
|
}
|
|
}
|
|
},
|
|
[handleSubmit, handleSubmitAndRun],
|
|
);
|
|
|
|
return (
|
|
<div className="px-4 pb-4">
|
|
<pre className="mb-3 p-2 rounded-md bg-muted/50 text-xs text-muted-foreground font-mono line-clamp-3 overflow-hidden whitespace-pre-wrap">
|
|
{previewText}
|
|
</pre>
|
|
<Textarea
|
|
ref={textareaRef}
|
|
value={comment}
|
|
onChange={(e) => setComment(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
placeholder="Add your comment or instruction..."
|
|
className="min-h-[72px] resize-none text-sm border-border/50 focus:border-primary/50"
|
|
/>
|
|
<div className="mt-3 flex items-center justify-between">
|
|
<span className="text-xs text-muted-foreground/70">
|
|
{modKey}+Enter to add{handleSubmitAndRun ? `, ${modKey}+Shift+Enter to run` : ""}
|
|
</span>
|
|
<TooltipProvider delayDuration={400}>
|
|
<div className="inline-flex">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
size="sm"
|
|
variant={handleSubmitAndRun ? "outline" : "default"}
|
|
onClick={handleSubmit}
|
|
disabled={isDisabled}
|
|
className={`gap-1.5 cursor-pointer ${handleSubmitAndRun ? "rounded-r-none border-r-0" : ""}`}
|
|
>
|
|
<IconPlus className="h-3.5 w-3.5" />
|
|
Add
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
<p>Save comment for review ({modKey}+Enter)</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
{handleSubmitAndRun && (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
size="sm"
|
|
onClick={handleSubmitAndRun}
|
|
disabled={isDisabled}
|
|
className="gap-1.5 rounded-l-none cursor-pointer"
|
|
>
|
|
<IconPlayerPlay className="h-3.5 w-3.5" />
|
|
Run
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
<p>Save and send to agent ({modKey}+Shift+Enter)</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
</TooltipProvider>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function EditorCommentPopover({
|
|
selectedText,
|
|
lineRange,
|
|
position,
|
|
onSubmit,
|
|
onSubmitAndRun,
|
|
onClose,
|
|
}: EditorCommentPopoverProps) {
|
|
const [comment, setComment] = useState("");
|
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
const popoverRef = useRef<HTMLDivElement>(null);
|
|
const { pos, onDragStart } = useDraggablePopover(position, getPopoverWidth(), 220);
|
|
usePopoverDismiss(onClose, popoverRef);
|
|
|
|
useEffect(() => {
|
|
textareaRef.current?.focus();
|
|
}, []);
|
|
|
|
const handleSubmit = useCallback(() => {
|
|
if (!comment.trim()) return;
|
|
onSubmit(comment.trim());
|
|
}, [comment, onSubmit]);
|
|
|
|
const handleSubmitAndRun = useCallback(() => {
|
|
if (!comment.trim() || !onSubmitAndRun) return;
|
|
onSubmitAndRun(comment.trim());
|
|
}, [comment, onSubmitAndRun]);
|
|
|
|
const lineRangeText =
|
|
lineRange.start === lineRange.end
|
|
? `Line ${lineRange.start}`
|
|
: `Lines ${lineRange.start}-${lineRange.end}`;
|
|
|
|
return (
|
|
<div
|
|
ref={popoverRef}
|
|
className={cn(
|
|
"fixed z-50 w-[min(24rem,calc(100vw-2rem))] rounded-xl border border-border/50 bg-popover/95 backdrop-blur-sm shadow-xl",
|
|
"animate-in fade-in-0 zoom-in-95 duration-150",
|
|
)}
|
|
style={{ left: pos.left, top: pos.top }}
|
|
>
|
|
<div
|
|
className="flex items-center justify-between px-4 pt-3 pb-1 cursor-grab active:cursor-grabbing select-none"
|
|
onMouseDown={onDragStart}
|
|
>
|
|
<span className="px-2 py-0.5 rounded-md bg-muted text-xs font-mono text-muted-foreground">
|
|
{lineRangeText}
|
|
</span>
|
|
<IconGripHorizontal className="h-3.5 w-3.5 text-muted-foreground/40" />
|
|
</div>
|
|
<PopoverBody
|
|
selectedText={selectedText}
|
|
comment={comment}
|
|
setComment={setComment}
|
|
handleSubmit={handleSubmit}
|
|
handleSubmitAndRun={onSubmitAndRun ? handleSubmitAndRun : undefined}
|
|
textareaRef={textareaRef}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|