import clsx from 'clsx'; import DOMPurify from 'dompurify'; import { Check, Download, FileText, Languages, MessageSquarePlus, Pin, PinOff, RefreshCw, Save, Sparkles, X, } from 'lucide-react'; import React, { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; import { Overlay } from '@/components/Overlay'; import { useEnv } from '@/context/EnvContext'; import { useIsMobileViewport } from '@/hooks/useIsMobileViewport'; import { useTranslation } from '@/hooks/useTranslation'; import { usePanelResize } from '@/hooks/usePanelResize'; import { useBookDataStore } from '@/store/bookDataStore'; import { useReaderStore } from '@/store/readerStore'; import { useReviewModeStore } from '@/store/reviewModeStore'; import { useThemeStore } from '@/store/themeStore'; import { getPanelTopInset } from '@/utils/insets'; import { openExternalUrl } from '@/utils/open'; import { useReviewPanelDrag } from '../hooks/useReviewPanelDrag'; import { useReviewPanelFloatingResize } from '../hooks/useReviewPanelFloatingResize'; import { exportReviewedEpub, generateReviewFeedback, loadReviewGlossary, retranslateReviewRow, saveReviewGlossary, saveReviewGptConfig, saveReviewRow, sidecarApi, switchReviewGlossaryPath, type ReviewEditPayload, type ReviewGlossaryEntry, type ReviewGlossaryPayload, type ReviewGptConfig, type ReviewSessionPayload, } from '@/services/reviewEditorService'; const MIN_REVIEW_PANEL_WIDTH = 0.24; const MAX_REVIEW_PANEL_WIDTH = 0.48; const MIN_FLOATING_PANEL_HEIGHT = 0.35; const MAX_FLOATING_PANEL_HEIGHT = 0.92; const FLOATING_PANEL_MARGIN = 12; const DEFAULT_REVIEW_PANEL_WIDTH = 0.32; const cnHtmlPurifyOptions = { ALLOWED_TAGS: [ 'ruby', 'rb', 'rt', 'rp', 'span', 'mark', 'br', 'em', 'strong', 'b', 'i', 'u', 's', 'sub', 'sup', ], ALLOWED_ATTR: ['class', 'title'], ALLOW_DATA_ATTR: false, }; type GptForm = { base_url: string; model: string; api_key: string; glossary_path: string; translation_prompt: string; format_prompt: string; character_prompt: string; }; const emptyEditState: ReviewEditPayload = { current_html: '', marked: false, issue_type: '', severity: '', tags: '', comment: '', learn_note: '', }; const emptyGptForm: GptForm = { base_url: '', model: '', api_key: '', glossary_path: '', translation_prompt: '', format_prompt: '', character_prompt: '', }; const sanitizeInlineHtml = (html: string) => DOMPurify.sanitize(html || '', cnHtmlPurifyOptions); const escapeInlineHtml = (value: string) => value.replace(/&/g, '&').replace(//g, '>'); const rowTitle = (row: { document_title?: string; file_label?: string; file: string }) => row.document_title || row.file_label || row.file; const toGptForm = (config: ReviewGptConfig | null | undefined): GptForm => ({ base_url: config?.base_url || '', model: config?.model || '', api_key: '', glossary_path: config?.glossary_path || '', translation_prompt: config?.translation_prompt || config?.prompt_defaults?.translation_prompt || '', format_prompt: config?.format_prompt || config?.prompt_defaults?.format_prompt || '', character_prompt: config?.character_prompt || config?.prompt_defaults?.character_prompt || '', }); const PanelButton = ({ children, icon, onClick, disabled, variant = 'ghost', }: { children: ReactNode; icon?: ReactNode; onClick: () => void; disabled?: boolean; variant?: 'primary' | 'ghost'; }) => ( ); function GptConfigPanel({ baseUrl, sessionId, form, setForm, onSaved, }: { baseUrl: string; sessionId: string | null | undefined; form: GptForm; setForm: React.Dispatch>; onSaved: (config: ReviewGptConfig) => void; }) { const [open, setOpen] = useState(false); const [saving, setSaving] = useState(false); const [message, setMessage] = useState(''); const save = async () => { setSaving(true); setMessage(''); try { const config = await saveReviewGptConfig(baseUrl, sessionId, { base_url: form.base_url || undefined, model: form.model || undefined, api_key: form.api_key, glossary_path: form.glossary_path, translation_prompt: form.translation_prompt, format_prompt: form.format_prompt, character_prompt: form.character_prompt, }); setForm((current) => ({ ...current, api_key: '' })); onSaved(config); setMessage('已保存 API、提示词与术语表路径。'); } catch (error) { setMessage(error instanceof Error ? error.message : String(error)); } finally { setSaving(false); } }; return (
{open ? (