feat: ARIA kann Dateien an User zurueckgeben (PDFs, Bilder, Office-Docs, ...)
ARIA setzt im Antworttext einen Marker `[FILE: /shared/uploads/aria_xxx.ext]`. Bridge extrahiert ihn (Marker wird aus dem TTS-Text entfernt) und sendet ein neues file_from_aria-Event ueber RVS an App + Diagnostic. Diagnostic: - Eigene Bubble mit Datei-Icon + Klick-Handler - PDF/Bild → neuer Browser-Tab via /shared/* HTTP-Route - Andere → Download via download-Attribut App: - Neues FileOpenerModule (Kotlin) — Intent.ACTION_VIEW mit FileProvider, Android-Picker waehlt App nach MIME-Type - file_paths.xml erweitert (cache + files + external) - file_response liefert jetzt mimeType mit - Klick auf ARIA-Anhang: lokal vorhanden → direkt oeffnen, sonst file_request mit autoOpen-Flag → bei Empfang persistAttachment + open Stefan muss noch im aria-core/OpenClaw System-Prompt einen Hinweis einbauen: "Wenn du dem User eine Datei erstellt hast (Pfad in /shared/uploads/), haenge am Ende deiner Antwort einmalig [FILE: /shared/uploads/aria_<name>.<ext>] an. Der Marker wird aus dem sichtbaren Text entfernt und als Anhang in App und Diagnostic angezeigt." Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ import com.facebook.react.uimanager.ViewManager
|
|||||||
|
|
||||||
class ApkInstallerPackage : ReactPackage {
|
class ApkInstallerPackage : ReactPackage {
|
||||||
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
||||||
return listOf(ApkInstallerModule(reactContext))
|
return listOf(ApkInstallerModule(reactContext), FileOpenerModule(reactContext))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.ariacockpit
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import com.facebook.react.bridge.Promise
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext
|
||||||
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||||
|
import com.facebook.react.bridge.ReactMethod
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Oeffnet eine beliebige Datei (PDF, Bild, Office-Doc, ...) mit der vom User
|
||||||
|
* gewaehlten App via Android-Intent-Picker. Nutzt FileProvider damit auch
|
||||||
|
* Android 7+ (content:// statt file://) das URI lesen darf.
|
||||||
|
*
|
||||||
|
* MIME-Type wird vom Caller bestimmt — App-Auswahl ist davon abhaengig (PDF
|
||||||
|
* → PDF-Viewer, image/* → Galerie, etc.).
|
||||||
|
*/
|
||||||
|
class FileOpenerModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
||||||
|
override fun getName() = "FileOpener"
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun open(filePath: String, mimeType: String, promise: Promise) {
|
||||||
|
try {
|
||||||
|
val cleanPath = filePath.removePrefix("file://")
|
||||||
|
val file = File(cleanPath)
|
||||||
|
if (!file.exists()) {
|
||||||
|
promise.reject("FILE_NOT_FOUND", "Datei nicht gefunden: $cleanPath")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val context = reactApplicationContext
|
||||||
|
val uri: Uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
|
||||||
|
} else {
|
||||||
|
Uri.fromFile(file)
|
||||||
|
}
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndType(uri, mimeType.ifBlank { "*/*" })
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
// Chooser zeigt Android-Auswahl falls mehrere Apps das MIME oeffnen koennen.
|
||||||
|
val chooser = Intent.createChooser(intent, "Oeffnen mit").apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
context.startActivity(chooser)
|
||||||
|
promise.resolve(true)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
promise.reject("OPEN_ERROR", e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<paths>
|
<paths>
|
||||||
<cache-path name="cache" path="." />
|
<cache-path name="cache" path="." />
|
||||||
|
<files-path name="files" path="." />
|
||||||
|
<external-path name="external" path="." />
|
||||||
|
<external-files-path name="external_files" path="." />
|
||||||
|
<external-cache-path name="external_cache" path="." />
|
||||||
</paths>
|
</paths>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
ToastAndroid,
|
ToastAndroid,
|
||||||
AppState,
|
AppState,
|
||||||
|
NativeModules,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import RNFS from 'react-native-fs';
|
import RNFS from 'react-native-fs';
|
||||||
@@ -80,6 +81,23 @@ const capMessages = (msgs: ChatMessage[]): ChatMessage[] =>
|
|||||||
const DEFAULT_ATTACHMENT_DIR = `${RNFS.DocumentDirectoryPath}/chat_attachments`;
|
const DEFAULT_ATTACHMENT_DIR = `${RNFS.DocumentDirectoryPath}/chat_attachments`;
|
||||||
const STORAGE_PATH_KEY = 'aria_attachment_storage_path';
|
const STORAGE_PATH_KEY = 'aria_attachment_storage_path';
|
||||||
|
|
||||||
|
const { FileOpener } = NativeModules as {
|
||||||
|
FileOpener?: { open: (filePath: string, mimeType: string) => Promise<boolean> };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Datei mit Android-Intent-Picker oeffnen (System waehlt App nach MIME). */
|
||||||
|
async function openFileWithIntent(filePath: string, mimeType: string): Promise<void> {
|
||||||
|
if (!FileOpener) {
|
||||||
|
ToastAndroid.show('FileOpener Native Module fehlt', ToastAndroid.SHORT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await FileOpener.open(filePath, mimeType || 'application/octet-stream');
|
||||||
|
} catch (err: any) {
|
||||||
|
ToastAndroid.show(`Oeffnen fehlgeschlagen: ${err?.message || err}`, ToastAndroid.LONG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Image-Vorschau in der Chat-Bubble. Misst die echte Bild-Dimension via
|
/** Image-Vorschau in der Chat-Bubble. Misst die echte Bild-Dimension via
|
||||||
* Image.getSize + setzt aspectRatio dynamisch — dadurch passt sich die
|
* Image.getSize + setzt aspectRatio dynamisch — dadurch passt sich die
|
||||||
* Bubble ans Bild an (kein "Strich" mehr bei breiten oder hohen Bildern). */
|
* Bubble ans Bild an (kein "Strich" mehr bei breiten oder hohen Bildern). */
|
||||||
@@ -179,6 +197,10 @@ const ChatScreen: React.FC = () => {
|
|||||||
|
|
||||||
const flatListRef = useRef<FlatList>(null);
|
const flatListRef = useRef<FlatList>(null);
|
||||||
const messageIdCounter = useRef(0);
|
const messageIdCounter = useRef(0);
|
||||||
|
// ServerPaths fuer die der User auf "oeffnen" geklickt hat — beim
|
||||||
|
// file_response wird die Datei nach dem Speichern direkt mit dem System-
|
||||||
|
// Intent geoeffnet (PDF-Viewer, Galerie, etc.).
|
||||||
|
const autoOpenPaths = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
// Eindeutige Message-ID generieren
|
// Eindeutige Message-ID generieren
|
||||||
const nextId = (): string => {
|
const nextId = (): string => {
|
||||||
@@ -349,11 +371,32 @@ const ChatScreen: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// file_from_aria: ARIA hat eine Datei rausgegeben → als ARIA-Bubble anzeigen
|
||||||
|
if (message.type === 'file_from_aria') {
|
||||||
|
const p = message.payload || {};
|
||||||
|
const ariaMsg: ChatMessage = {
|
||||||
|
id: nextId(),
|
||||||
|
sender: 'aria',
|
||||||
|
text: '',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
attachments: [{
|
||||||
|
type: (typeof p.mimeType === 'string' && p.mimeType.startsWith('image/')) ? 'image' : 'file',
|
||||||
|
name: (p.name as string) || 'datei',
|
||||||
|
size: (p.size as number) || 0,
|
||||||
|
mimeType: (p.mimeType as string) || '',
|
||||||
|
serverPath: (p.serverPath as string) || '',
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
setMessages(prev => capMessages([...prev, ariaMsg]));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// file_response: Re-Download von Server — lokal speichern
|
// file_response: Re-Download von Server — lokal speichern
|
||||||
if (message.type === 'file_response') {
|
if (message.type === 'file_response') {
|
||||||
const reqId = (message.payload.requestId as string) || '';
|
const reqId = (message.payload.requestId as string) || '';
|
||||||
const b64 = (message.payload.base64 as string) || '';
|
const b64 = (message.payload.base64 as string) || '';
|
||||||
const serverPath = (message.payload.serverPath as string) || '';
|
const serverPath = (message.payload.serverPath as string) || '';
|
||||||
|
const mimeType = (message.payload.mimeType as string) || '';
|
||||||
if (b64 && reqId) {
|
if (b64 && reqId) {
|
||||||
const fileName = (message.payload.name as string) || 'download';
|
const fileName = (message.payload.name as string) || 'download';
|
||||||
persistAttachment(b64, reqId, fileName).then(filePath => {
|
persistAttachment(b64, reqId, fileName).then(filePath => {
|
||||||
@@ -363,6 +406,11 @@ const ChatScreen: React.FC = () => {
|
|||||||
a.serverPath === serverPath ? { ...a, uri: filePath } : a
|
a.serverPath === serverPath ? { ...a, uri: filePath } : a
|
||||||
),
|
),
|
||||||
})));
|
})));
|
||||||
|
// Wenn der User dieses File explizit oeffnen wollte → Intent-Picker
|
||||||
|
if (serverPath && autoOpenPaths.current.has(serverPath)) {
|
||||||
|
autoOpenPaths.current.delete(serverPath);
|
||||||
|
openFileWithIntent(filePath.replace(/^file:\/\//, ''), mimeType);
|
||||||
|
}
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -1008,7 +1056,22 @@ const ChatScreen: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.attachmentFile}>
|
<TouchableOpacity
|
||||||
|
style={styles.attachmentFile}
|
||||||
|
onPress={() => {
|
||||||
|
// Lokal vorhanden \u2192 direkt mit System-Intent oeffnen
|
||||||
|
if (att.uri) {
|
||||||
|
openFileWithIntent(att.uri.replace(/^file:\/\//, ''), att.mimeType || '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Sonst: file_request \u2192 bei file_response wird die Datei
|
||||||
|
// gespeichert UND geoeffnet (autoOpenPaths-Tracking).
|
||||||
|
if (att.serverPath) {
|
||||||
|
autoOpenPaths.current.add(att.serverPath);
|
||||||
|
rvs.send('file_request' as any, { serverPath: att.serverPath, requestId: item.id });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Text style={styles.attachmentFileIcon}>
|
<Text style={styles.attachmentFileIcon}>
|
||||||
{att.mimeType?.includes('pdf') ? '\uD83D\uDCC4' :
|
{att.mimeType?.includes('pdf') ? '\uD83D\uDCC4' :
|
||||||
att.mimeType?.includes('word') || att.mimeType?.includes('document') ? '\uD83D\uDCC3' :
|
att.mimeType?.includes('word') || att.mimeType?.includes('document') ? '\uD83D\uDCC3' :
|
||||||
@@ -1018,12 +1081,10 @@ const ChatScreen: React.FC = () => {
|
|||||||
<Text style={styles.attachmentFileName} numberOfLines={1}>{att.name}</Text>
|
<Text style={styles.attachmentFileName} numberOfLines={1}>{att.name}</Text>
|
||||||
{att.size ? <Text style={styles.attachmentFileSize}>{Math.round(att.size / 1024)}KB</Text> : null}
|
{att.size ? <Text style={styles.attachmentFileSize}>{Math.round(att.size / 1024)}KB</Text> : null}
|
||||||
{!att.uri && att.serverPath && (
|
{!att.uri && att.serverPath && (
|
||||||
<TouchableOpacity onPress={() => rvs.send('file_request' as any, { serverPath: att.serverPath, requestId: item.id })}>
|
<Text style={[styles.attachmentFileSize, {color: '#0096FF'}]}>(tippen zum oeffnen)</Text>
|
||||||
<Text style={[styles.attachmentFileSize, {color: '#0096FF'}]}>(laden)</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
)}
|
||||||
{!att.uri && !att.serverPath && <Text style={styles.attachmentFileSize}>(nicht verfuegbar)</Text>}
|
{!att.uri && !att.serverPath && <Text style={styles.attachmentFileSize}>(nicht verfuegbar)</Text>}
|
||||||
</View>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ import asyncio
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import signal
|
import signal
|
||||||
import ssl
|
import ssl
|
||||||
import sys
|
import sys
|
||||||
@@ -882,6 +884,48 @@ class ARIABridge:
|
|||||||
pass
|
pass
|
||||||
return payload.get("text", "")
|
return payload.get("text", "")
|
||||||
|
|
||||||
|
# File-Marker-Pattern: `[FILE: /pfad/zur/datei.ext]` (Pfad kann Spaces
|
||||||
|
# enthalten, Endung beliebig). Mehrfach im Text moeglich.
|
||||||
|
_FILE_MARKER_RE = re.compile(r"\[FILE:\s*(/shared/uploads/[^\]]+?)\s*\]", re.IGNORECASE)
|
||||||
|
|
||||||
|
def _extract_file_markers(self, text: str) -> tuple[str, list[dict]]:
|
||||||
|
"""Sucht [FILE: /shared/uploads/...]-Marker, gibt (cleaned_text, file_list) zurueck."""
|
||||||
|
files: list[dict] = []
|
||||||
|
for m in self._FILE_MARKER_RE.finditer(text):
|
||||||
|
path = m.group(1).strip()
|
||||||
|
if not path.startswith("/shared/uploads/"):
|
||||||
|
logger.warning("[core] FILE-Marker mit unerlaubtem Pfad ignoriert: %s", path)
|
||||||
|
continue
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
logger.warning("[core] FILE-Marker zeigt auf nicht existente Datei: %s", path)
|
||||||
|
continue
|
||||||
|
name = os.path.basename(path)
|
||||||
|
mime, _ = mimetypes.guess_type(path)
|
||||||
|
size = os.path.getsize(path)
|
||||||
|
files.append({
|
||||||
|
"serverPath": path,
|
||||||
|
"name": name,
|
||||||
|
"mimeType": mime or "application/octet-stream",
|
||||||
|
"size": size,
|
||||||
|
})
|
||||||
|
cleaned = self._FILE_MARKER_RE.sub("", text).strip()
|
||||||
|
# Zwei aufeinanderfolgende Leerzeilen → eine
|
||||||
|
cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
|
||||||
|
return cleaned, files
|
||||||
|
|
||||||
|
async def _broadcast_aria_file(self, file_info: dict) -> None:
|
||||||
|
"""ARIA hat eine Datei fuer den User erstellt — App+Diagnostic informieren."""
|
||||||
|
logger.info("[rvs] ARIA-Datei rausgeben: %s (%s, %dKB)",
|
||||||
|
file_info["name"], file_info["mimeType"], file_info["size"] // 1024)
|
||||||
|
try:
|
||||||
|
await self._send_to_rvs({
|
||||||
|
"type": "file_from_aria",
|
||||||
|
"payload": file_info,
|
||||||
|
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("[rvs] file_from_aria broadcast fehlgeschlagen: %s", e)
|
||||||
|
|
||||||
async def _process_core_response(self, text: str, payload: dict) -> None:
|
async def _process_core_response(self, text: str, payload: dict) -> None:
|
||||||
"""Verarbeitet eine fertige Antwort von aria-core.
|
"""Verarbeitet eine fertige Antwort von aria-core.
|
||||||
|
|
||||||
@@ -896,6 +940,14 @@ class ARIABridge:
|
|||||||
logger.info("[core] NO_REPLY empfangen — Antwort still verworfen")
|
logger.info("[core] NO_REPLY empfangen — Antwort still verworfen")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# File-Marker `[FILE: /shared/uploads/aria_xyz.pdf]` extrahieren —
|
||||||
|
# ARIA legt damit Dateien fuer den User bereit (Bilder, PDFs, etc.).
|
||||||
|
# Der Marker wird aus dem Antworttext entfernt (TTS soll ihn nicht
|
||||||
|
# vorlesen) und parallel als file_from_aria-Event geschickt.
|
||||||
|
text, aria_files = self._extract_file_markers(text)
|
||||||
|
for f in aria_files:
|
||||||
|
await self._broadcast_aria_file(f)
|
||||||
|
|
||||||
metadata = payload.get("metadata", {})
|
metadata = payload.get("metadata", {})
|
||||||
is_critical = metadata.get("critical", False)
|
is_critical = metadata.get("critical", False)
|
||||||
requested_voice = metadata.get("voice")
|
requested_voice = metadata.get("voice")
|
||||||
@@ -1545,6 +1597,7 @@ class ARIABridge:
|
|||||||
return
|
return
|
||||||
with open(server_path, "rb") as f:
|
with open(server_path, "rb") as f:
|
||||||
file_b64 = base64.b64encode(f.read()).decode("ascii")
|
file_b64 = base64.b64encode(f.read()).decode("ascii")
|
||||||
|
mime, _ = mimetypes.guess_type(server_path)
|
||||||
logger.info("[rvs] Re-Download: %s (%dKB)", server_path, len(file_b64) // 1365)
|
logger.info("[rvs] Re-Download: %s (%dKB)", server_path, len(file_b64) // 1365)
|
||||||
await self._send_to_rvs({
|
await self._send_to_rvs({
|
||||||
"type": "file_response",
|
"type": "file_response",
|
||||||
@@ -1553,6 +1606,7 @@ class ARIABridge:
|
|||||||
"serverPath": server_path,
|
"serverPath": server_path,
|
||||||
"base64": file_b64,
|
"base64": file_b64,
|
||||||
"name": os.path.basename(server_path),
|
"name": os.path.basename(server_path),
|
||||||
|
"mimeType": mime or "application/octet-stream",
|
||||||
},
|
},
|
||||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -996,6 +996,11 @@
|
|||||||
addChat('received', msg.text, 'chat:final');
|
addChat('received', msg.text, 'chat:final');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (msg.type === 'file_from_aria') {
|
||||||
|
const p = msg.payload || {};
|
||||||
|
addAriaFile(p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (msg.type === 'chat_delta') { return; }
|
if (msg.type === 'chat_delta') { return; }
|
||||||
if (msg.type === 'chat_error') {
|
if (msg.type === 'chat_error') {
|
||||||
addChat('error', msg.error, 'chat:error');
|
addChat('error', msg.error, 'chat:error');
|
||||||
@@ -1475,6 +1480,40 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** ARIA hat eine Datei rausgegeben — als eigene Bubble mit Klick-Handler. */
|
||||||
|
function addAriaFile(p) {
|
||||||
|
const name = p.name || 'datei';
|
||||||
|
const serverPath = p.serverPath || '';
|
||||||
|
const mimeType = p.mimeType || '';
|
||||||
|
const sizeKB = p.size ? Math.round(p.size / 1024) : 0;
|
||||||
|
const isImage = mimeType.startsWith('image/');
|
||||||
|
const isPdf = mimeType === 'application/pdf';
|
||||||
|
const url = serverPath; // Diagnostic-Server liefert /shared/* aus
|
||||||
|
const sizeStr = sizeKB > 1024 ? `${(sizeKB/1024).toFixed(1)}MB` : `${sizeKB}KB`;
|
||||||
|
const icon = isImage ? '🖼️' : isPdf ? '📄' : '📎';
|
||||||
|
// PDFs/Bilder: target=_blank → neuer Tab. Andere: download-Attribut.
|
||||||
|
const linkAttrs = (isImage || isPdf)
|
||||||
|
? `href="${url}" target="_blank" rel="noopener"`
|
||||||
|
: `href="${url}" download="${escapeHtml(name)}"`;
|
||||||
|
let preview = '';
|
||||||
|
if (isImage) {
|
||||||
|
preview = `<img src="${url}" class="chat-media" onclick="openLightbox('image','${url}')" onerror="this.style.display='none'" style="margin-top:6px;">`;
|
||||||
|
}
|
||||||
|
const html = `<div style="font-weight:bold;">${icon} ARIA hat eine Datei erstellt</div>` +
|
||||||
|
`<a ${linkAttrs} style="color:#0096FF;text-decoration:underline;">${escapeHtml(name)}</a>` +
|
||||||
|
` <span style="color:#888;font-size:11px;">(${escapeHtml(mimeType)}, ${sizeStr})</span>` +
|
||||||
|
preview +
|
||||||
|
`<div class="meta">ARIA-Datei — ${new Date().toLocaleTimeString('de-DE')}</div>`;
|
||||||
|
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
|
||||||
|
if (!box) continue;
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'chat-msg received';
|
||||||
|
el.innerHTML = html;
|
||||||
|
box.appendChild(el);
|
||||||
|
box.scrollTop = box.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let chatFullscreen = false;
|
let chatFullscreen = false;
|
||||||
function toggleChatFullscreen() {
|
function toggleChatFullscreen() {
|
||||||
const modal = document.getElementById('chat-fullscreen');
|
const modal = document.getElementById('chat-fullscreen');
|
||||||
|
|||||||
@@ -620,6 +620,11 @@ function connectRVS(forcePlain) {
|
|||||||
type: "chat",
|
type: "chat",
|
||||||
payload: { text: `Anhang: ${name}\n${serverPath}`, sender: "user" }
|
payload: { text: `Anhang: ${name}\n${serverPath}`, sender: "user" }
|
||||||
}});
|
}});
|
||||||
|
} else if (msg.type === "file_from_aria" && msg.payload) {
|
||||||
|
// ARIA hat eine Datei fuer den User erstellt — im Chat als Anhang anzeigen
|
||||||
|
const p = msg.payload;
|
||||||
|
log("info", "rvs", `ARIA-Datei: ${p.name} (${p.mimeType}, ${(p.size||0)/1024|0}KB)`);
|
||||||
|
broadcast({ type: "file_from_aria", payload: p });
|
||||||
} else if (msg.type === "heartbeat") {
|
} else if (msg.type === "heartbeat") {
|
||||||
// ignorieren
|
// ignorieren
|
||||||
} else if (msg.type === "mode") {
|
} else if (msg.type === "mode") {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const ALLOWED_TYPES = new Set([
|
|||||||
"update_check", "update_available", "update_download", "update_data",
|
"update_check", "update_available", "update_download", "update_data",
|
||||||
"agent_activity", "cancel_request",
|
"agent_activity", "cancel_request",
|
||||||
"audio_pcm",
|
"audio_pcm",
|
||||||
|
"file_from_aria",
|
||||||
"xtts_delete_voice",
|
"xtts_delete_voice",
|
||||||
"voice_preload", "voice_ready",
|
"voice_preload", "voice_ready",
|
||||||
"stt_request", "stt_response",
|
"stt_request", "stt_response",
|
||||||
|
|||||||
Reference in New Issue
Block a user