diff --git a/frontend/src/app/components/editor/richEditorUtils.ts b/frontend/src/app/components/editor/richEditorUtils.ts index 9de93af01..543fd99bf 100644 --- a/frontend/src/app/components/editor/richEditorUtils.ts +++ b/frontend/src/app/components/editor/richEditorUtils.ts @@ -91,6 +91,11 @@ function formatPasteLabel(charCount: number): string { return `Pasted text (${charCount.toLocaleString()} chars)`; } +export function updatePasteCardLabel(card: HTMLElement, charCount: number): void { + const label = card.firstElementChild as HTMLElement | null; + if (label) label.textContent = formatPasteLabel(charCount); +} + export function createPasteCardElement( pasteId: string, charCount: number, @@ -122,7 +127,13 @@ export function createPasteCardElement( const label = document.createElement('span'); label.textContent = formatPasteLabel(charCount); - Object.assign(label.style, { maxWidth: '240px', overflow: 'hidden', textOverflow: 'ellipsis' }); + Object.assign(label.style, { + maxWidth: '240px', + overflow: 'hidden', + textOverflow: 'ellipsis', + opacity: '0.85', + transition: 'transform 0.15s ease, opacity 0.15s ease', + }); label.addEventListener('mousedown', (e) => { e.preventDefault(); e.stopPropagation(); onExpand(pasteId); }); const closeBtn = document.createElement('span'); @@ -145,6 +156,9 @@ export function createPasteCardElement( closeBtn.addEventListener('mouseout', () => { closeBtn.style.opacity = '0.6'; closeBtn.style.color = 'inherit'; }); closeBtn.addEventListener('mousedown', (e) => { e.preventDefault(); e.stopPropagation(); onRemove(pasteId); }); + card.addEventListener('mouseenter', () => { label.style.transform = 'translateX(2px)'; label.style.opacity = '1'; }); + card.addEventListener('mouseleave', () => { label.style.transform = 'translateX(0)'; label.style.opacity = '0.85'; }); + card.appendChild(label); card.appendChild(closeBtn); return card; diff --git a/frontend/src/app/pages/AgentChat/ChatInput.tsx b/frontend/src/app/pages/AgentChat/ChatInput.tsx index de9d18335..7c00daf17 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput.tsx +++ b/frontend/src/app/pages/AgentChat/ChatInput.tsx @@ -107,8 +107,6 @@ const ChatInput = forwardRef(({ onSend, disabled, mode, }; }, [prefillPrompt]); - useDraftLoad(editorRef, ownerId); - const [hasContent, setHasContent] = useState(() => !!loadDraft(ownerId)); const [attachedSkills, setAttachedSkills] = useState>({}); const [previewPasteId, setPreviewPasteId] = useState(null); @@ -292,6 +290,7 @@ const ChatInput = forwardRef(({ onSend, disabled, mode, isDragOver, handleInput, handleEditorClick, handlePickerSelect, handleKeyDown, handlePaste, handleDragOver, handleDragLeave, handleDrop, + removePasteCard, savePasteCard, } = useEditorHandlers({ editorRef, generalFileInputRef, ownerId, sessionId, autoRunMode, c, skills, elementSelection, setHasContent, setAttachedSkills, setForcedTools, onModeChange, @@ -299,6 +298,8 @@ const ChatInput = forwardRef(({ onSend, disabled, mode, onPasteExpand: setPreviewPasteId, }); + useDraftLoad(editorRef, ownerId, setPreviewPasteId, removePasteCard, c.font.mono, c.status.error); + const currentMode = modesMap[mode]; const FALLBACK_MODE = { ...FALLBACK_MODE_BASE, color: c.accent.primary }; const modeConf = currentMode @@ -310,7 +311,7 @@ const ChatInput = forwardRef(({ onSend, disabled, mode, return ( <> - setPreviewPasteId(null)} /> + setPreviewPasteId(null)} onSave={savePasteCard} /> (); @@ -26,7 +26,14 @@ export function deleteDraft(ownerId: string) { _draftStore.delete(ownerId); } -export function useDraftLoad(editorRef: RefObject, ownerId: string) { +export function useDraftLoad( + editorRef: RefObject, + ownerId: string, + onPasteExpand: (id: string) => void, + onPasteRemove: (id: string) => void, + monoFont: string, + errorColor: string, +) { useEffect(() => { const saved = _draftStore.get(ownerId); const editor = editorRef.current; @@ -41,10 +48,17 @@ export function useDraftLoad(editorRef: RefObject, ownerId: stri } if (!editor.textContent?.trim()) { editor.innerHTML = saved; + // innerHTML restore drops JS listeners, so stale paste cards are rebuilt fresh below instead of just kept. const staleCards = editor.querySelectorAll(`[${PASTE_CARD_ATTR}]`); staleCards.forEach((el) => { const pid = el.getAttribute(PASTE_CARD_ATTR); - if (!pid || !getPasteContent(pid)) el.remove(); + const content = pid ? getPasteContent(pid) : undefined; + if (!pid || content === undefined) { + el.remove(); + return; + } + const fresh = createPasteCardElement(pid, content.length, onPasteExpand, onPasteRemove, monoFont, errorColor); + el.replaceWith(fresh); }); const range = document.createRange(); range.selectNodeContents(editor); diff --git a/frontend/src/app/pages/AgentChat/ChatInput/hooks/useEditorHandlers.ts b/frontend/src/app/pages/AgentChat/ChatInput/hooks/useEditorHandlers.ts index 1df6af7e0..a743fecea 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput/hooks/useEditorHandlers.ts +++ b/frontend/src/app/pages/AgentChat/ChatInput/hooks/useEditorHandlers.ts @@ -11,6 +11,7 @@ import { createPasteId, setPasteContent, deletePasteContent, + updatePasteCardLabel, detectEditorTrigger, TriggerState, EMPTY_TRIGGER, @@ -112,6 +113,15 @@ export function useEditorHandlers(p: Params) { editor.focus(); }, [updateHasContent]); + const savePasteCard = useCallback((pasteId: string, text: string) => { + setPasteContent(pasteId, text); + const editor = editorRef.current; + if (!editor) return; + const card = editor.querySelector(`[${PASTE_CARD_ATTR}="${pasteId}"]`) as HTMLElement | null; + if (card) updatePasteCardLabel(card, text.length); + scheduleDraftSave(ownerId, () => readEditorHTML(editor)); + }, [ownerId]); + const removeSkillPill = useCallback((skillId: string) => { const editor = editorRef.current; if (!editor) return; @@ -349,5 +359,6 @@ export function useEditorHandlers(p: Params) { updateHasContent, handleInput, handleEditorClick, handlePickerSelect, handleKeyDown, handlePaste, handleDragOver, handleDragLeave, handleDrop, + removePasteCard, savePasteCard, }; } diff --git a/frontend/src/app/pages/AgentChat/ChatInput/view/PastePreviewDialog.tsx b/frontend/src/app/pages/AgentChat/ChatInput/view/PastePreviewDialog.tsx index f884ad8d8..d4ca875e7 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput/view/PastePreviewDialog.tsx +++ b/frontend/src/app/pages/AgentChat/ChatInput/view/PastePreviewDialog.tsx @@ -1,20 +1,31 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import Dialog from '@mui/material/Dialog'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; +import TextField from '@mui/material/TextField'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { getPasteContent } from '@/app/components/editor/richEditorUtils'; interface Props { pasteId: string | null; onClose: () => void; + onSave: (pasteId: string, text: string) => void; } -export const PastePreviewDialog: React.FC = ({ pasteId, onClose }) => { +export const PastePreviewDialog: React.FC = ({ pasteId, onClose, onSave }) => { const c = useClaudeTokens(); const open = !!pasteId; - const content = pasteId ? (getPasteContent(pasteId) ?? '') : ''; - const chars = content.length; + const original = pasteId ? getPasteContent(pasteId) : undefined; + const [draft, setDraft] = useState(''); + + useEffect(() => { + setDraft(original ?? ''); + }, [pasteId]); + + const handleChange = (text: string) => { + setDraft(text); + if (pasteId) onSave(pasteId, text); + }; return ( = ({ pasteId, onClose }) => { Pasted text - {chars.toLocaleString()} characters + {draft.length.toLocaleString()} characters - - {content || ( + + {original === undefined ? ( This pasted text is no longer available. Re-paste to restore it. + ) : ( + handleChange(e.target.value)} + multiline + fullWidth + minRows={6} + maxRows={20} + autoFocus + sx={{ + '& .MuiInputBase-root': { + fontFamily: c.font.mono, + fontSize: '0.78rem', + color: c.text.primary, + alignItems: 'flex-start', + }, + '& .MuiOutlinedInput-notchedOutline': { borderColor: c.border.subtle }, + }} + /> )}