Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d430fa113e | |||
| 1fb512c2fd |
@@ -79,8 +79,8 @@ android {
|
|||||||
applicationId "com.ariacockpit"
|
applicationId "com.ariacockpit"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 10904
|
versionCode 10905
|
||||||
versionName "0.1.9.4"
|
versionName "0.1.9.5"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.1.9.4",
|
"version": "0.1.9.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* SettingsScreen.tsx in der Section 'projects'.
|
* SettingsScreen.tsx in der Section 'projects'.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Alert,
|
Alert,
|
||||||
@@ -60,17 +60,23 @@ export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onAc
|
|||||||
const [editName, setEditName] = useState('');
|
const [editName, setEditName] = useState('');
|
||||||
const [editDesc, setEditDesc] = useState('');
|
const [editDesc, setEditDesc] = useState('');
|
||||||
|
|
||||||
|
// Refs damit useCallback NICHT bei jeder Re-Render des Parents neu erzeugt
|
||||||
|
// wird (parent uebergibt oft inline-arrow-Callbacks, neue Identity jedes
|
||||||
|
// Render → useCallback re-runs → useEffect refeuert → infinite spinner).
|
||||||
|
const onActiveChangedRef = useRef(onActiveChanged);
|
||||||
|
useEffect(() => { onActiveChangedRef.current = onActiveChanged; }, [onActiveChanged]);
|
||||||
|
|
||||||
const load = useCallback(() => {
|
const load = useCallback(() => {
|
||||||
setLoading(true); setErr(null);
|
setLoading(true); setErr(null);
|
||||||
brainApi.getProjectStatus()
|
brainApi.getProjectStatus()
|
||||||
.then(status => {
|
.then(status => {
|
||||||
setProjects(status.projects || []);
|
setProjects(status.projects || []);
|
||||||
setActiveId(status.active_id || '');
|
setActiveId(status.active_id || '');
|
||||||
onActiveChanged?.(status.active);
|
onActiveChangedRef.current?.(status.active);
|
||||||
})
|
})
|
||||||
.catch(e => setErr(String(e?.message || e)))
|
.catch(e => setErr(String(e?.message || e)))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [onActiveChanged]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => { if (visible) load(); }, [visible, load]);
|
useEffect(() => { if (visible) load(); }, [visible, load]);
|
||||||
|
|
||||||
@@ -85,10 +91,10 @@ export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onAc
|
|||||||
brainApi.switchProject(id)
|
brainApi.switchProject(id)
|
||||||
.then(status => {
|
.then(status => {
|
||||||
setActiveId(status.active_id || '');
|
setActiveId(status.active_id || '');
|
||||||
onActiveChanged?.(status.active);
|
onActiveChangedRef.current?.(status.active);
|
||||||
})
|
})
|
||||||
.catch(e => Alert.alert('Fehler', String(e?.message || e)));
|
.catch(e => Alert.alert('Fehler', String(e?.message || e)));
|
||||||
}, [onActiveChanged]);
|
}, []);
|
||||||
|
|
||||||
const createProject = useCallback(() => {
|
const createProject = useCallback(() => {
|
||||||
const name = newName.trim();
|
const name = newName.trim();
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ interface ChatMessage {
|
|||||||
text: string;
|
text: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
attachments?: Attachment[];
|
attachments?: Attachment[];
|
||||||
|
/** Projekt-Zuordnung — leer = Hauptchat. Wird genutzt um Bubbles zu
|
||||||
|
* Projekt-Bloecken zu gruppieren (auf/einklappbar). */
|
||||||
|
projectId?: string;
|
||||||
/** Bridge-Message-ID zur Zuordnung von TTS-Audio */
|
/** Bridge-Message-ID zur Zuordnung von TTS-Audio */
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
/** Lokaler Pfad zur gecachten TTS-Audio-Datei (file://...) */
|
/** Lokaler Pfad zur gecachten TTS-Audio-Datei (file://...) */
|
||||||
@@ -284,6 +287,8 @@ const ChatScreen: React.FC = () => {
|
|||||||
const [searchVisible, setSearchVisible] = useState(false);
|
const [searchVisible, setSearchVisible] = useState(false);
|
||||||
const [projectsVisible, setProjectsVisible] = useState(false);
|
const [projectsVisible, setProjectsVisible] = useState(false);
|
||||||
const [activeProject, setActiveProject] = useState<BrainProject | null>(null);
|
const [activeProject, setActiveProject] = useState<BrainProject | null>(null);
|
||||||
|
// Lookup-Map id → Projekt — fuer Header-Names im Chat-Verlauf
|
||||||
|
const [projectNameById, setProjectNameById] = useState<Record<string, string>>({});
|
||||||
const [searchIndex, setSearchIndex] = useState(0); // welcher Treffer aktiv ist
|
const [searchIndex, setSearchIndex] = useState(0); // welcher Treffer aktiv ist
|
||||||
const [pendingAttachments, setPendingAttachments] = useState<{file: any, isPhoto: boolean}[]>([]);
|
const [pendingAttachments, setPendingAttachments] = useState<{file: any, isPhoto: boolean}[]>([]);
|
||||||
const [agentActivity, setAgentActivity] = useState<{activity: string, tool: string}>({activity: 'idle', tool: ''});
|
const [agentActivity, setAgentActivity] = useState<{activity: string, tool: string}>({activity: 'idle', tool: ''});
|
||||||
@@ -466,7 +471,14 @@ const ChatScreen: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadProject = () => {
|
const loadProject = () => {
|
||||||
brainApi.getProjectStatus()
|
brainApi.getProjectStatus()
|
||||||
.then(s => setActiveProject(s.active || null))
|
.then(s => {
|
||||||
|
setActiveProject(s.active || null);
|
||||||
|
// Lookup-Map fuellen damit der Chat-Verlauf Header mit Namen rendern kann
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
for (const p of (s.projects || [])) map[p.id] = p.name;
|
||||||
|
if (s.active) map[s.active.id] = s.active.name;
|
||||||
|
setProjectNameById(prev => ({ ...prev, ...map }));
|
||||||
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
};
|
};
|
||||||
loadProject();
|
loadProject();
|
||||||
@@ -1072,6 +1084,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
attachments: message.payload.attachments as Attachment[] | undefined,
|
attachments: message.payload.attachments as Attachment[] | undefined,
|
||||||
messageId: (message.payload.messageId as string) || undefined,
|
messageId: (message.payload.messageId as string) || undefined,
|
||||||
backupTs: (message.payload.backupTs as number) || undefined,
|
backupTs: (message.payload.backupTs as number) || undefined,
|
||||||
|
projectId: ((message.payload as any).projectId as string) || '',
|
||||||
};
|
};
|
||||||
// ARIA hat geantwortet → alle User-Bubbles davor als 'delivered'
|
// ARIA hat geantwortet → alle User-Bubbles davor als 'delivered'
|
||||||
// markieren (WhatsApp-Doppelhaken ✓✓). Brain hat sie verarbeitet.
|
// markieren (WhatsApp-Doppelhaken ✓✓). Brain hat sie verarbeitet.
|
||||||
@@ -1614,7 +1627,87 @@ const ChatScreen: React.FC = () => {
|
|||||||
() => messages.filter(m => !m.memorySaved && !m.triggerCreated && !m.skillCreated),
|
() => messages.filter(m => !m.memorySaved && !m.triggerCreated && !m.skillCreated),
|
||||||
[messages],
|
[messages],
|
||||||
);
|
);
|
||||||
const invertedMessages = useMemo(() => [...chatVisibleMessages].reverse(), [chatVisibleMessages]);
|
|
||||||
|
// Projekt-Bloecke: aufeinanderfolgende Nachrichten mit gleicher projectId
|
||||||
|
// werden visuell gruppiert. Erstes Message-Item einer Gruppe bekommt einen
|
||||||
|
// Header darueber (tappable für collapse). Collapsed → restliche Messages
|
||||||
|
// der Gruppe werden ausgefiltert; nur Header bleibt sichtbar.
|
||||||
|
// Hauptchat-Nachrichten (projectId leer) bleiben ungruppiert dazwischen.
|
||||||
|
const [collapsedProjects, setCollapsedProjects] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const projectMeta = useMemo(() => {
|
||||||
|
// Pre-compute: welche Message ist Erst-Element ihrer Projekt-Gruppe?
|
||||||
|
// Plus: wieviele Messages pro Projekt insgesamt (fuer Header-Count).
|
||||||
|
const firstOfGroup = new Set<string>();
|
||||||
|
const counts = new Map<string, number>();
|
||||||
|
let lastPid: string | null = null;
|
||||||
|
for (const m of chatVisibleMessages) {
|
||||||
|
const pid = m.projectId || '';
|
||||||
|
if (pid) counts.set(pid, (counts.get(pid) || 0) + 1);
|
||||||
|
if (pid && pid !== lastPid) firstOfGroup.add(m.id);
|
||||||
|
lastPid = pid || null;
|
||||||
|
}
|
||||||
|
return { firstOfGroup, counts };
|
||||||
|
}, [chatVisibleMessages]);
|
||||||
|
|
||||||
|
// Render-Filter: bei collapsed Projekten zeigen wir NUR das erste Message-
|
||||||
|
// Item der Gruppe (das traegt den Header). Restliche Messages werden ausge-
|
||||||
|
// blendet — Header allein steht dann zwischen Hauptchat-Bubbles.
|
||||||
|
const messagesForRender = useMemo(() => {
|
||||||
|
return chatVisibleMessages.filter(m => {
|
||||||
|
const pid = m.projectId || '';
|
||||||
|
if (!pid) return true;
|
||||||
|
if (!collapsedProjects.has(pid)) return true;
|
||||||
|
return projectMeta.firstOfGroup.has(m.id);
|
||||||
|
});
|
||||||
|
}, [chatVisibleMessages, collapsedProjects, projectMeta]);
|
||||||
|
|
||||||
|
// Auto-Collapse beim Projekt-Wechsel: altes Projekt einklappen, neues aufklappen.
|
||||||
|
const prevActiveIdRef = useRef<string>('');
|
||||||
|
useEffect(() => {
|
||||||
|
const newActive = activeProject?.id || '';
|
||||||
|
const prevActive = prevActiveIdRef.current;
|
||||||
|
if (newActive === prevActive) return;
|
||||||
|
setCollapsedProjects(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (prevActive) next.add(prevActive);
|
||||||
|
if (newActive) next.delete(newActive);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
prevActiveIdRef.current = newActive;
|
||||||
|
}, [activeProject]);
|
||||||
|
|
||||||
|
// Default: alle Projekte einklappen außer dem aktiven (Stefan-Vision:
|
||||||
|
// „beim ersten Öffnen sind alle projekte eingeklappt").
|
||||||
|
const collapseInitialized = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (collapseInitialized.current) return;
|
||||||
|
if (chatVisibleMessages.length === 0) return;
|
||||||
|
const allPids = new Set<string>();
|
||||||
|
for (const m of chatVisibleMessages) {
|
||||||
|
if (m.projectId) allPids.add(m.projectId);
|
||||||
|
}
|
||||||
|
if (allPids.size === 0) return;
|
||||||
|
const activePid = activeProject?.id || '';
|
||||||
|
setCollapsedProjects(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
for (const pid of allPids) {
|
||||||
|
if (pid !== activePid) next.add(pid);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
collapseInitialized.current = true;
|
||||||
|
}, [chatVisibleMessages, activeProject]);
|
||||||
|
|
||||||
|
const toggleProjectCollapse = useCallback((projectId: string) => {
|
||||||
|
setCollapsedProjects(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(projectId)) next.delete(projectId); else next.add(projectId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const invertedMessages = useMemo(() => [...messagesForRender].reverse(), [messagesForRender]);
|
||||||
|
|
||||||
// Such-Treffer: alle Message-IDs die zur Query passen. NEUESTE ZUERST —
|
// Such-Treffer: alle Message-IDs die zur Query passen. NEUESTE ZUERST —
|
||||||
// analog zu WhatsApp/Telegram: User ist visuell unten im Chat, der erste
|
// analog zu WhatsApp/Telegram: User ist visuell unten im Chat, der erste
|
||||||
@@ -1816,6 +1909,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
const location = await getCurrentLocation();
|
const location = await getCurrentLocation();
|
||||||
|
|
||||||
const cmid = nextClientMsgId();
|
const cmid = nextClientMsgId();
|
||||||
|
const activePid = activeProject?.id || '';
|
||||||
const userMsg: ChatMessage = {
|
const userMsg: ChatMessage = {
|
||||||
id: nextId(),
|
id: nextId(),
|
||||||
sender: 'user',
|
sender: 'user',
|
||||||
@@ -1824,16 +1918,18 @@ const ChatScreen: React.FC = () => {
|
|||||||
clientMsgId: cmid,
|
clientMsgId: cmid,
|
||||||
deliveryStatus: connectionStateRef.current === 'connected' ? 'sending' : 'queued',
|
deliveryStatus: connectionStateRef.current === 'connected' ? 'sending' : 'queued',
|
||||||
sendAttempts: 1,
|
sendAttempts: 1,
|
||||||
|
projectId: activePid,
|
||||||
};
|
};
|
||||||
setMessages(prev => capMessages([...prev, userMsg]));
|
setMessages(prev => capMessages([...prev, userMsg]));
|
||||||
|
|
||||||
console.log('[Chat] sende cmid=%s voice=%s speed=%s interrupted=%s',
|
console.log('[Chat] sende cmid=%s voice=%s speed=%s interrupted=%s project=%s',
|
||||||
cmid, localXttsVoiceRef.current || '(default)', ttsSpeedRef.current, wasInterrupted);
|
cmid, localXttsVoiceRef.current || '(default)', ttsSpeedRef.current, wasInterrupted, activePid || '(main)');
|
||||||
dispatchWithAck(cmid, 'chat', {
|
dispatchWithAck(cmid, 'chat', {
|
||||||
text,
|
text,
|
||||||
voice: localXttsVoiceRef.current,
|
voice: localXttsVoiceRef.current,
|
||||||
speed: ttsSpeedRef.current,
|
speed: ttsSpeedRef.current,
|
||||||
interrupted: wasInterrupted,
|
interrupted: wasInterrupted,
|
||||||
|
projectId: activePid,
|
||||||
...(location && { location }),
|
...(location && { location }),
|
||||||
});
|
});
|
||||||
}, [inputText, getCurrentLocation, pendingAttachments, sendPendingAttachments, interruptAriaIfBusy, dispatchWithAck]);
|
}, [inputText, getCurrentLocation, pendingAttachments, sendPendingAttachments, interruptAriaIfBusy, dispatchWithAck]);
|
||||||
@@ -2332,6 +2428,44 @@ const ChatScreen: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Wrapper: setzt einen einklappbaren Projekt-Header VOR die Bubble wenn
|
||||||
|
// diese das erste Element ihrer Projekt-Gruppe ist. Bei collapsed
|
||||||
|
// → nur Header, keine Bubble. Hauptchat-Nachrichten gehen ohne Header durch.
|
||||||
|
const renderMessageWithProjectHeader = ({ item }: { item: ChatMessage }) => {
|
||||||
|
const pid = item.projectId || '';
|
||||||
|
if (!pid) return renderMessage({ item });
|
||||||
|
const isFirstOfGroup = projectMeta.firstOfGroup.has(item.id);
|
||||||
|
if (!isFirstOfGroup) return renderMessage({ item });
|
||||||
|
const isCollapsed = collapsedProjects.has(pid);
|
||||||
|
const projectName = projectNameById[pid] || pid;
|
||||||
|
const projectCount = projectMeta.counts.get(pid) || 0;
|
||||||
|
const header = (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => toggleProjectCollapse(pid)}
|
||||||
|
style={{
|
||||||
|
marginVertical: 6, marginHorizontal: 8, paddingHorizontal: 10, paddingVertical: 8,
|
||||||
|
borderRadius: 6, backgroundColor: 'rgba(52,199,89,0.10)',
|
||||||
|
borderLeftWidth: 3, borderLeftColor: '#34C759',
|
||||||
|
flexDirection: 'row', alignItems: 'center', gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: 14, color: '#34C759', fontWeight: '700', flex: 1 }} numberOfLines={1}>
|
||||||
|
{isCollapsed ? '▶' : '▼'} 📁 {projectName}
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontSize: 11, color: '#8888AA' }}>
|
||||||
|
{projectCount} {projectCount === 1 ? 'Nachricht' : 'Nachrichten'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
if (isCollapsed) return header;
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{header}
|
||||||
|
{renderMessage({ item })}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Extrahiert kopierbare Items aus dem Bubble-Text (URLs, Mails, Telefon).
|
// Extrahiert kopierbare Items aus dem Bubble-Text (URLs, Mails, Telefon).
|
||||||
// Wird vom Long-Press/Copy-Menu genutzt damit Stefan den einzelnen Wert
|
// Wird vom Long-Press/Copy-Menu genutzt damit Stefan den einzelnen Wert
|
||||||
// teilen kann ohne den umliegenden Text mitzunehmen.
|
// teilen kann ohne den umliegenden Text mitzunehmen.
|
||||||
@@ -2483,9 +2617,8 @@ const ChatScreen: React.FC = () => {
|
|||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{/* Projekt-Indicator: zeigt Hauptchat oder aktives Projekt, Tap öffnet Liste */}
|
{/* Projekt-Indicator: zeigt Hauptchat oder aktives Projekt */}
|
||||||
<TouchableOpacity
|
<View
|
||||||
onPress={() => setProjectsVisible(true)}
|
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row', alignItems: 'center',
|
flexDirection: 'row', alignItems: 'center',
|
||||||
paddingHorizontal: 12, paddingVertical: 6,
|
paddingHorizontal: 12, paddingVertical: 6,
|
||||||
@@ -2494,13 +2627,26 @@ const ChatScreen: React.FC = () => {
|
|||||||
borderColor: activeProject ? '#34C759' : '#1E1E2E',
|
borderColor: activeProject ? '#34C759' : '#1E1E2E',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<TouchableOpacity onPress={() => setProjectsVisible(true)} style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
|
||||||
<Text style={{ fontSize: 13, color: activeProject ? '#34C759' : '#8888AA', fontWeight: activeProject ? '700' : '500', flex: 1 }} numberOfLines={1}>
|
<Text style={{ fontSize: 13, color: activeProject ? '#34C759' : '#8888AA', fontWeight: activeProject ? '700' : '500', flex: 1 }} numberOfLines={1}>
|
||||||
{activeProject ? `📁 ${activeProject.name}` : '💬 Hauptchat'}
|
{activeProject ? `📁 ${activeProject.name}` : '💬 Hauptchat'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ fontSize: 11, color: '#555570' }}>
|
<Text style={{ fontSize: 11, color: '#555570', marginRight: 8 }}>
|
||||||
{activeProject ? 'wechseln ›' : 'Projekte ›'}
|
{activeProject ? 'wechseln ›' : 'Projekte ›'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
{activeProject && (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
brainApi.switchProject('').then(s => setActiveProject(s.active || null)).catch(() => {});
|
||||||
|
}}
|
||||||
|
style={{ paddingHorizontal: 8, paddingVertical: 2, borderRadius: 4, backgroundColor: 'rgba(255,255,255,0.08)' }}
|
||||||
|
hitSlop={{top:8,bottom:8,left:8,right:8}}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: 11, color: '#E0E0F0', fontWeight: '600' }}>× Hauptchat</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Projekt-Modal */}
|
{/* Projekt-Modal */}
|
||||||
<ProjectsBrowser
|
<ProjectsBrowser
|
||||||
@@ -2591,7 +2737,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
}, 300);
|
}, 300);
|
||||||
}}
|
}}
|
||||||
keyExtractor={item => item.id}
|
keyExtractor={item => item.id}
|
||||||
renderItem={renderMessage}
|
renderItem={renderMessageWithProjectHeader}
|
||||||
contentContainerStyle={styles.messageList}
|
contentContainerStyle={styles.messageList}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ import SkillBrowser from '../components/SkillBrowser';
|
|||||||
import OAuthBrowser from '../components/OAuthBrowser';
|
import OAuthBrowser from '../components/OAuthBrowser';
|
||||||
import VoiceIdEnrollment from '../components/VoiceIdEnrollment';
|
import VoiceIdEnrollment from '../components/VoiceIdEnrollment';
|
||||||
import ProjectsBrowser from '../components/ProjectsBrowser';
|
import ProjectsBrowser from '../components/ProjectsBrowser';
|
||||||
|
import brainApi from '../services/brainApi';
|
||||||
import { isVerboseLogging, setVerboseLogging, isDebugLogsToBridge, setDebugLogsToBridge, APP_LOG_EVENT } from '../services/logger';
|
import { isVerboseLogging, setVerboseLogging, isDebugLogsToBridge, setDebugLogsToBridge, APP_LOG_EVENT } from '../services/logger';
|
||||||
import {
|
import {
|
||||||
isWakeReadySoundEnabled,
|
isWakeReadySoundEnabled,
|
||||||
@@ -204,7 +205,9 @@ const SettingsScreen: React.FC = () => {
|
|||||||
const [availableVoices, setAvailableVoices] = useState<Array<{name: string, size: number}>>([]);
|
const [availableVoices, setAvailableVoices] = useState<Array<{name: string, size: number}>>([]);
|
||||||
// Datei-Manager
|
// Datei-Manager
|
||||||
const [fileManagerOpen, setFileManagerOpen] = useState(false);
|
const [fileManagerOpen, setFileManagerOpen] = useState(false);
|
||||||
const [fileManagerFiles, setFileManagerFiles] = useState<Array<{name: string; path: string; size: number; mtime: number; fromAria: boolean}>>([]);
|
const [fileManagerFiles, setFileManagerFiles] = useState<Array<{name: string; path: string; size: number; mtime: number; fromAria: boolean; projectId?: string}>>([]);
|
||||||
|
const [fileFilterProjectId, setFileFilterProjectId] = useState<string>('__all__');
|
||||||
|
const [fileFilterProjects, setFileFilterProjects] = useState<Array<{id: string; name: string}>>([]);
|
||||||
const [fileManagerLoading, setFileManagerLoading] = useState(false);
|
const [fileManagerLoading, setFileManagerLoading] = useState(false);
|
||||||
const [fileManagerError, setFileManagerError] = useState('');
|
const [fileManagerError, setFileManagerError] = useState('');
|
||||||
const [fileManagerSearch, setFileManagerSearch] = useState('');
|
const [fileManagerSearch, setFileManagerSearch] = useState('');
|
||||||
@@ -726,6 +729,20 @@ const SettingsScreen: React.FC = () => {
|
|||||||
return () => unsub();
|
return () => unsub();
|
||||||
}, [fileManagerOpen]);
|
}, [fileManagerOpen]);
|
||||||
|
|
||||||
|
// Beim Oeffnen des Datei-Managers: Projekt-Liste laden fuer den Filter.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!fileManagerOpen) return;
|
||||||
|
brainApi.listProjects(true)
|
||||||
|
.then(list => setFileFilterProjects(list.map(p => ({ id: p.id, name: p.name }))))
|
||||||
|
.catch(() => {});
|
||||||
|
// Default-Filter: aktives Projekt (falls vorhanden), sonst "alle".
|
||||||
|
brainApi.getProjectStatus()
|
||||||
|
.then(s => {
|
||||||
|
if (s.active_id) setFileFilterProjectId(s.active_id);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, [fileManagerOpen]);
|
||||||
|
|
||||||
// --- QR-Code scannen ---
|
// --- QR-Code scannen ---
|
||||||
|
|
||||||
const openQRScanner = useCallback(() => {
|
const openQRScanner = useCallback(() => {
|
||||||
@@ -962,6 +979,29 @@ const SettingsScreen: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
{/* Projekt-Filter: scrollbare Pill-Reihe. „Alle Projekte" + „Hauptchat" +
|
||||||
|
ein Pill pro Projekt. Default = aktives Projekt (siehe useEffect oben). */}
|
||||||
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}
|
||||||
|
style={{marginTop:6}} contentContainerStyle={{gap:6, paddingRight:8}}>
|
||||||
|
{[
|
||||||
|
{ id: '__all__', name: '📁 Alle Projekte' },
|
||||||
|
{ id: '', name: '💬 Hauptchat' },
|
||||||
|
...fileFilterProjects,
|
||||||
|
].map(p => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={p.id || 'mainchat'}
|
||||||
|
onPress={() => setFileFilterProjectId(p.id)}
|
||||||
|
style={{
|
||||||
|
paddingVertical:6, paddingHorizontal:12, borderRadius:14,
|
||||||
|
backgroundColor: fileFilterProjectId === p.id ? '#34C759' : '#1E1E2E',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{color: fileFilterProjectId === p.id ? '#fff' : '#8888AA', fontSize:12}}>
|
||||||
|
{p.name}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
{fileManagerLoading ? (
|
{fileManagerLoading ? (
|
||||||
<Text style={{color:'#8888AA', textAlign:'center', marginTop:20}}>Lade...</Text>
|
<Text style={{color:'#8888AA', textAlign:'center', marginTop:20}}>Lade...</Text>
|
||||||
@@ -972,6 +1012,11 @@ const SettingsScreen: React.FC = () => {
|
|||||||
let files = fileManagerFiles;
|
let files = fileManagerFiles;
|
||||||
if (fileManagerFilter === 'aria') files = files.filter(f => f.fromAria);
|
if (fileManagerFilter === 'aria') files = files.filter(f => f.fromAria);
|
||||||
else if (fileManagerFilter === 'user') files = files.filter(f => !f.fromAria);
|
else if (fileManagerFilter === 'user') files = files.filter(f => !f.fromAria);
|
||||||
|
// Projekt-Filter: '__all__' = alles, '' = Hauptchat (kein project_id),
|
||||||
|
// sonst exakte project_id-Match.
|
||||||
|
if (fileFilterProjectId !== '__all__') {
|
||||||
|
files = files.filter(f => (f.projectId || '') === fileFilterProjectId);
|
||||||
|
}
|
||||||
if (fileManagerSearch) {
|
if (fileManagerSearch) {
|
||||||
const q = fileManagerSearch.toLowerCase();
|
const q = fileManagerSearch.toLowerCase();
|
||||||
files = files.filter(f => f.name.toLowerCase().includes(q));
|
files = files.filter(f => f.name.toLowerCase().includes(q));
|
||||||
|
|||||||
@@ -614,6 +614,11 @@ class ChatOut(BaseModel):
|
|||||||
turns: int
|
turns: int
|
||||||
distilling: bool
|
distilling: bool
|
||||||
events: list = Field(default_factory=list)
|
events: list = Field(default_factory=list)
|
||||||
|
# Aktive Projekt-ID NACH dem Turn (kann durch project_enter/exit-Tools
|
||||||
|
# waehrend des Turns gewechselt haben). Bridge gibt das an die Chat-
|
||||||
|
# Bubble-Broadcasts weiter damit App + Diagnostic die Nachricht zum
|
||||||
|
# richtigen Projekt-Block sortieren koennen.
|
||||||
|
project_id: str = ""
|
||||||
|
|
||||||
|
|
||||||
@app.post("/chat", response_model=ChatOut)
|
@app.post("/chat", response_model=ChatOut)
|
||||||
@@ -637,6 +642,7 @@ def chat(body: ChatIn, background: BackgroundTasks):
|
|||||||
turns=len(a.conversation.turns),
|
turns=len(a.conversation.turns),
|
||||||
distilling=needs_distill,
|
distilling=needs_distill,
|
||||||
events=a.pop_events(),
|
events=a.pop_events(),
|
||||||
|
project_id=projects_mod.get_active(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+44
-1
@@ -1005,6 +1005,37 @@ class ARIABridge:
|
|||||||
cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
|
cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
|
||||||
return cleaned, files, missing
|
return cleaned, files, missing
|
||||||
|
|
||||||
|
def _tag_file_to_active_project(self, file_path: str) -> None:
|
||||||
|
"""Holt vom Brain das aktive Projekt + schreibt file_path → project_id
|
||||||
|
in /shared/config/file_projects.json. Best-effort, fail-silent.
|
||||||
|
Wird vom File-Save-Handler nach erfolgreichem Schreiben aufgerufen."""
|
||||||
|
try:
|
||||||
|
brain_url = os.environ.get("BRAIN_URL", "http://aria-brain:8080")
|
||||||
|
with urllib.request.urlopen(f"{brain_url}/projects/status", timeout=5) as r:
|
||||||
|
data = json.loads(r.read())
|
||||||
|
active_id = (data.get("active_id") or "").strip()
|
||||||
|
if not active_id:
|
||||||
|
return
|
||||||
|
manifest_path = "/shared/config/file_projects.json"
|
||||||
|
os.makedirs("/shared/config", exist_ok=True)
|
||||||
|
try:
|
||||||
|
with open(manifest_path) as f:
|
||||||
|
manifest = json.load(f)
|
||||||
|
if not isinstance(manifest, dict):
|
||||||
|
manifest = {}
|
||||||
|
except FileNotFoundError:
|
||||||
|
manifest = {}
|
||||||
|
except Exception:
|
||||||
|
manifest = {}
|
||||||
|
manifest[file_path] = active_id
|
||||||
|
tmp = manifest_path + ".tmp"
|
||||||
|
with open(tmp, "w") as f:
|
||||||
|
json.dump(manifest, f, indent=2, ensure_ascii=False)
|
||||||
|
os.replace(tmp, manifest_path)
|
||||||
|
logger.info("[file-project] %s → %s", file_path, active_id)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("[file-project] tag failed (%s): %s", file_path, exc)
|
||||||
|
|
||||||
async def _broadcast_aria_file(self, file_info: dict) -> None:
|
async def _broadcast_aria_file(self, file_info: dict) -> None:
|
||||||
"""ARIA hat eine Datei fuer den User erstellt — App+Diagnostic informieren."""
|
"""ARIA hat eine Datei fuer den User erstellt — App+Diagnostic informieren."""
|
||||||
logger.info("[rvs] ARIA-Datei rausgeben: %s (%s, %dKB)",
|
logger.info("[rvs] ARIA-Datei rausgeben: %s (%s, %dKB)",
|
||||||
@@ -1224,6 +1255,9 @@ class ARIABridge:
|
|||||||
"backupTs": assistant_backup_ts,
|
"backupTs": assistant_backup_ts,
|
||||||
# Debug: aufbereiteter Text fuer TTS (App ignoriert, Diagnostic zeigt optional)
|
# Debug: aufbereiteter Text fuer TTS (App ignoriert, Diagnostic zeigt optional)
|
||||||
"ttsText": tts_text_preview if tts_text_preview != text else "",
|
"ttsText": tts_text_preview if tts_text_preview != text else "",
|
||||||
|
# Projekt-Zuordnung — App + Diagnostic sortieren die Bubble in
|
||||||
|
# den passenden Projekt-Block. Leer = Hauptchat.
|
||||||
|
"projectId": (payload.get("projectId") or "") if isinstance(payload, dict) else "",
|
||||||
},
|
},
|
||||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||||
})
|
})
|
||||||
@@ -1521,6 +1555,11 @@ class ARIABridge:
|
|||||||
await self._emit_activity("idle", "")
|
await self._emit_activity("idle", "")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Projekt-Kontext des Turns — wird an _process_core_response weiter-
|
||||||
|
# gegeben damit der chat-Broadcast die Bubble dem richtigen Projekt-
|
||||||
|
# Block in App + Diagnostic zuordnen kann.
|
||||||
|
turn_project_id = (data.get("project_id") or "").strip()
|
||||||
|
|
||||||
# Side-Channel-Events VOR der Chat-Bubble broadcasten (z.B. skill_created)
|
# Side-Channel-Events VOR der Chat-Bubble broadcasten (z.B. skill_created)
|
||||||
# damit sie in der UI vor der Reply auftauchen
|
# damit sie in der UI vor der Reply auftauchen
|
||||||
for event in data.get("events", []) or []:
|
for event in data.get("events", []) or []:
|
||||||
@@ -1586,7 +1625,7 @@ class ARIABridge:
|
|||||||
# passend behandelt wird (hier minimal, weil Brain noch keine
|
# passend behandelt wird (hier minimal, weil Brain noch keine
|
||||||
# metadata mitschickt).
|
# metadata mitschickt).
|
||||||
try:
|
try:
|
||||||
await self._process_core_response(reply, {})
|
await self._process_core_response(reply, {"projectId": turn_project_id})
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("[brain] _process_core_response Fehler")
|
logger.exception("[brain] _process_core_response Fehler")
|
||||||
await self._emit_activity("idle", "")
|
await self._emit_activity("idle", "")
|
||||||
@@ -2125,6 +2164,10 @@ class ARIABridge:
|
|||||||
f.write(base64.b64decode(file_b64))
|
f.write(base64.b64decode(file_b64))
|
||||||
size_kb = len(file_b64) // 1365
|
size_kb = len(file_b64) // 1365
|
||||||
logger.info("[rvs] Datei gespeichert: %s (%dKB)", file_path, size_kb)
|
logger.info("[rvs] Datei gespeichert: %s (%dKB)", file_path, size_kb)
|
||||||
|
# Datei dem aktuellen Projekt zuordnen (falls Stefan in einem ist).
|
||||||
|
# Manifest in /shared/config/file_projects.json — File-Manager
|
||||||
|
# in App + Diagnostic filtert danach.
|
||||||
|
self._tag_file_to_active_project(file_path)
|
||||||
|
|
||||||
# Pixel-Bilder fuer Claude-Vision shrinken wenn > 2 MB. SVG/PDF/ZIP
|
# Pixel-Bilder fuer Claude-Vision shrinken wenn > 2 MB. SVG/PDF/ZIP
|
||||||
# bleiben unangetastet (Vision laeuft eh nur auf Raster-Formaten).
|
# bleiben unangetastet (Vision laeuft eh nur auf Raster-Formaten).
|
||||||
|
|||||||
@@ -1109,6 +1109,11 @@
|
|||||||
<option value="aria">Von ARIA (aria_*)</option>
|
<option value="aria">Von ARIA (aria_*)</option>
|
||||||
<option value="user">Vom Benutzer</option>
|
<option value="user">Vom Benutzer</option>
|
||||||
</select>
|
</select>
|
||||||
|
<select id="files-filter-project" onchange="renderFilesList()" style="background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;font-size:11px;">
|
||||||
|
<option value="__all__">Alle Projekte</option>
|
||||||
|
<option value="">💬 Hauptchat</option>
|
||||||
|
<!-- Project options werden dynamisch via loadFiles() befuellt -->
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div id="files-info" style="margin-top:6px;font-size:10px;color:#8888AA;"></div>
|
<div id="files-info" style="margin-top:6px;font-size:10px;color:#8888AA;"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -4209,6 +4214,37 @@
|
|||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
if (!d.ok) throw new Error(d.error || 'Unbekannter Fehler');
|
if (!d.ok) throw new Error(d.error || 'Unbekannter Fehler');
|
||||||
filesCache = d.files || [];
|
filesCache = d.files || [];
|
||||||
|
// Projekt-Filter-Optionen aktualisieren — Liste aller bekannten projectIds
|
||||||
|
// aus den Dateien + Namen via brain api.
|
||||||
|
const pidsInFiles = new Set(filesCache.map(f => f.projectId).filter(Boolean));
|
||||||
|
try {
|
||||||
|
const pr = await fetch('/api/brain/projects/list?include_archived=true');
|
||||||
|
const pdata = await pr.json();
|
||||||
|
const projects = pdata?.projects || [];
|
||||||
|
const sel = document.getElementById('files-filter-project');
|
||||||
|
if (sel) {
|
||||||
|
const current = sel.value;
|
||||||
|
// Bestehende Options ab Index 2 (nach __all__ und Hauptchat) entfernen
|
||||||
|
while (sel.options.length > 2) sel.remove(2);
|
||||||
|
for (const p of projects) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = p.id;
|
||||||
|
opt.textContent = `📁 ${p.name}`;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
}
|
||||||
|
// Auch IDs aus Files die nicht in projects sind (gelöschte Projekte)
|
||||||
|
const knownIds = new Set(projects.map(p => p.id));
|
||||||
|
for (const pid of pidsInFiles) {
|
||||||
|
if (!knownIds.has(pid)) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = pid;
|
||||||
|
opt.textContent = `📁 ${pid} (gelöscht?)`;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sel.value = current || '__all__';
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
// Selection bereinigen — nicht mehr existierende Pfade raus
|
// Selection bereinigen — nicht mehr existierende Pfade raus
|
||||||
const existing = new Set(filesCache.map(f => f.path));
|
const existing = new Set(filesCache.map(f => f.path));
|
||||||
for (const p of [...filesSelected]) if (!existing.has(p)) filesSelected.delete(p);
|
for (const p of [...filesSelected]) if (!existing.has(p)) filesSelected.delete(p);
|
||||||
@@ -4221,9 +4257,11 @@
|
|||||||
function getVisibleFiles() {
|
function getVisibleFiles() {
|
||||||
const q = (document.getElementById('files-search').value || '').toLowerCase();
|
const q = (document.getElementById('files-search').value || '').toLowerCase();
|
||||||
const filter = document.getElementById('files-filter').value;
|
const filter = document.getElementById('files-filter').value;
|
||||||
|
const pidFilter = document.getElementById('files-filter-project')?.value || '__all__';
|
||||||
let files = filesCache.slice();
|
let files = filesCache.slice();
|
||||||
if (filter === 'aria') files = files.filter(f => f.fromAria);
|
if (filter === 'aria') files = files.filter(f => f.fromAria);
|
||||||
else if (filter === 'user') files = files.filter(f => !f.fromAria);
|
else if (filter === 'user') files = files.filter(f => !f.fromAria);
|
||||||
|
if (pidFilter !== '__all__') files = files.filter(f => (f.projectId || '') === pidFilter);
|
||||||
if (q) files = files.filter(f => f.name.toLowerCase().includes(q));
|
if (q) files = files.filter(f => f.name.toLowerCase().includes(q));
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -297,6 +297,32 @@ function writeRuntimeConfig(patch) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Atomic write: temp-file + rename, laute Logs bei Fehler.
|
// Atomic write: temp-file + rename, laute Logs bei Fehler.
|
||||||
|
// ── File-Project-Manifest ───────────────────────────────────────────
|
||||||
|
// Jeder Eintrag map[absoluter_pfad] = project_id (leer = Hauptchat).
|
||||||
|
// Wird vom files-list-Endpoint + files-set-project gepflegt.
|
||||||
|
const FILE_PROJECTS_FILE = "/shared/config/file_projects.json";
|
||||||
|
|
||||||
|
function loadFileProjects() {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(FILE_PROJECTS_FILE)) return {};
|
||||||
|
const data = JSON.parse(fs.readFileSync(FILE_PROJECTS_FILE, "utf-8"));
|
||||||
|
return (data && typeof data === "object") ? data : {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveFileProjects(manifest) {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync("/shared/config", { recursive: true });
|
||||||
|
const tmp = FILE_PROJECTS_FILE + ".tmp";
|
||||||
|
fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2));
|
||||||
|
fs.renameSync(tmp, FILE_PROJECTS_FILE);
|
||||||
|
} catch (err) {
|
||||||
|
log("warn", "files", `file-projects-Manifest schreiben fehlgeschlagen: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function persistActiveSession(key) {
|
function persistActiveSession(key) {
|
||||||
try {
|
try {
|
||||||
const tmp = SESSION_KEY_FILE + ".tmp";
|
const tmp = SESSION_KEY_FILE + ".tmp";
|
||||||
@@ -1598,6 +1624,7 @@ const server = http.createServer((req, res) => {
|
|||||||
const dir = "/shared/uploads";
|
const dir = "/shared/uploads";
|
||||||
let entries = [];
|
let entries = [];
|
||||||
try { entries = fs.readdirSync(dir); } catch { entries = []; }
|
try { entries = fs.readdirSync(dir); } catch { entries = []; }
|
||||||
|
const manifest = loadFileProjects();
|
||||||
const files = entries
|
const files = entries
|
||||||
.map(name => {
|
.map(name => {
|
||||||
try {
|
try {
|
||||||
@@ -1610,6 +1637,7 @@ const server = http.createServer((req, res) => {
|
|||||||
size: st.size,
|
size: st.size,
|
||||||
mtime: Math.floor(st.mtimeMs),
|
mtime: Math.floor(st.mtimeMs),
|
||||||
fromAria: name.startsWith("aria_"),
|
fromAria: name.startsWith("aria_"),
|
||||||
|
projectId: manifest[full] || '',
|
||||||
};
|
};
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
})
|
})
|
||||||
@@ -1622,6 +1650,31 @@ const server = http.createServer((req, res) => {
|
|||||||
res.end(JSON.stringify({ ok: false, error: err.message }));
|
res.end(JSON.stringify({ ok: false, error: err.message }));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
} else if (req.url === "/api/files-set-project" && req.method === "POST") {
|
||||||
|
// Body: { path, projectId } — projectId leer = Hauptchat (= Eintrag entfernen)
|
||||||
|
let body = "";
|
||||||
|
req.on("data", c => { body += c; if (body.length > 8192) req.destroy(); });
|
||||||
|
req.on("end", () => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(body || "{}");
|
||||||
|
const fpath = String(data.path || "");
|
||||||
|
const pid = String(data.projectId || "");
|
||||||
|
if (!fpath.startsWith("/shared/uploads/") || !fs.existsSync(fpath)) {
|
||||||
|
res.writeHead(404, { "Content-Type": "application/json" });
|
||||||
|
return res.end(JSON.stringify({ ok: false, error: "Datei nicht gefunden" }));
|
||||||
|
}
|
||||||
|
const manifest = loadFileProjects();
|
||||||
|
if (pid) manifest[fpath] = pid;
|
||||||
|
else delete manifest[fpath];
|
||||||
|
saveFileProjects(manifest);
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ ok: true, path: fpath, projectId: pid }));
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(500, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ ok: false, error: err.message }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
} else if ((req.url.startsWith("/api/files-download?") || req.url.startsWith("/api/files-view?")) && req.method === "GET") {
|
} else if ((req.url.startsWith("/api/files-download?") || req.url.startsWith("/api/files-view?")) && req.method === "GET") {
|
||||||
// /api/files-download → mit Content-Disposition:attachment (Browser downloaded)
|
// /api/files-download → mit Content-Disposition:attachment (Browser downloaded)
|
||||||
// /api/files-view → mit Disposition:inline (Browser zeigt PDF/Bilder im Tab)
|
// /api/files-view → mit Disposition:inline (Browser zeigt PDF/Bilder im Tab)
|
||||||
|
|||||||
Reference in New Issue
Block a user