/** * WorkspaceEditorPage -- Diff editor for reviewing AI agent file edit proposals. * * Full-page layout with: * - Header: back-to-dashboard, accept-all / reject-all * - Tab bar: one tab per pending edit * - Center: Monaco DiffEditor (original vs. modified) * - Footer: status bar with counts and file metadata */ import React, { useMemo, useState, useEffect, useRef } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { DiffEditor } from '@monaco-editor/react'; import type { editor as monacoEditor } from 'monaco-editor'; import { useInstanceId } from '../../../hooks/useCurrentInstance'; import { useWorkspaceEditor, type EditorFileEdit } from './useWorkspaceEditor'; import { FaArrowLeft, FaCheck, FaTimes, FaCheckDouble, FaBan, FaSync } from 'react-icons/fa'; function _getMonacoLanguage(fileName: string): string { const ext = fileName.split('.').pop()?.toLowerCase() || ''; const langMap: Record = { js: 'javascript', jsx: 'javascript', ts: 'typescript', tsx: 'typescript', py: 'python', json: 'json', html: 'html', css: 'css', md: 'markdown', xml: 'xml', yaml: 'yaml', yml: 'yaml', sh: 'shell', sql: 'sql', txt: 'plaintext', csv: 'plaintext', log: 'plaintext', }; return langMap[ext] || 'plaintext'; } function _formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } export const WorkspaceEditorPage: React.FC = () => { const instanceId = useInstanceId() || ''; const navigate = useNavigate(); const { mandateId, featureCode, instanceId: routeInstanceId } = useParams<{ mandateId: string; featureCode: string; instanceId: string; }>(); const editor = useWorkspaceEditor(instanceId); const activeEdit = useMemo( () => editor.edits.find(e => e.id === editor.activeEditId) || null, [editor.edits, editor.activeEditId], ); const pendingEdits = useMemo( () => editor.edits.filter(e => e.status === 'pending'), [editor.edits], ); const _goBack = () => navigate(`/mandates/${mandateId}/${featureCode}/${routeInstanceId}/dashboard`); if (!instanceId) { return (
Keine Workspace-Instanz ausgewaehlt.
); } return (
{/* Header */}
File Edit Review {editor.pendingCount} pending
{/* Tab bar */} {pendingEdits.length > 0 && (
{pendingEdits.map(edit => ( <_EditorTab key={edit.id} edit={edit} isActive={edit.id === editor.activeEditId} onClick={() => editor.setActiveEditId(edit.id)} /> ))}
)} {/* Main content */}
{editor.isLoading ? (
Lade Aenderungsvorschlaege...
) : pendingEdits.length === 0 ? (
Keine offenen Aenderungsvorschlaege
) : activeEdit ? ( <_SafeDiffEditor key={activeEdit.id} original={activeEdit.oldContent} modified={activeEdit.newContent} language={_getMonacoLanguage(activeEdit.fileName)} /> ) : null}
{/* Footer / action bar for active edit */} {activeEdit && activeEdit.status === 'pending' && (
{activeEdit.fileName} Original: {_formatBytes(activeEdit.oldContent.length)} Geaendert: {_formatBytes(activeEdit.newContent.length)}
)}
); }; // --------------------------------------------------------------------------- // Safe DiffEditor wrapper -- prevents "TextModel got disposed" errors // by tracking the editor ref and skipping disposal when already torn down. // --------------------------------------------------------------------------- const _SafeDiffEditor: React.FC<{ original: string; modified: string; language: string; }> = ({ original, modified, language }) => { const editorRef = useRef(null); const [ready, setReady] = useState(false); useEffect(() => { setReady(true); return () => { if (editorRef.current) { try { editorRef.current.dispose(); } catch { /* already disposed */ } editorRef.current = null; } }; }, []); if (!ready) return null; return ( { editorRef.current = diffEditor; }} options={{ readOnly: true, renderSideBySide: true, minimap: { enabled: false }, fontSize: 13, lineNumbers: 'on', scrollBeyondLastLine: false, wordWrap: 'on', originalEditable: false, }} /> ); }; // --------------------------------------------------------------------------- // Sub-components // --------------------------------------------------------------------------- const _EditorTab: React.FC<{ edit: EditorFileEdit; isActive: boolean; onClick: () => void; }> = ({ edit, isActive, onClick }) => ( ); // --------------------------------------------------------------------------- // Shared styles // --------------------------------------------------------------------------- const _btnStyle: React.CSSProperties = { padding: '6px 8px', borderRadius: 4, border: '1px solid var(--border-color, #ddd)', background: 'transparent', cursor: 'pointer', display: 'flex', alignItems: 'center', }; const _actionBtnStyle: React.CSSProperties = { padding: '5px 14px', borderRadius: 4, border: 'none', cursor: 'pointer', fontSize: 12, fontWeight: 600, display: 'flex', alignItems: 'center', gap: 6, }; export default WorkspaceEditorPage;