/** * CodeEditorTile — Live-Code-Editor (WebView, editorHtml.ts). * * Zeigt die Dateien eines Code-Projekts aus /shared/projects//: * - beim Oeffnen werden die BEREITS vorhandenen Dateien vom Brain geladen * (listProjectFiles/readProjectFile) — sonst waere der Editor leer, obwohl * ARIA schon Dateien geschrieben hat. * - live schreibt ARIA weiter → code_file-Stream aktualisiert die offene Datei. * Stefan kann selbst editieren → code_file_edit zurueck an die Bridge. */ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { WebView, WebViewMessageEvent } from 'react-native-webview'; import codeFile from '../../services/codeFile'; import brainApi from '../../services/brainApi'; import { EDITOR_HTML } from '../assets/editorHtml'; interface Props { projectId: string; } function guessLang(path: string): string { const ext = (path.split('.').pop() || '').toLowerCase(); const map: Record = { js: 'javascript', ts: 'typescript', tsx: 'typescript', py: 'python', c: 'c', h: 'c', cpp: 'cpp', asm: 'asm', s: 'asm', sh: 'shell', bash: 'shell', html: 'html', css: 'css', json: 'json', yaml: 'yaml', yml: 'yaml', md: 'markdown', go: 'go', rs: 'rust', java: 'java', kt: 'kotlin', txt: 'text', }; return map[ext] || 'text'; } const CodeEditorTile: React.FC = ({ projectId }) => { const webRef = useRef(null); // Pfade aus dem Brain (vorhandene Dateien) — mit Live-Dateien gemergt. const [serverPaths, setServerPaths] = useState([]); const [currentPath, setCurrentPath] = useState(null); const [loadErr, setLoadErr] = useState(''); const readyRef = useRef(false); const currentPathRef = useRef(currentPath); currentPathRef.current = currentPath; // Vereinigte, sortierte Dateiliste (Live-Spiegel + Server-Dateien). const files = useMemo(() => { const set = new Set(serverPaths); for (const f of codeFile.getFiles(projectId)) set.add(f.path); return Array.from(set).sort((a, b) => a.localeCompare(b)); }, [serverPaths, projectId]); const sendToWeb = useCallback((payload: Record) => { const js = `window.ariaBridge && window.ariaBridge.onMessage(${JSON.stringify(JSON.stringify(payload))}); true;`; webRef.current?.injectJavaScript(js); }, []); const loadFileIntoEditor = useCallback(async (path: string | null) => { if (!path) { sendToWeb({ cmd: 'setContent', content: '', language: 'text', version: 0 }); return; } // Live-Version bevorzugen (falls ARIA gerade schreibt), sonst vom Brain holen. const live = codeFile.getFile(projectId, path); if (live) { sendToWeb({ cmd: 'setContent', content: live.content, language: live.language, version: live.version }); return; } try { const res = await brainApi.readProjectFile(projectId, path); sendToWeb({ cmd: 'setContent', content: res.content ?? '', language: guessLang(path), version: 0 }); } catch (e: any) { sendToWeb({ cmd: 'setContent', content: `// Konnte ${path} nicht laden: ${e?.message || e}`, language: 'text', version: 0 }); } }, [projectId, sendToWeb]); // Projektwechsel: vorhandene Dateien vom Brain laden. useEffect(() => { let cancelled = false; setLoadErr(''); brainApi.listProjectFiles(projectId) .then(res => { if (cancelled) return; const paths = (res.files || []).map(f => f.path); setServerPaths(paths); setCurrentPath(prev => (prev && paths.includes(prev)) ? prev : (paths[0] ?? codeFile.getFiles(projectId)[0]?.path ?? null)); }) .catch(e => { if (!cancelled) setLoadErr(String(e?.message || e)); }); return () => { cancelled = true; }; }, [projectId]); // Live-Updates aus dem Spiegel. useEffect(() => { return codeFile.subscribe((u) => { if ((u.projectId || '') !== (projectId || '')) return; setServerPaths(prev => prev.includes(u.path) ? prev : [...prev, u.path]); if (!currentPathRef.current) { setCurrentPath(u.path); return; } if (u.path !== currentPathRef.current || !readyRef.current) return; if (u.patch) { sendToWeb({ cmd: 'applyPatch', from: u.patch.from, to: u.patch.to, insert: u.patch.insert, version: u.version }); } else { sendToWeb({ cmd: 'setContent', content: u.content ?? '', language: u.language, version: u.version }); } }); }, [projectId, sendToWeb]); // Datei-Auswahl gewechselt → laden (falls WebView bereit). useEffect(() => { if (readyRef.current) loadFileIntoEditor(currentPath); }, [currentPath, loadFileIntoEditor]); const onMessage = useCallback((e: WebViewMessageEvent) => { let m: any; try { m = JSON.parse(e.nativeEvent.data); } catch { return; } if (m.event === 'ready') { readyRef.current = true; loadFileIntoEditor(currentPathRef.current); } else if (m.event === 'onEditFromUser') { const path = currentPathRef.current; if (!path) return; codeFile.sendEdit(projectId, path, { from: m.from, to: m.to, insert: m.insert }, m.fullText, m.version); } }, [projectId, loadFileIntoEditor]); return ( {files.length === 0 ? ( {loadErr ? `Fehler: ${loadErr}` : 'Noch keine Datei in diesem Projekt'} ) : ( {files.map((path) => { const active = path === currentPath; const name = path.split('/').pop() || path; return ( setCurrentPath(path)} style={[styles.tab, active && styles.tabActive]}> {name} ); })} )} ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#0D0D1A' }, tabsRow: { height: 40, backgroundColor: '#12122A', borderBottomColor: '#1E1E2E', borderBottomWidth: 1, justifyContent: 'center' }, tabs: { alignItems: 'center', paddingHorizontal: 6 }, noFiles: { color: '#9090B0', fontSize: 13, paddingHorizontal: 12 }, tab: { paddingHorizontal: 12, paddingVertical: 6, marginHorizontal: 3, borderRadius: 12, backgroundColor: '#0D0D1A', maxWidth: 180 }, tabActive: { backgroundColor: '#0096FF' }, tabText: { color: '#9090B0', fontSize: 12, fontWeight: '600' }, tabTextActive: { color: '#FFFFFF' }, web: { flex: 1, backgroundColor: '#0D0D1A' }, }); export default CodeEditorTile;