diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index 6d58c84..948cda8 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -16,6 +16,7 @@ import { Platform, StyleSheet, Image, + ScrollView, Modal, } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -94,7 +95,7 @@ const ChatScreen: React.FC = () => { const [fullscreenImage, setFullscreenImage] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [searchVisible, setSearchVisible] = useState(false); - const [pendingAttachment, setPendingAttachment] = useState<{file: any, isPhoto: boolean} | null>(null); + const [pendingAttachments, setPendingAttachments] = useState<{file: any, isPhoto: boolean}[]>([]); const flatListRef = useRef(null); const messageIdCounter = useRef(0); @@ -402,9 +403,9 @@ const ChatScreen: React.FC = () => { const sendTextMessage = useCallback(async () => { const text = inputText.trim(); - // Wenn pending Anhang vorhanden → Anhang + Text zusammen senden - if (pendingAttachment) { - sendPendingAttachment(text); + // Wenn pending Anhaenge vorhanden → Anhaenge + Text zusammen senden + if (pendingAttachments.length > 0) { + sendPendingAttachments(text); return; } @@ -427,7 +428,7 @@ const ChatScreen: React.FC = () => { text, ...(location && { location }), }); - }, [inputText, getCurrentLocation, pendingAttachment, sendPendingAttachment]); + }, [inputText, getCurrentLocation, pendingAttachments, sendPendingAttachments]); // Sprachaufnahme abgeschlossen const handleVoiceRecording = useCallback(async (result: RecordingResult) => { @@ -449,69 +450,81 @@ const ChatScreen: React.FC = () => { }); }, [getCurrentLocation]); - // Datei auswaehlen → als pending speichern (nicht sofort senden) + // Datei auswaehlen → zur Pending-Liste hinzufuegen const handleFileSelected = useCallback(async (file: FileData) => { setShowFileUpload(false); - setPendingAttachment({ file, isPhoto: false }); - setInputText(''); // Focus auf Textfeld + setPendingAttachments(prev => [...prev, { file, isPhoto: false }]); }, []); - // Foto auswaehlen → als pending speichern (nicht sofort senden) + // Foto auswaehlen → zur Pending-Liste hinzufuegen const handlePhotoSelected = useCallback(async (photo: PhotoData) => { setShowCameraUpload(false); - setPendingAttachment({ file: photo, isPhoto: true }); - setInputText(''); // Focus auf Textfeld + setPendingAttachments(prev => [...prev, { file: photo, isPhoto: true }]); }, []); - // Pending Anhang + Text/Sprache senden - const sendPendingAttachment = useCallback(async (messageText: string) => { - if (!pendingAttachment) return; - const { file, isPhoto } = pendingAttachment; + // Alle Pending Anhaenge + Text senden + const sendPendingAttachments = useCallback(async (messageText: string) => { + if (pendingAttachments.length === 0) return; const location = await getCurrentLocation(); const msgId = nextId(); - // Chat-Nachricht erstellen - const isImage = isPhoto || (file.type && file.type.startsWith('image/')); - const name = isPhoto ? file.fileName : file.name; - const base64 = file.base64 || ''; - const mimeType = file.type || ''; - const imageUri = isImage && base64 ? `data:${mimeType};base64,${base64}` : file.uri; + // Alle Attachments fuer die Chat-Nachricht sammeln + const attachments: Attachment[] = []; + for (const { file, isPhoto } of pendingAttachments) { + const isImage = isPhoto || (file.type && file.type.startsWith('image/')); + const name = isPhoto ? file.fileName : file.name; + const base64 = file.base64 || ''; + const mimeType = file.type || ''; + const imageUri = isImage && base64 ? `data:${mimeType};base64,${base64}` : file.uri; - const userMsg: ChatMessage = { - id: msgId, - sender: 'user', - text: messageText || 'Anhang', - timestamp: Date.now(), - attachments: [{ + attachments.push({ type: isImage ? 'image' : 'file', name, size: file.size, uri: imageUri, mimeType, - }], + }); + } + + // Chat-Nachricht mit allen Anhaengen + const userMsg: ChatMessage = { + id: msgId, + sender: 'user', + text: messageText || `${pendingAttachments.length} Anhang/Anhaenge`, + timestamp: Date.now(), + attachments, }; setMessages(prev => [...prev, userMsg]); - // Auf Disk speichern - if (base64) { - persistAttachment(base64, msgId, name).then(filePath => { - setMessages(prev => prev.map(m => - m.id === msgId ? { ...m, attachments: m.attachments?.map(a => ({ ...a, uri: filePath })) } : m - )); - }).catch(() => {}); + // Alle Dateien an RVS senden + auf Disk speichern + for (const { file, isPhoto } of pendingAttachments) { + const name = isPhoto ? file.fileName : file.name; + const base64 = file.base64 || ''; + const mimeType = file.type || ''; + + // Auf Disk speichern + if (base64) { + persistAttachment(base64, msgId + '_' + name, name).then(filePath => { + setMessages(prev => prev.map(m => + m.id === msgId ? { ...m, attachments: m.attachments?.map(a => + a.name === name && !a.uri?.startsWith('file://') ? { ...a, uri: filePath } : a + )} : m + )); + }).catch(() => {}); + } + + // An RVS senden + rvs.send('file', { + name, + type: mimeType, + size: file.size, + base64, + ...(isPhoto && file.width && { width: file.width, height: file.height }), + ...(location && { location }), + }); } - // Datei an RVS senden - rvs.send('file', { - name, - type: mimeType, - size: file.size, - base64, - ...(isPhoto && file.width && { width: file.width, height: file.height }), - ...(location && { location }), - }); - - // Wenn Text dabei ist, als separate Chat-Nachricht senden (damit ARIA weiss was zu tun ist) + // Text als separate Nachricht (damit ARIA weiss was zu tun ist) if (messageText) { rvs.send('chat', { text: messageText, @@ -519,9 +532,9 @@ const ChatScreen: React.FC = () => { }); } - setPendingAttachment(null); + setPendingAttachments([]); setInputText(''); - }, [pendingAttachment, getCurrentLocation]); + }, [pendingAttachments, getCurrentLocation]); }, [getCurrentLocation]); // --- Rendering --- @@ -670,24 +683,36 @@ const ChatScreen: React.FC = () => { } /> - {/* Pending Anhang Vorschau */} - {pendingAttachment && ( + {/* Pending Anhaenge Vorschau */} + {pendingAttachments.length > 0 && ( - {pendingAttachment.file.type?.startsWith('image/') || pendingAttachment.isPhoto ? ( - - ) : ( - {'\uD83D\uDCC4'} - )} - - {pendingAttachment.isPhoto ? pendingAttachment.file.fileName : pendingAttachment.file.name} - - setPendingAttachment(null)}> - X + + {pendingAttachments.map((att, idx) => ( + + {att.file.type?.startsWith('image/') || att.isPhoto ? ( + + ) : ( + + {'\uD83D\uDCC4'} + + )} + setPendingAttachments(prev => prev.filter((_, i) => i !== idx))} + > + X + + + ))} + + {pendingAttachments.length} + setPendingAttachments([])}> + Alle X )} @@ -714,7 +739,7 @@ const ChatScreen: React.FC = () => { style={styles.textInput} value={inputText} onChangeText={setInputText} - placeholder={pendingAttachment ? "Text zum Anhang (optional)..." : "Nachricht an ARIA..."} + placeholder={pendingAttachments.length > 0 ? "Text zu den Anhaengen (optional)..." : "Nachricht an ARIA..."} placeholderTextColor="#555570" multiline maxLength={4000} @@ -723,7 +748,7 @@ const ChatScreen: React.FC = () => { /> {/* Senden oder Sprache */} - {inputText.trim() || pendingAttachment ? ( + {inputText.trim() || pendingAttachments.length > 0 ? ( {'\u2B06\uFE0F'} @@ -963,17 +988,26 @@ const styles = StyleSheet.create({ borderTopWidth: 1, borderTopColor: '#2A2A3E', }, - pendingThumb: { - width: 40, - height: 40, - borderRadius: 6, + pendingItem: { + position: 'relative', marginRight: 8, + }, + pendingThumb: { + width: 50, + height: 50, + borderRadius: 6, backgroundColor: '#0D0D1A', }, - pendingName: { - flex: 1, - color: '#E0E0F0', - fontSize: 13, + pendingRemove: { + position: 'absolute', + top: -4, + right: -4, + width: 18, + height: 18, + borderRadius: 9, + backgroundColor: '#FF3B30', + justifyContent: 'center', + alignItems: 'center', }, searchBar: { flexDirection: 'row', diff --git a/diagnostic/index.html b/diagnostic/index.html index dc15eb9..555446b 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -205,8 +205,14 @@ 💭 ARIA denkt... +
- + +
@@ -939,21 +945,39 @@ } } + function sendDiagAttachments() { + // Alle pending Dateien an RVS senden + for (const f of diagPendingFiles) { + send({ action: 'send_file', name: f.name, type: f.type, size: f.size, base64: f.base64 }); + } + if (diagPendingFiles.length > 0) { + addChat('sent', `${diagPendingFiles.length} Anhang/Anhaenge`, 'Datei'); + } + diagPendingFiles = []; + renderDiagPending(); + } + function testGateway() { const input = document.getElementById('chat-input'); const text = input.value.trim(); - if (!text) return; - addChat('sent', text, 'Gateway direkt'); - send({ action: 'test_gateway', text }); + if (!text && diagPendingFiles.length === 0) return; + if (diagPendingFiles.length > 0) sendDiagAttachments(); + if (text) { + addChat('sent', text, 'Gateway direkt'); + send({ action: 'test_gateway', text }); + } input.value = ''; } function testRVS() { const input = document.getElementById('chat-input'); const text = input.value.trim(); - if (!text) return; - addChat('sent', text, 'via RVS'); - send({ action: 'test_rvs', text }); + if (!text && diagPendingFiles.length === 0) return; + if (diagPendingFiles.length > 0) sendDiagAttachments(); + if (text) { + addChat('sent', text, 'via RVS'); + send({ action: 'test_rvs', text }); + } input.value = ''; } @@ -1302,6 +1326,56 @@ } } + // ── Diagnostic Anhang-Handling ───────────── + let diagPendingFiles = []; + + function handleDiagFileSelect(files) { + for (const file of files) { + const reader = new FileReader(); + reader.onload = () => { + const base64 = reader.result.split(',')[1]; + diagPendingFiles.push({ name: file.name, type: file.type, size: file.size, base64 }); + renderDiagPending(); + }; + reader.readAsDataURL(file); + } + } + + function handleDiagPaste(event) { + const items = event.clipboardData?.items; + if (!items) return; + for (const item of items) { + if (item.kind === 'file') { + event.preventDefault(); + const file = item.getAsFile(); + if (file) handleDiagFileSelect([file]); + } + } + } + + function renderDiagPending() { + const container = document.getElementById('diag-pending-attachments'); + if (diagPendingFiles.length === 0) { + container.style.display = 'none'; + return; + } + container.style.display = 'flex'; + container.innerHTML = diagPendingFiles.map((f, i) => { + const isImage = f.type.startsWith('image/'); + const preview = isImage ? `` : `📄`; + return `
+ ${preview} + X +
`; + }).join('') + `${diagPendingFiles.length} Datei(en) + Alle X`; + } + + function removeDiagPending(idx) { + diagPendingFiles.splice(idx, 1); + renderDiagPending(); + } + // ── Abbrechen ────────────────────────────── function cancelRequest() { send({ action: 'cancel_request' }); diff --git a/diagnostic/server.js b/diagnostic/server.js index 1592266..5bab2e6 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -1181,6 +1181,14 @@ wss.on("connection", (ws) => { if (ws._sshSock) ws._sshSock.write(msg.data); } else if (msg.action === "live_ssh_close") { if (ws._sshSock) { ws._sshSock.end(); ws._sshSock = null; } + } else if (msg.action === "send_file") { + // Datei von Diagnostic an Bridge via RVS senden + sendToRVS_raw({ + type: "file", + payload: { name: msg.name, type: msg.type, size: msg.size, base64: msg.base64 }, + timestamp: Date.now(), + }); + log("info", "server", `Datei gesendet: ${msg.name} (${msg.type})`); } else if (msg.action === "cancel_request") { // Laufende Anfrage abbrechen — doctor --fix beendet stuck runs log("warn", "server", "Anfrage abgebrochen — fuehre doctor --fix aus");