import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { RefreshCw, X, ArrowRightLeft, ArrowDownToLine, Loader2, ChevronDown, Brain, SendHorizontal, Square, } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import type { RewriteAction, RewriteTone } from "../../api/rewrite"; import type { RewriteStatus } from "../../hooks/useWritingRewrite"; const ACTION_LABEL: Record = { polish: "润色", expand: "扩写", shorten: "缩写", tone: "改语气", imitate: "仿写", }; const TONE_LABEL: Record = { professional: "更专业", casual: "更口语化", readable: "更易读", academic: "更学术", expressive: "更有文采", }; interface MenuPos { x: number; y: number } interface WritingRewritePreviewProps { pos: MenuPos; status: RewriteStatus; originalText: string; result: string | null; thinking: string | null; error: string | null; action: RewriteAction; tone?: RewriteTone; onReplace: () => void; onInsert: () => void; onRetry: () => void; onAbort: () => void; onClose: () => void; onFollowUp: (instruction: string) => void; } export function WritingRewritePreview({ pos, status, result, thinking, error, action, tone, onReplace, onInsert, onRetry, onAbort, onClose, onFollowUp, }: WritingRewritePreviewProps) { const isLoading = status === "loading"; const isError = status === "error"; const isSuccess = status === "success" && result !== null; const hasResult = result !== null && result.length > 0; // ── Follow-up input ───────────────────────────────────────────────────── const [followUpInput, setFollowUpInput] = useState(""); const followUpInputRef = useRef(null); // Auto-focus the follow-up input when generation finishes useEffect(() => { if (status === "success") { setTimeout(() => followUpInputRef.current?.focus(), 50); } }, [status]); const handleFollowUpSubmit = () => { const instruction = followUpInput.trim(); if (!instruction || isLoading) return; setFollowUpInput(""); onFollowUp(instruction); }; // ── Thinking expand / collapse ────────────────────────────────────────── const [thinkingOpen, setThinkingOpen] = useState(false); const prevStatusRef = useRef(status); // Auto-expand when thinking content first streams in during loading useEffect(() => { if (isLoading && thinking) setThinkingOpen(true); }, [isLoading, thinking]); // Auto-collapse when generation finishes (loading → success/error) useEffect(() => { const prev = prevStatusRef.current; prevStatusRef.current = status; if (prev === "loading" && status !== "loading") { setThinkingOpen(false); } }, [status]); // ── Position ──────────────────────────────────────────────────────────── const panelWidth = 480; const [computedPos, setComputedPos] = useState<{ left: number; top?: number; bottom?: number; maxHeight: number; } | null>(null); useLayoutEffect(() => { const MARGIN = 8; const GAP = 6; const MIN_HEIGHT = 160; const safeX = Math.min(Math.max(pos.x, MARGIN), window.innerWidth - panelWidth - MARGIN); const spaceBelow = window.innerHeight - pos.y - MARGIN; const spaceAbove = pos.y - MARGIN; if (spaceBelow >= spaceAbove) { // More space below — anchor top at selection bottom, grow downward setComputedPos({ left: safeX, top: pos.y + GAP, maxHeight: Math.max(MIN_HEIGHT, spaceBelow - GAP), }); } else { // More space above — anchor bottom at selection top, grow upward setComputedPos({ left: safeX, bottom: window.innerHeight - pos.y + GAP, maxHeight: Math.max(MIN_HEIGHT, spaceAbove - GAP), }); } }, [pos.x, pos.y]); const actionLabel = tone ? `${ACTION_LABEL[action]} · ${TONE_LABEL[tone]}` : ACTION_LABEL[action]; return createPortal( <> {/* Backdrop */}
{/* Header */}
{actionLabel}
{isLoading && ( )}
{/* Body */}
{/* Thinking — collapsible with animation */} {thinking && (
{/* Toggle button */} {/* Animated content — grid-rows trick for smooth height transition */}
{thinking} {isLoading && ( )}
)} {/* Result area */} {/* Loading — no text yet */} {isLoading && !hasResult && (
AI 正在生成…
)} {/* Error */} {isError && (
{error ?? "改写失败,请重试"}
)} {/* Result text — show while streaming and after success */} {hasResult && (
{result} {isLoading && ( )}
)}
{/* Footer actions — only after success */} {isSuccess && (
)} {/* Follow-up chat input — shown after success */} {isSuccess && (
setFollowUpInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleFollowUpSubmit(); } }} placeholder="继续优化,如:更简洁一些、语气更正式…" className="flex-1 text-xs bg-muted/50 rounded-md px-2.5 py-1.5 outline-none border border-input focus:border-primary/50 transition-colors placeholder:text-muted-foreground/50" />
)}
, document.body, ); }