Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dfd357ee91 | |||
| 596d0bb243 |
@@ -2047,12 +2047,14 @@ const ChatScreen: React.FC = () => {
|
||||
// Chat-Nachricht mit allen Anhaengen. clientMsgId nur wenn Text dabei
|
||||
// ist — files selber haben (noch) kein ACK-Tracking auf der Bridge.
|
||||
const cmid = messageText ? nextClientMsgId() : undefined;
|
||||
const activePid = focusedProjectIdRef.current;
|
||||
const userMsg: ChatMessage = {
|
||||
id: msgId,
|
||||
sender: 'user',
|
||||
text: messageText || `${pendingAttachments.length} Anhang/Anhaenge`,
|
||||
timestamp: Date.now(),
|
||||
attachments,
|
||||
projectId: activePid,
|
||||
...(cmid && {
|
||||
clientMsgId: cmid,
|
||||
deliveryStatus: connectionStateRef.current === 'connected' ? 'sending' : 'queued',
|
||||
@@ -2086,6 +2088,7 @@ const ChatScreen: React.FC = () => {
|
||||
type: mimeType,
|
||||
size: file.size,
|
||||
base64,
|
||||
projectId: activePid,
|
||||
...(isPhoto && file.width && { width: file.width, height: file.height }),
|
||||
...(location && { location }),
|
||||
});
|
||||
@@ -2099,6 +2102,7 @@ const ChatScreen: React.FC = () => {
|
||||
text: messageText,
|
||||
voice: localXttsVoiceRef.current,
|
||||
speed: ttsSpeedRef.current,
|
||||
projectId: activePid,
|
||||
...(location && { location }),
|
||||
});
|
||||
}
|
||||
|
||||
+36
-12
@@ -658,6 +658,10 @@ class ARIABridge:
|
||||
# Liste von Tuples: (file_path, name, file_type, size_kb, width, height)
|
||||
self._pending_files: list[tuple[str, str, str, int, int, int]] = []
|
||||
self._pending_files_flush_task: Optional[asyncio.Task] = None
|
||||
# Projekt-Kontext der gerade gepufferten Anhaenge (aus dem file-Upload).
|
||||
# Wird beim Flush an send_to_core gegeben, damit Anhaenge im richtigen
|
||||
# Projekt landen statt im Hauptchat.
|
||||
self._pending_files_project_id: str = ""
|
||||
self._PENDING_FILES_WINDOW_SEC: float = 0.8
|
||||
|
||||
def initialize(self) -> None:
|
||||
@@ -1508,12 +1512,19 @@ class ARIABridge:
|
||||
text = self._build_pending_files_message("")
|
||||
self._pending_files = []
|
||||
self._pending_files_flush_task = None
|
||||
await self.send_to_core(text, source="app-file")
|
||||
pid = self._pending_files_project_id
|
||||
self._pending_files_project_id = ""
|
||||
await self.send_to_core(text, source="app-file", project_id=pid)
|
||||
|
||||
async def _flush_pending_files_with_text(self, user_text: str) -> bool:
|
||||
async def _flush_pending_files_with_text(self, user_text: str,
|
||||
project_id: str = "") -> bool:
|
||||
"""Wenn ein chat-Text reinkommt waehrend Files gepuffert sind:
|
||||
Files + Text zu einer einzigen aria-core-Nachricht mergen.
|
||||
Returns True wenn gemerged wurde (Caller soll dann nicht nochmal senden)."""
|
||||
Returns True wenn gemerged wurde (Caller soll dann nicht nochmal senden).
|
||||
|
||||
project_id: Projekt-Kontext aus dem chat-Payload (der sichtbare Focus
|
||||
beim Absenden). Faellt auf den beim File-Upload gemerkten Kontext
|
||||
zurueck, damit Anhaenge im richtigen Projekt landen statt im Hauptchat."""
|
||||
if not self._pending_files:
|
||||
return False
|
||||
if self._pending_files_flush_task and not self._pending_files_flush_task.done():
|
||||
@@ -1521,9 +1532,11 @@ class ARIABridge:
|
||||
self._pending_files_flush_task = None
|
||||
text = self._build_pending_files_message(user_text)
|
||||
self._pending_files = []
|
||||
pid = (project_id or "").strip() or self._pending_files_project_id
|
||||
self._pending_files_project_id = ""
|
||||
# create_task statt await — sonst blockt der RVS-recv-Loop bis Brain
|
||||
# fertig ist (siehe chat-handler oben).
|
||||
asyncio.create_task(self.send_to_core(text, source="app-file+chat"))
|
||||
asyncio.create_task(self.send_to_core(text, source="app-file+chat", project_id=pid))
|
||||
return True
|
||||
|
||||
async def send_to_core(self, text: str, source: str = "bridge",
|
||||
@@ -1965,9 +1978,11 @@ class ARIABridge:
|
||||
# Wenn Files gerade gepuffert sind (Bild + Text gleichzeitig
|
||||
# gesendet), mergen wir sie zu einer einzigen Anfrage statt
|
||||
# zwei separater send_to_core-Calls.
|
||||
merged = await self._flush_pending_files_with_text(text)
|
||||
merged = await self._flush_pending_files_with_text(
|
||||
text, project_id=str(payload.get("projectId") or ""))
|
||||
if merged:
|
||||
logger.info("[rvs] App-Chat (mit Anhaengen): '%s'", text[:80])
|
||||
logger.info("[rvs] App-Chat (mit Anhaengen) project=%s: '%s'",
|
||||
str(payload.get("projectId") or "") or "(main)", text[:80])
|
||||
else:
|
||||
core_text = self._build_core_text(text, interrupted, location)
|
||||
logger.info("[rvs] App-Chat%s%s: '%s'",
|
||||
@@ -2189,8 +2204,14 @@ class ARIABridge:
|
||||
file_b64 = payload.get("base64", "")
|
||||
width = payload.get("width", 0)
|
||||
height = payload.get("height", 0)
|
||||
logger.info("[rvs] Datei empfangen: %s (%s, %dKB)",
|
||||
file_name, file_type, len(file_b64) // 1365 if file_b64 else 0)
|
||||
# Projekt-Kontext des Uploads (sichtbarer App-Focus). Merken, damit
|
||||
# der spaetere Flush (Files+Text oder Files-only) die Anfrage im
|
||||
# richtigen Projekt an das Brain schickt statt im Hauptchat.
|
||||
file_project_id = str(payload.get("projectId") or "")
|
||||
self._pending_files_project_id = file_project_id
|
||||
logger.info("[rvs] Datei empfangen: %s (%s, %dKB) project=%s",
|
||||
file_name, file_type, len(file_b64) // 1365 if file_b64 else 0,
|
||||
file_project_id or "(main)")
|
||||
|
||||
SHARED_DIR = "/shared/uploads"
|
||||
os.makedirs(SHARED_DIR, exist_ok=True)
|
||||
@@ -2198,7 +2219,8 @@ class ARIABridge:
|
||||
if not file_b64:
|
||||
text = f"Stefan hat eine Datei gesendet ({file_name}, {file_type}) aber die Daten sind leer angekommen."
|
||||
# create_task statt await — RVS-recv darf nicht blocken
|
||||
asyncio.create_task(self.send_to_core(text, source="app-file"))
|
||||
asyncio.create_task(self.send_to_core(text, source="app-file",
|
||||
project_id=file_project_id))
|
||||
return
|
||||
|
||||
if file_type.startswith("image/"):
|
||||
@@ -2212,10 +2234,12 @@ class ARIABridge:
|
||||
f.write(base64.b64decode(file_b64))
|
||||
size_kb = len(file_b64) // 1365
|
||||
logger.info("[rvs] Datei gespeichert: %s (%dKB)", file_path, size_kb)
|
||||
# Datei dem aktuellen Projekt zuordnen (falls Stefan in einem ist).
|
||||
# Datei dem Projekt des Uploads zuordnen (Multi-Threading: explizit
|
||||
# aus dem file-Payload, kein globaler active_project-State mehr).
|
||||
# Manifest in /shared/config/file_projects.json — File-Manager
|
||||
# in App + Diagnostic filtert danach.
|
||||
self._tag_file_to_active_project(file_path)
|
||||
# in App + Diagnostic filtert danach. Leer = Hauptchat.
|
||||
if file_project_id:
|
||||
self._tag_file_to_project(file_path, file_project_id)
|
||||
|
||||
# Pixel-Bilder fuer Claude-Vision shrinken wenn > 2 MB. SVG/PDF/ZIP
|
||||
# bleiben unangetastet (Vision laeuft eh nur auf Raster-Formaten).
|
||||
|
||||
@@ -4328,6 +4328,7 @@
|
||||
|
||||
// ── Datei-Manager ──────────────────────────────────────
|
||||
let filesCache = [];
|
||||
let diagFileProjectsList = []; // [{id, name}] fuer das Zuordnungs-Dropdown pro Datei
|
||||
const filesSelected = new Set(); // Set of paths
|
||||
|
||||
async function loadFiles() {
|
||||
@@ -4345,6 +4346,8 @@
|
||||
const pr = await fetch('/api/brain/projects/list?include_archived=true');
|
||||
const pdata = await pr.json();
|
||||
const projects = pdata?.projects || [];
|
||||
// Fuer das Pro-Datei-Zuordnungs-Dropdown merken.
|
||||
diagFileProjectsList = projects.map(p => ({ id: p.id, name: p.name }));
|
||||
const sel = document.getElementById('files-filter-project');
|
||||
if (sel) {
|
||||
const current = sel.value;
|
||||
@@ -4439,12 +4442,25 @@
|
||||
: '<span style="background:#34C75922;color:#34C759;padding:1px 6px;border-radius:3px;font-size:10px;margin-right:6px;">User</span>';
|
||||
const checked = filesSelected.has(f.path) ? 'checked' : '';
|
||||
const pathEsc = escapeHtml(f.path);
|
||||
const curPid = f.projectId || '';
|
||||
// Pro-Datei Projekt-Zuordnung: Hauptchat ('') + alle Projekte. Auch
|
||||
// eine unbekannte (geloeschtes Projekt) ID als Option behalten, damit
|
||||
// der aktuelle Wert nicht still verlorengeht.
|
||||
const projOpts = [{ id: '', name: '💬 Hauptchat' }, ...diagFileProjectsList];
|
||||
if (curPid && !projOpts.some(p => p.id === curPid)) {
|
||||
projOpts.push({ id: curPid, name: `📁 ${curPid} (gelöscht?)` });
|
||||
}
|
||||
const projSelect = `<select onchange="assignFileProject('${pathEsc}', this.value)" title="Projekt-Zuordnung"
|
||||
style="background:#080810;color:${curPid ? '#34C759' : '#8888AA'};border:1px solid #1E1E2E;padding:2px 4px;border-radius:4px;font-family:inherit;font-size:10px;max-width:130px;flex-shrink:0;">
|
||||
${projOpts.map(p => `<option value="${escapeHtml(p.id)}" ${p.id === curPid ? 'selected' : ''}>${escapeHtml(p.name)}</option>`).join('')}
|
||||
</select>`;
|
||||
return `<div style="padding:8px 0;border-bottom:1px solid #1E1E2E;display:flex;gap:8px;align-items:center;">
|
||||
<input type="checkbox" ${checked} onchange="toggleFileSelect('${pathEsc}')" style="cursor:pointer;flex-shrink:0;">
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="color:#E0E0F0;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${badge}<strong>${escapeHtml(f.name)}</strong></div>
|
||||
<div style="color:#555570;font-size:10px;">${fmtSize(f.size)} · ${fmtDate(f.mtime)}</div>
|
||||
</div>
|
||||
${projSelect}
|
||||
<button class="btn secondary" onclick="openFileInline('${encodeURIComponent(f.path)}')" style="padding:2px 8px;font-size:10px;" title="Öffnen">👁</button>
|
||||
<button class="btn secondary" onclick="downloadFile('${encodeURIComponent(f.path)}')" style="padding:2px 8px;font-size:10px;" title="Herunterladen">⬇</button>
|
||||
<button class="btn secondary" onclick="showVersions('${escapeHtml(f.name)}')" style="padding:2px 8px;font-size:10px;" title="Versionen">🕒</button>
|
||||
@@ -4453,6 +4469,27 @@
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Datei einem Projekt zuordnen (oder leer = Hauptchat). Schreibt ins
|
||||
// Manifest via /api/files-set-project, aktualisiert den lokalen Cache und
|
||||
// rendert neu (respektiert den aktiven Projekt-Filter).
|
||||
async function assignFileProject(path, projectId) {
|
||||
try {
|
||||
const r = await fetch('/api/files-set-project', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path, projectId: projectId || '' }),
|
||||
});
|
||||
const d = await r.json();
|
||||
if (!d.ok) throw new Error(d.error || 'Fehler');
|
||||
const f = filesCache.find(x => x.path === path);
|
||||
if (f) f.projectId = projectId || '';
|
||||
renderFilesList();
|
||||
} catch (e) {
|
||||
alert('Zuordnung fehlgeschlagen: ' + e.message);
|
||||
renderFilesList(); // Dropdown auf alten Wert zuruecksetzen
|
||||
}
|
||||
}
|
||||
|
||||
// ── Versions-Modal ──────────────────────────────────────
|
||||
async function showVersions(fileName) {
|
||||
// path-relative-to-/shared/uploads ist hier == fileName, weil unser
|
||||
|
||||
Reference in New Issue
Block a user