Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e61a0ff871 | ||
|
|
c9a1d80696 | ||
|
|
8a0670a3d2 | ||
|
|
2d9c42a7ea |
@@ -10,6 +10,69 @@ Alle Änderungen am Projekt. Format: [Keep a Changelog](https://keepachangelog.c
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [0.2.1.4] — 2026-07-12 — Lokales LLM: der ehrliche Rückbau
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
|
||||||
|
**Lokales LLM wieder tool-los (B1a) — ein 8B ist ein schlechter Tool-Caller**
|
||||||
|
- B1b hatte dem lokalen Modell Werkzeuge (`run_*`/`web_search`) gegeben — die gemeinsame Wurzel von **zwei** Problemen: (1) ein 8B erfindet mit Werkzeug in der Hand lieber eine plausible Antwort („der Song ist X") statt es zu rufen → Halluzination; (2) das erzwang per-Skill-Guards (skaliert nicht). Local ist jetzt wieder **tool-los** = reines Reden; alles mit Grundwahrheit (Fakt/Live-Zustand/Gedächtnis/Aktion) gehört an Claude oder den deterministischen Fast-Path. Kein Skill-Ergebnis mehr fälschbar
|
||||||
|
- **Keine Input-Wortliste im Router:** eine kurz eingeführte `_LIVE_HINTS`-Blacklist (Wetter/Musik/… → Claude) wieder entfernt — aus offenem Freitext die Absicht per Wortliste zu raten ist nie vollständig, jeder Miss = ein Halo (nur von per-Skill auf per-Wort verschoben). Generisch = das Modell entscheidet **selbst** (`<<ESCALATE>>`); ein stärkeres lokales Modell übernimmt die Selbst-Erkennung später, bis dahin ist local per Einstellung abschaltbar (aus, nicht raus)
|
||||||
|
- Expliziter Nutzer-Wunsch „nimm Claude/Clodi" wird im Router respektiert (geht nie lokal)
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
|
||||||
|
- **Info-Halluzination:** local nannte manchmal aktuellen Song/Restzeit/Skip-Titel ohne `run_spotify` zu rufen (mal echt, mal frei erfunden — „Midnight City von M83" nie aufgerufen). Neuer Output-Guard `_claims_live_media_state` eskaliert behauptete Live-Auskünfte ohne echten Skill-Call an Claude; Local-Prompt zusätzlich gehärtet (nie Titel/Zeit/Gerät ohne Tool-Ergebnis; bei „OK: next" keinen Titel erfinden)
|
||||||
|
- **Leeres `<voice></voice>` machte TTS stumm:** Claude hängt reflexartig manchmal ein leeres Voice-Tag an → `clean_text_for_tts` nahm den leeren Inhalt → gar keine Sprachausgabe (Playlist „Fliegen" gesprochen, „Prodigy" stumm — reiner Claude-Output-Zufall, nicht Skill/Playlist-Name). Leeres/whitespace-Tag wird jetzt ignoriert, der normale Anzeigetext gelesen
|
||||||
|
- **Datei-Anhang erschien erst nach Seitenwechsel:** die Live-`chat`-Payload trug keine `files` (Anhänge kamen nur als separates `file_from_aria`-Event) → an der Nachricht tauchte die Datei erst nach Reload aus `chat_backup` auf. Bridge schickt die `files` jetzt in der chat-Payload, App hängt sie live an die Text-Bubble (wie der Reload-Pfad) und entfernt die redundante Solo-Bubble
|
||||||
|
- **TDZ-Zeitbombe (App):** `sendTextMessage` stand vor seinen Dependencies (`interruptAriaIfBusy`, `sendPendingAttachments`) im deps-Array — Temporal Dead Zone; lief nur dank Babels `const`→`var`-Hebung, ein strengerer Bundler hätte beim Mount weißgescreent. Deklaration hinter die Deps verschoben
|
||||||
|
- **QRScanner tsc-clean:** toter Prop `colorForScannerFrame` (existiert in `react-native-camera-kit` v13 nicht) entfernt; die fehlerhaften Lib-Typen (optionale Props als required markiert) lokal + dokumentiert umgangen → **Projekt komplett tsc-clean (0 Fehler)**
|
||||||
|
|
||||||
|
**Spotify-Skill — von ARIA live im Gespräch weiter geschärft**
|
||||||
|
- Skip (`next`/`previous`) sagt jetzt den **echten** neuen Titel an (holt den Track nach dem Skip via API) statt local einen erfinden zu lassen
|
||||||
|
- `playlist_play`/`search_and_play`/`play` nennen das **tatsächliche** Wiedergabegerät (aus `GET /v1/me/player`, kein Raten — verhinderte den Fehler, dass Claude ein falsch geratenes Gerät auch noch ansteuerte) und lesen konsistent vor; saubere „Sprach-Grammatik": Ansagen sprechen, Steuerbefehle (play/pause/transfer/volume) schweigen
|
||||||
|
- `yt-dlp-download`-Skill um einen MP3-Modus erweitert — ARIA hat das fehlende Werkzeug **selbst gebaut**, als eine deutsche Titelmelodie nicht auf Spotify lag (Web-Suche → YouTube-Download → MP3 in den Chat, in <1 min)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.2.1.3] — 2026-07-11
|
||||||
|
|
||||||
|
### Hinzugefügt
|
||||||
|
|
||||||
|
- **`skill_get`-Tool:** ARIA liest den echten Quellcode + Manifest + Readme eines Skills, **bevor** sie ihn ändert — kein Blind-Rewrite mehr (vorher wurde ein guter Skill durch eine schlechtere Neufassung ersetzt, weil das referenzierte `skill_get` gar nicht existierte)
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
|
||||||
|
- **`converse` folgt dem Skill (Fast-Path):** auch ein Fast-Path-Befehl kann einen Skill auslösen, nach dem noch etwas zu sagen ist — `converse` kommt jetzt aus Manifest/Skill-Output statt hart auf `False`
|
||||||
|
- **Sprachnachricht-Bubble verschwand nach manuellem Stop:** bei „ohne Ohr" aufgenommener Sprachnachricht + Stop entfernte ein leeres stream-end-Endpoint die schon gefüllte Bubble; jetzt wird nur noch der unaufgelöste Platzhalter entfernt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.2.1.2] — 2026-07-11 — Standort-Intelligenz + Skill steuert seine Ausgabe
|
||||||
|
|
||||||
|
### Hinzugefügt
|
||||||
|
|
||||||
|
**GPS → Ortsname im Standort-Präfix (keyless, keine Tokens)**
|
||||||
|
- Reverse-Geocoding der Koordinaten in der Bridge (Nominatim zoom=18: Straße+Hausnr, PLZ+Ort, Bundesland; Straßen-Ref + Autobahn-km via Overpass) — damit das lokale Modell nicht „Berlin" für Oldenburg rät; Ortsname wird **vor** dem Präfix-Bau awaited (rechtzeitig für die erste Nachricht)
|
||||||
|
- Fahrtrichtung als Himmelsrichtung **+ exakte Peilung in Grad** (Haversine/Bearing aus aufeinanderfolgenden Fixes, `MIN_MOVE_M`-Schwelle gegen Zittern)
|
||||||
|
|
||||||
|
**Skill steuert seine Ausgabe selbst — `speak` + `converse` pro Aufruf**
|
||||||
|
- Ein Skill entscheidet per JSON-Output `{speak, converse}` pro Operation, ob vorgelesen wird und ob danach 30 s weitergelauscht wird (Manifest-Default, Output überschreibt) — z. B. „was läuft" vorlesen aber kein Dialog, „nächstes Lied" stumm. In der Skill-Bauanleitung **mit dem WARUM** dokumentiert, damit die KI die Flags beim Bauen versteht (kein Hardcode im Brain)
|
||||||
|
|
||||||
|
### Behoben / Geändert
|
||||||
|
|
||||||
|
- **Generischer Skill-Prompt (kein Hardcode):** der Router beschreibt `run_*`-Skills generisch (weiß nicht mehr, welche „schwer" sind); ein Stop im 30-s-Lauschen beendet dieses jetzt wirklich (kein zweiter Gong / erneutes Öffnen)
|
||||||
|
- **Anti-Halluzination:** behauptet local eine Steuerbefehl-Quittung („Spotify: …", „Playlist abspielen") ohne das Tool wirklich zu rufen → Eskalation an Claude statt erfundene Bestätigung durchzulassen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.2.1.1] — 2026-07-11
|
||||||
|
|
||||||
|
### Hinzugefügt
|
||||||
|
|
||||||
|
- **Skill entscheidet selbst, ob vorgelesen wird (`manifest.speak`):** Grundlage der späteren `speak`/`converse`-Architektur — der `speak`-Flag greift sowohl im lokalen als auch im Claude-Pfad, statt am fragilen leeren `<voice></voice>`-Hack zu hängen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.2.0.6] — 2026-07-11
|
## [0.2.0.6] — 2026-07-11
|
||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
|
|||||||
@@ -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 20104
|
versionCode 20105
|
||||||
versionName "0.2.1.4"
|
versionName "0.2.1.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.2.1.4",
|
"version": "0.2.1.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -64,6 +64,14 @@ interface Attachment {
|
|||||||
deleted?: boolean; // Datei wurde nachtraeglich geloescht (Diagnostic-Manager)
|
deleted?: boolean; // Datei wurde nachtraeglich geloescht (Diagnostic-Manager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pro-Projekt-Queue-Zustand: idle = nichts laeuft; running = ARIA arbeitet am
|
||||||
|
// aktuellen Task; awaiting_reply = ARIA hat eine blockierende Rueckfrage gestellt,
|
||||||
|
// die naechste Eingabe beantwortet sie (statt einen neuen Auftrag anzustellen).
|
||||||
|
type CtxState = 'idle' | 'running' | 'awaiting_reply';
|
||||||
|
|
||||||
|
// Wartender Queue-Eintrag (id = die Bubble-ID der pending_queue-Nachricht).
|
||||||
|
interface QueuedItem { id: string; text: string; }
|
||||||
|
|
||||||
interface ChatMessage {
|
interface ChatMessage {
|
||||||
id: string;
|
id: string;
|
||||||
sender: 'user' | 'aria';
|
sender: 'user' | 'aria';
|
||||||
@@ -131,7 +139,7 @@ interface ChatMessage {
|
|||||||
/** Delivery-Status der User-Bubble (WhatsApp-style): queued = noch nicht
|
/** Delivery-Status der User-Bubble (WhatsApp-style): queued = noch nicht
|
||||||
* raus (offline), sending = an Bridge unterwegs, sent = Bridge hat ACK
|
* raus (offline), sending = an Bridge unterwegs, sent = Bridge hat ACK
|
||||||
* gesendet, delivered = Brain hat geantwortet, failed = Retry-Limit. */
|
* gesendet, delivered = Brain hat geantwortet, failed = Retry-Limit. */
|
||||||
deliveryStatus?: 'queued' | 'sending' | 'sent' | 'delivered' | 'failed';
|
deliveryStatus?: 'queued' | 'pending_queue' | 'sending' | 'sent' | 'delivered' | 'failed';
|
||||||
/** Anzahl der bisherigen Sende-Versuche (fuer Retry-Limit). */
|
/** Anzahl der bisherigen Sende-Versuche (fuer Retry-Limit). */
|
||||||
sendAttempts?: number;
|
sendAttempts?: number;
|
||||||
}
|
}
|
||||||
@@ -275,6 +283,7 @@ async function checkFileExists(uri: string): Promise<boolean> {
|
|||||||
const ChatScreen: React.FC = () => {
|
const ChatScreen: React.FC = () => {
|
||||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
const [inputText, setInputText] = useState('');
|
const [inputText, setInputText] = useState('');
|
||||||
|
const inputTextRef = useRef('');
|
||||||
const [connectionState, setConnectionState] = useState<ConnectionState>('disconnected');
|
const [connectionState, setConnectionState] = useState<ConnectionState>('disconnected');
|
||||||
const [showFileUpload, setShowFileUpload] = useState(false);
|
const [showFileUpload, setShowFileUpload] = useState(false);
|
||||||
const [showCameraUpload, setShowCameraUpload] = useState(false);
|
const [showCameraUpload, setShowCameraUpload] = useState(false);
|
||||||
@@ -301,6 +310,24 @@ const ChatScreen: React.FC = () => {
|
|||||||
// Ref-Spiegel fuer Callbacks (interruptAriaIfBusy liest den aktuellen
|
// Ref-Spiegel fuer Callbacks (interruptAriaIfBusy liest den aktuellen
|
||||||
// Busy-Status des fokussierten Kontexts ohne stale Closure).
|
// Busy-Status des fokussierten Kontexts ohne stale Closure).
|
||||||
const queueStatusRef = useRef<Record<string, { busy: boolean; queue_size: number }>>({});
|
const queueStatusRef = useRef<Record<string, { busy: boolean; queue_size: number }>>({});
|
||||||
|
// ── Pro-Projekt-Nachrichten-Queue + Zustandsautomat (app-lokal) ──
|
||||||
|
// Key = projectId ('' = Hauptchat). state pro Kontext + Queue der wartenden
|
||||||
|
// User-Bubbles. Sendet man waehrend 'running', wird angestellt statt
|
||||||
|
// abgebrochen; 'awaiting_reply' leitet die naechste Eingabe als Antwort weiter.
|
||||||
|
const [projectStates, setProjectStates] = useState<Record<string, CtxState>>({});
|
||||||
|
const [projectQueues, setProjectQueues] = useState<Record<string, QueuedItem[]>>({});
|
||||||
|
const projectStatesRef = useRef<Record<string, CtxState>>({});
|
||||||
|
const projectQueuesRef = useRef<Record<string, QueuedItem[]>>({});
|
||||||
|
// Pro-Projekt-Textfeld-Entwuerfe (noch nicht gesendeter Feldinhalt). Key = pid.
|
||||||
|
const projectDraftsRef = useRef<Record<string, string>>({});
|
||||||
|
const prevFocusedPidRef = useRef<string>('');
|
||||||
|
// Ref-Bruecke, damit der frueh deklarierte RVS-Message-Handler den erst spaeter
|
||||||
|
// deklarierten Queue-Automaten aufrufen kann (ohne TDZ / stale Closure).
|
||||||
|
const queueApiRef = useRef<{
|
||||||
|
getCtxState: (pid: string) => CtxState;
|
||||||
|
setCtxState: (pid: string, s: CtxState) => void;
|
||||||
|
advanceQueue: (pid: string) => void;
|
||||||
|
} | null>(null);
|
||||||
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: ''});
|
||||||
@@ -509,14 +536,39 @@ const ChatScreen: React.FC = () => {
|
|||||||
return () => unsub();
|
return () => unsub();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Focus in Storage spiegeln damit der letzte Kontext nach Neustart wieder
|
// Focus in Storage spiegeln + Pro-Projekt-Textfeld-Entwuerfe wechseln.
|
||||||
// da ist. Kein zwingender UX-Fix (Default = Hauptchat waere auch ok), aber
|
|
||||||
// fuer den Auto-Fall angenehm.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
AsyncStorage.setItem('aria_focused_project_id', focusedProjectId).catch(() => {});
|
AsyncStorage.setItem('aria_focused_project_id', focusedProjectId).catch(() => {});
|
||||||
focusedProjectIdRef.current = focusedProjectId;
|
focusedProjectIdRef.current = focusedProjectId;
|
||||||
|
// Draft-Wechsel: der aktuelle Feldinhalt gehoert dem VERLASSENEN Projekt,
|
||||||
|
// der Entwurf des neuen Projekts wird geladen (leer = leeres Feld).
|
||||||
|
const prevPid = prevFocusedPidRef.current;
|
||||||
|
if (prevPid !== focusedProjectId) {
|
||||||
|
projectDraftsRef.current = { ...projectDraftsRef.current, [prevPid]: inputTextRef.current };
|
||||||
|
setInputText(projectDraftsRef.current[focusedProjectId] || '');
|
||||||
|
prevFocusedPidRef.current = focusedProjectId;
|
||||||
|
AsyncStorage.setItem('aria_project_drafts', JSON.stringify(projectDraftsRef.current)).catch(() => {});
|
||||||
|
}
|
||||||
}, [focusedProjectId]);
|
}, [focusedProjectId]);
|
||||||
|
|
||||||
|
// inputText-Spiegel (damit der Draft-Wechsel oben inputText nicht als Dep braucht).
|
||||||
|
useEffect(() => { inputTextRef.current = inputText; }, [inputText]);
|
||||||
|
|
||||||
|
// Drafts beim Start aus Storage laden.
|
||||||
|
useEffect(() => {
|
||||||
|
AsyncStorage.getItem('aria_project_drafts').then(v => {
|
||||||
|
if (!v) return;
|
||||||
|
try {
|
||||||
|
const map = JSON.parse(v);
|
||||||
|
if (map && typeof map === 'object') {
|
||||||
|
projectDraftsRef.current = map;
|
||||||
|
const cur = map[focusedProjectIdRef.current] || '';
|
||||||
|
if (cur) setInputText(cur);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Ref-Spiegel damit useCallback-Handler die aktuelle Focus-ID lesen
|
// Ref-Spiegel damit useCallback-Handler die aktuelle Focus-ID lesen
|
||||||
// ohne dass wir die Deps in jedes Callback muessen (sonst re-createn
|
// ohne dass wir die Deps in jedes Callback muessen (sonst re-createn
|
||||||
// die sich bei jedem Wechsel).
|
// die sich bei jedem Wechsel).
|
||||||
@@ -1236,6 +1288,18 @@ const ChatScreen: React.FC = () => {
|
|||||||
: prev);
|
: prev);
|
||||||
setAgentActivity(cur =>
|
setAgentActivity(cur =>
|
||||||
cur.activity === 'idle' ? cur : { activity: 'idle', tool: '' });
|
cur.activity === 'idle' ? cur : { activity: 'idle', tool: '' });
|
||||||
|
// ── Pro-Projekt-Queue-Automat: auf ARIAs Antwort reagieren ──
|
||||||
|
const awaiting = !!(message.payload as any).awaiting_reply;
|
||||||
|
const qapi = queueApiRef.current;
|
||||||
|
if (qapi) {
|
||||||
|
const st = qapi.getCtxState(ansPid);
|
||||||
|
// Nur reagieren, wenn wir fuer diesen Kontext wirklich auf eine
|
||||||
|
// Antwort warten (nicht bei unaufgeforderten Trigger-Nachrichten).
|
||||||
|
if (st === 'running' || st === 'awaiting_reply') {
|
||||||
|
if (awaiting) qapi.setCtxState(ansPid, 'awaiting_reply');
|
||||||
|
else qapi.advanceQueue(ansPid);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// ALLE noch laufenden ACK-Timer clearen — Bridge hat unsere Messages
|
// ALLE noch laufenden ACK-Timer clearen — Bridge hat unsere Messages
|
||||||
// ja offensichtlich verarbeitet (sonst keine ARIA-Antwort). Wenn
|
// ja offensichtlich verarbeitet (sonst keine ARIA-Antwort). Wenn
|
||||||
@@ -1972,6 +2036,65 @@ const ChatScreen: React.FC = () => {
|
|||||||
|
|
||||||
// --- Nachricht senden ---
|
// --- Nachricht senden ---
|
||||||
|
|
||||||
|
// ── Pro-Projekt-Queue: Zustands-Helfer ──────────────────────────
|
||||||
|
const getCtxState = useCallback((pid: string): CtxState =>
|
||||||
|
projectStatesRef.current[pid] || 'idle', []);
|
||||||
|
|
||||||
|
const setCtxState = useCallback((pid: string, s: CtxState) => {
|
||||||
|
projectStatesRef.current = { ...projectStatesRef.current, [pid]: s };
|
||||||
|
setProjectStates(prev => ({ ...prev, [pid]: s }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setCtxQueue = useCallback((pid: string, items: QueuedItem[]) => {
|
||||||
|
projectQueuesRef.current = { ...projectQueuesRef.current, [pid]: items };
|
||||||
|
setProjectQueues(prev => ({ ...prev, [pid]: items }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Sendet eine Nachricht WIRKLICH (Location holen + dispatchWithAck) und setzt
|
||||||
|
// den Kontext auf 'running'. existingId gesetzt = eine bereits als pending_queue
|
||||||
|
// angezeigte Bubble wird auf 'sending' gehoben (Dequeue), sonst neue Bubble.
|
||||||
|
const actuallySend = useCallback(async (pid: string, text: string, existingId?: string) => {
|
||||||
|
const cmid = nextClientMsgId();
|
||||||
|
const location = await getCurrentLocation();
|
||||||
|
const status: ChatMessage['deliveryStatus'] =
|
||||||
|
connectionStateRef.current === 'connected' ? 'sending' : 'queued';
|
||||||
|
if (existingId) {
|
||||||
|
setMessages(prev => prev.map(m =>
|
||||||
|
m.id === existingId ? { ...m, clientMsgId: cmid, deliveryStatus: status, sendAttempts: 1 } : m));
|
||||||
|
} else {
|
||||||
|
setMessages(prev => capMessages([...prev, {
|
||||||
|
id: nextId(), sender: 'user', text, timestamp: Date.now(),
|
||||||
|
clientMsgId: cmid, deliveryStatus: status, sendAttempts: 1, projectId: pid,
|
||||||
|
}]));
|
||||||
|
}
|
||||||
|
setCtxState(pid, 'running');
|
||||||
|
dispatchWithAck(cmid, 'chat', {
|
||||||
|
text, voice: localXttsVoiceRef.current, speed: ttsSpeedRef.current,
|
||||||
|
projectId: pid, ...(location && { location }),
|
||||||
|
});
|
||||||
|
}, [getCurrentLocation, dispatchWithAck, setCtxState]);
|
||||||
|
|
||||||
|
// Naechsten Queue-Eintrag von pid abarbeiten (falls vorhanden), sonst idle.
|
||||||
|
const advanceQueue = useCallback((pid: string) => {
|
||||||
|
const q = projectQueuesRef.current[pid] || [];
|
||||||
|
if (q.length === 0) { setCtxState(pid, 'idle'); return; }
|
||||||
|
const [next, ...rest] = q;
|
||||||
|
setCtxQueue(pid, rest);
|
||||||
|
actuallySend(pid, next.text, next.id);
|
||||||
|
}, [actuallySend, setCtxQueue, setCtxState]);
|
||||||
|
|
||||||
|
// Einen wartenden Eintrag aus der Queue entfernen (User tippt auf ✕).
|
||||||
|
const removeQueued = useCallback((pid: string, id: string) => {
|
||||||
|
setCtxQueue(pid, (projectQueuesRef.current[pid] || []).filter(it => it.id !== id));
|
||||||
|
setMessages(prev => prev.filter(m => m.id !== id));
|
||||||
|
}, [setCtxQueue]);
|
||||||
|
|
||||||
|
// Ref-Bruecke fuellen, damit der frueh deklarierte Message-Handler den Automaten
|
||||||
|
// erreicht (ohne TDZ / stale Closure).
|
||||||
|
useEffect(() => {
|
||||||
|
queueApiRef.current = { getCtxState, setCtxState, advanceQueue };
|
||||||
|
}, [getCtxState, setCtxState, advanceQueue]);
|
||||||
|
|
||||||
// Aufraeumen von "verarbeitet"-Placeholder die nie ein STT-Result bekommen
|
// Aufraeumen von "verarbeitet"-Placeholder die nie ein STT-Result bekommen
|
||||||
// haben (leere Aufnahme, Wake-Word-Echo, STT-Fehler etc). Timeout skaliert
|
// haben (leere Aufnahme, Wake-Word-Echo, STT-Fehler etc). Timeout skaliert
|
||||||
// mit der Aufnahmedauer — Whisper braucht auf der Gamebox grob real-time/5,
|
// mit der Aufnahmedauer — Whisper braucht auf der Gamebox grob real-time/5,
|
||||||
@@ -2002,34 +2125,22 @@ const ChatScreen: React.FC = () => {
|
|||||||
setAgentActivityByCtx(prev => ({ ...prev, [pid]: { activity: 'idle', tool: '' } }));
|
setAgentActivityByCtx(prev => ({ ...prev, [pid]: { activity: 'idle', tool: '' } }));
|
||||||
clearStuckWatchdog();
|
clearStuckWatchdog();
|
||||||
rvs.send('cancel_request' as any, { projectId: pid });
|
rvs.send('cancel_request' as any, { projectId: pid });
|
||||||
}, []);
|
// Aktuellen Task abgebrochen → naechsten Queue-Eintrag dieses Projekts
|
||||||
|
// starten (oder idle, wenn leer). Wartende Eintraege einzeln loeschbar.
|
||||||
|
advanceQueue(pid);
|
||||||
|
}, [advanceQueue]);
|
||||||
|
|
||||||
// Barge-In: wenn der User waehrend ARIA arbeitet/spricht eine neue Sprach-
|
// Queue-Modus („immer anstellen"): eine neue Sprachnachricht bricht ARIAs
|
||||||
// Nachricht aufnimmt, alte Aktivitaet sofort abbrechen — TTS verstummen,
|
// laufende Arbeit NICHT mehr ab. Sie wird — wie Text — angestellt und laeuft
|
||||||
// aria-core-Run via cancel_request abbrechen. So kann man "ach vergiss es,
|
// serialisiert (der Brain-Lock pro Projekt reiht /chat-/audio-Turns auf).
|
||||||
// mach lieber X" sagen wie in einem echten Gespraech.
|
// Nur das TTS wird akustisch gestoppt, damit das Mikro ARIAs eigene Stimme
|
||||||
|
// nicht mithoert. Explizites Abbrechen laeuft ueber den Stop-Button
|
||||||
|
// (cancelRequest). Rueckgabe = false, weil kein Barge-In/Interrupt mehr.
|
||||||
const interruptAriaIfBusy = useCallback(() => {
|
const interruptAriaIfBusy = useCallback(() => {
|
||||||
const speaking = audioService.isPlayingAudio();
|
if (audioService.isPlayingAudio()) {
|
||||||
// Multi-Threading: NUR den fokussierten Kontext als "busy" werten — nicht
|
audioService.haltAllPlayback('user startet Aufnahme (Queue-Modus, kein Abbruch)');
|
||||||
// global. Sonst bricht eine Nachricht im Hauptchat die parallele Arbeit in
|
|
||||||
// einem Projekt ab (bzw. wird faelschlich als Barge-In behandelt und die
|
|
||||||
// eigene Anfrage geht unter). Der Busy-Status kommt kontextgenau aus
|
|
||||||
// /projects/queue-status (queueStatusRef). agentActivity ist global und
|
|
||||||
// taugt dafuer nicht.
|
|
||||||
const pid = focusedProjectIdRef.current || '';
|
|
||||||
const focusKey = pid || '__main__';
|
|
||||||
const focusBusy = !!queueStatusRef.current?.[focusKey]?.busy;
|
|
||||||
if (!speaking && !focusBusy) return false;
|
|
||||||
console.log('[Chat] Barge-In: speaking=%s focusBusy=%s (ctx=%s) — interrupting',
|
|
||||||
speaking, focusBusy, focusKey);
|
|
||||||
// TTS immer stoppen wenn ARIA gerade spricht — egal welcher Kontext.
|
|
||||||
if (speaking) audioService.haltAllPlayback('user spricht (barge-in)');
|
|
||||||
// Brain-Arbeit nur abbrechen wenn GENAU dieser Kontext arbeitet.
|
|
||||||
if (focusBusy) {
|
|
||||||
clearStuckWatchdog();
|
|
||||||
rvs.send('cancel_request' as any, { projectId: pid });
|
|
||||||
}
|
}
|
||||||
return true;
|
return false;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Manueller Aufnahme-Knopf (VoiceButton) — Start.
|
// Manueller Aufnahme-Knopf (VoiceButton) — Start.
|
||||||
@@ -2216,37 +2327,37 @@ const ChatScreen: React.FC = () => {
|
|||||||
|
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
setInputText('');
|
|
||||||
|
|
||||||
// Barge-In: laufende ARIA-Aktivitaet abbrechen wenn welche da ist.
|
|
||||||
const wasInterrupted = interruptAriaIfBusy();
|
|
||||||
const location = await getCurrentLocation();
|
|
||||||
|
|
||||||
const cmid = nextClientMsgId();
|
|
||||||
const activePid = focusedProjectIdRef.current;
|
const activePid = focusedProjectIdRef.current;
|
||||||
const userMsg: ChatMessage = {
|
setInputText('');
|
||||||
id: nextId(),
|
// Draft dieses Projekts leeren (wurde ja gerade abgeschickt/angestellt).
|
||||||
sender: 'user',
|
projectDraftsRef.current = { ...projectDraftsRef.current, [activePid]: '' };
|
||||||
text,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
clientMsgId: cmid,
|
|
||||||
deliveryStatus: connectionStateRef.current === 'connected' ? 'sending' : 'queued',
|
|
||||||
sendAttempts: 1,
|
|
||||||
projectId: activePid,
|
|
||||||
};
|
|
||||||
setMessages(prev => capMessages([...prev, userMsg]));
|
|
||||||
|
|
||||||
console.log('[Chat] sende cmid=%s voice=%s speed=%s interrupted=%s project=%s',
|
const focusKey = activePid || '__main__';
|
||||||
cmid, localXttsVoiceRef.current || '(default)', ttsSpeedRef.current, wasInterrupted, activePid || '(main)');
|
const brainBusy = !!queueStatusRef.current?.[focusKey]?.busy;
|
||||||
dispatchWithAck(cmid, 'chat', {
|
let state = getCtxState(activePid);
|
||||||
text,
|
// Fallback: ein per SPRACHE gestarteter Turn streamt live und setzt den
|
||||||
voice: localXttsVoiceRef.current,
|
// App-State nicht — aber der Brain meldet busy. Dann Kontext als 'running'
|
||||||
speed: ttsSpeedRef.current,
|
// behandeln (anstellen; die spaetere Antwort schaltet die Queue weiter).
|
||||||
interrupted: wasInterrupted,
|
if (state === 'idle' && brainBusy) { setCtxState(activePid, 'running'); state = 'running'; }
|
||||||
projectId: activePid,
|
if (state === 'running') {
|
||||||
...(location && { location }),
|
// ARIA arbeitet noch am aktuellen Task → ANSTELLEN statt abbrechen.
|
||||||
});
|
// Sichtbare pending_queue-Bubble; laeuft der Reihe nach, wenn der
|
||||||
}, [inputText, getCurrentLocation, pendingAttachments, sendPendingAttachments, interruptAriaIfBusy, dispatchWithAck]);
|
// aktuelle Task (und ggf. seine Rueckfragen) fertig ist.
|
||||||
|
const id = nextId();
|
||||||
|
setMessages(prev => capMessages([...prev, {
|
||||||
|
id, sender: 'user', text, timestamp: Date.now(),
|
||||||
|
projectId: activePid, deliveryStatus: 'pending_queue',
|
||||||
|
}]));
|
||||||
|
setCtxQueue(activePid, [...(projectQueuesRef.current[activePid] || []), { id, text }]);
|
||||||
|
console.log('[Chat] angestellt (queue) project=%s len=%d', activePid || '(main)',
|
||||||
|
(projectQueuesRef.current[activePid] || []).length);
|
||||||
|
} else {
|
||||||
|
// idle ODER awaiting_reply → sofort senden. Bei awaiting_reply ist DAS die
|
||||||
|
// Antwort auf ARIAs Rueckfrage (normaler Turn in diesem Projekt).
|
||||||
|
console.log('[Chat] sende sofort (state=%s) project=%s', state, activePid || '(main)');
|
||||||
|
actuallySend(activePid, text);
|
||||||
|
}
|
||||||
|
}, [inputText, pendingAttachments, sendPendingAttachments, getCtxState, setCtxState, setCtxQueue, actuallySend]);
|
||||||
|
|
||||||
// --- Rendering ---
|
// --- Rendering ---
|
||||||
|
|
||||||
@@ -2546,7 +2657,16 @@ const ChatScreen: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : null}
|
) : null}
|
||||||
{isUser && item.deliveryStatus ? (
|
{isUser && item.deliveryStatus ? (
|
||||||
item.deliveryStatus === 'failed' && item.clientMsgId ? (
|
item.deliveryStatus === 'pending_queue' ? (
|
||||||
|
// Wartet in der Projekt-Queue → tippen entfernt den Eintrag.
|
||||||
|
<TouchableOpacity
|
||||||
|
hitSlop={{top:6,bottom:6,left:6,right:6}}
|
||||||
|
onPress={() => removeQueued(item.projectId || '', item.id)}
|
||||||
|
accessibilityLabel="Aus Warteschlange entfernen"
|
||||||
|
>
|
||||||
|
<Text style={styles.statusQueued}>{'⏸ wartet · ✕'}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : item.deliveryStatus === 'failed' && item.clientMsgId ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
hitSlop={{top:6,bottom:6,left:6,right:6}}
|
hitSlop={{top:6,bottom:6,left:6,right:6}}
|
||||||
onPress={() => retryFailedMessage(item.clientMsgId!)}
|
onPress={() => retryFailedMessage(item.clientMsgId!)}
|
||||||
@@ -2969,6 +3089,18 @@ const ChatScreen: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Rueckfrage-Banner / Queue-Zaehler fuer den fokussierten Kontext */}
|
||||||
|
{(projectStates[focusedProjectId] === 'awaiting_reply' ||
|
||||||
|
(projectQueues[focusedProjectId]?.length || 0) > 0) && (
|
||||||
|
<View style={styles.queueBanner}>
|
||||||
|
<Text style={styles.queueBannerText}>
|
||||||
|
{projectStates[focusedProjectId] === 'awaiting_reply'
|
||||||
|
? '❓ ARIA fragt nach — deine Eingabe beantwortet das'
|
||||||
|
: `⏸ ${projectQueues[focusedProjectId]?.length || 0} in der Warteschlange`}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Eingabebereich */}
|
{/* Eingabebereich */}
|
||||||
<View style={styles.inputContainer}>
|
<View style={styles.inputContainer}>
|
||||||
{/* Datei-Buttons */}
|
{/* Datei-Buttons */}
|
||||||
@@ -3466,6 +3598,19 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
},
|
},
|
||||||
|
queueBanner: {
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 6,
|
||||||
|
backgroundColor: '#2A2410',
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: '#4A3F14',
|
||||||
|
},
|
||||||
|
queueBannerText: {
|
||||||
|
color: '#FFD60A',
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: '600',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
inputContainer: {
|
inputContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'flex-end',
|
alignItems: 'flex-end',
|
||||||
|
|||||||
+45
-4
@@ -1164,6 +1164,22 @@ def _claims_live_media_state(text: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
_AWAIT_MARKER_RE = re.compile(r"\[\[\s*AWAIT(?:_REPLY)?\s*\]\]", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_await_marker(text: str) -> tuple:
|
||||||
|
"""Erkennt ARIAs Rueckfrage-Marker `[[AWAIT]]` — den sie ans Ende haengt,
|
||||||
|
wenn ihre Antwort eine echte Rueckfrage ist, auf die sie eine Nutzer-Antwort
|
||||||
|
BRAUCHT, bevor der aktuelle Task fertig ist. Entfernt den Marker (nicht
|
||||||
|
anzeigen/vorlesen/in History) und meldet, ob er da war. Wie speak/converse:
|
||||||
|
ARIA deklariert den Zustand selbst — kein '?'-Raten."""
|
||||||
|
if not text:
|
||||||
|
return text, False
|
||||||
|
if _AWAIT_MARKER_RE.search(text):
|
||||||
|
return _AWAIT_MARKER_RE.sub("", text).strip(), True
|
||||||
|
return text, False
|
||||||
|
|
||||||
|
|
||||||
def _normalize_for_fast_match(text: str) -> str:
|
def _normalize_for_fast_match(text: str) -> str:
|
||||||
norm = _strip_leading_hint_blocks(text).lower()
|
norm = _strip_leading_hint_blocks(text).lower()
|
||||||
norm = _fold_umlauts(norm)
|
norm = _fold_umlauts(norm)
|
||||||
@@ -1588,7 +1604,8 @@ class Agent:
|
|||||||
speak = bool(getattr(self, "_fast_path_speak", False))
|
speak = bool(getattr(self, "_fast_path_speak", False))
|
||||||
# converse folgt dem Skill (Manifest/Output) — nicht mehr generell False.
|
# converse folgt dem Skill (Manifest/Output) — nicht mehr generell False.
|
||||||
converse = bool(getattr(self, "_fast_path_converse", False))
|
converse = bool(getattr(self, "_fast_path_converse", False))
|
||||||
return fast_reply, "fast-path", speak, converse
|
# Fast-Path = reiner Steuerbefehl, nie eine Rueckfrage → awaiting=False.
|
||||||
|
return fast_reply, "fast-path", speak, converse, False
|
||||||
|
|
||||||
# 1. User-Turn an die Konversation
|
# 1. User-Turn an die Konversation
|
||||||
self.conversation.add("user", user_message, source=source,
|
self.conversation.add("user", user_message, source=source,
|
||||||
@@ -1606,7 +1623,8 @@ class Agent:
|
|||||||
# dem Skill (bzw. Default: Info/Gespraech = vorlesen + 30s).
|
# dem Skill (bzw. Default: Info/Gespraech = vorlesen + 30s).
|
||||||
speak = getattr(self, "_local_turn_speak", True)
|
speak = getattr(self, "_local_turn_speak", True)
|
||||||
converse = getattr(self, "_local_turn_converse", True)
|
converse = getattr(self, "_local_turn_converse", True)
|
||||||
return local_reply, "local", speak, converse
|
# Local ist tool-loses Reden; blockierende Rueckfragen macht Claude.
|
||||||
|
return local_reply, "local", speak, converse, False
|
||||||
|
|
||||||
# 2. Hot Memory (alle pinned Punkte)
|
# 2. Hot Memory (alle pinned Punkte)
|
||||||
hot = self.store.list_pinned()
|
hot = self.store.list_pinned()
|
||||||
@@ -1653,6 +1671,23 @@ class Agent:
|
|||||||
oauth_callback_host=oauth_host,
|
oauth_callback_host=oauth_host,
|
||||||
oauth_callback_port=oauth_port,
|
oauth_callback_port=oauth_port,
|
||||||
oauth_callback_tls=oauth_tls)
|
oauth_callback_tls=oauth_tls)
|
||||||
|
# Rueckfrage-Signal: ARIA haengt [[AWAIT]] an, wenn ihre Antwort eine
|
||||||
|
# echte, blockierende Rueckfrage ist. Die App pausiert dann die Projekt-
|
||||||
|
# Queue und leitet Stefans naechste Eingabe als ANTWORT darauf weiter
|
||||||
|
# (statt als neuen Auftrag). Wie speak/converse: ARIA deklariert selbst.
|
||||||
|
system_prompt += (
|
||||||
|
"\n\n## RUECKFRAGE-SIGNAL [[AWAIT]]\n"
|
||||||
|
"Wenn deine Antwort eine echte RUECKFRAGE ist, auf die du Stefans "
|
||||||
|
"Antwort BRAUCHST, um den aktuellen Task abzuschliessen (z.B. 'Welche "
|
||||||
|
"der drei Playlists meinst du?', 'Soll ich X oder Y nehmen?'), haenge "
|
||||||
|
"als ALLERLETZTES exakt `[[AWAIT]]` an. Der Marker wird entfernt (nicht "
|
||||||
|
"angezeigt, nicht vorgelesen) und sagt der App: warte auf Stefans "
|
||||||
|
"Antwort, bevor der naechste Task der Warteschlange laeuft.\n"
|
||||||
|
"NUR bei echten, blockierenden Rueckfragen — NICHT bei rhetorischen "
|
||||||
|
"Fragen, unverbindlichen Vorschlaegen ('soll ich noch...?', die auch "
|
||||||
|
"ohne Antwort ok sind) oder wenn du den Task einfach fertig hast. Im "
|
||||||
|
"Zweifel: KEIN Marker."
|
||||||
|
)
|
||||||
# Queue-Aware Prompting: wenn nach diesem Turn weitere Nachrichten
|
# Queue-Aware Prompting: wenn nach diesem Turn weitere Nachrichten
|
||||||
# in der Warteschlange liegen, muss ARIA pruefen ob eine spaetere die
|
# in der Warteschlange liegen, muss ARIA pruefen ob eine spaetere die
|
||||||
# aktuelle Aufgabe korrigiert/annuliert (→ Skip statt Doppelarbeit).
|
# aktuelle Aufgabe korrigiert/annuliert (→ Skip statt Doppelarbeit).
|
||||||
@@ -1817,13 +1852,19 @@ class Agent:
|
|||||||
final_reply = ("Hey, ich bin ARIA. \U0001F60A Bei mir ist alles bereit — "
|
final_reply = ("Hey, ich bin ARIA. \U0001F60A Bei mir ist alles bereit — "
|
||||||
"sag mir einfach, was du brauchst.")
|
"sag mir einfach, was du brauchst.")
|
||||||
|
|
||||||
|
# Rueckfrage-Marker aus dem finalen Text ziehen (vor History/Return, damit
|
||||||
|
# er nicht angezeigt/vorgelesen wird und nicht die Conversation vergiftet).
|
||||||
|
final_reply, awaiting_reply = _extract_await_marker(final_reply)
|
||||||
|
|
||||||
# 7. Assistant-Turn (final reply) in die Conversation
|
# 7. Assistant-Turn (final reply) in die Conversation
|
||||||
self.conversation.add("assistant", final_reply,
|
self.conversation.add("assistant", final_reply,
|
||||||
project_id=active_project_id)
|
project_id=active_project_id)
|
||||||
# speak/converse folgen dem ausgefuehrten Skill (sonst Default: Gespraech).
|
# speak/converse folgen dem ausgefuehrten Skill (sonst Default: Gespraech);
|
||||||
|
# awaiting_reply = ARIA stellt eine blockierende Rueckfrage (Queue pausiert).
|
||||||
return (final_reply, "claude",
|
return (final_reply, "claude",
|
||||||
bool(getattr(self, "_claude_turn_speak", True)),
|
bool(getattr(self, "_claude_turn_speak", True)),
|
||||||
bool(getattr(self, "_claude_turn_converse", True)))
|
bool(getattr(self, "_claude_turn_converse", True)),
|
||||||
|
awaiting_reply)
|
||||||
|
|
||||||
# ── Tool-Dispatcher ───────────────────────────────────────
|
# ── Tool-Dispatcher ───────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ async def _fire(trigger: dict, agent_factory) -> None:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
agent = agent_factory()
|
agent = agent_factory()
|
||||||
reply, _, _, _ = agent.chat(prompt, source="trigger")
|
reply, _, _, _, _ = agent.chat(prompt, source="trigger")
|
||||||
events = agent.pop_events()
|
events = agent.pop_events()
|
||||||
logger.info("[trigger] %s gefeuert → ARIA-Reply: %s", name, reply[:80])
|
logger.info("[trigger] %s gefeuert → ARIA-Reply: %s", name, reply[:80])
|
||||||
triggers_mod.append_log(name, {"event": "reply", "text": reply[:500]})
|
triggers_mod.append_log(name, {"event": "reply", "text": reply[:500]})
|
||||||
|
|||||||
+6
-1
@@ -639,6 +639,10 @@ class ChatOut(BaseModel):
|
|||||||
# Soll die App nach der Antwort 30s weiterlauschen (Gespraech)? Einzelaktionen/
|
# Soll die App nach der Antwort 30s weiterlauschen (Gespraech)? Einzelaktionen/
|
||||||
# Skills = False (direkt zurueck aufs Wake-Word), Konversation = True.
|
# Skills = False (direkt zurueck aufs Wake-Word), Konversation = True.
|
||||||
converse: bool = True
|
converse: bool = True
|
||||||
|
# Stellt ARIA eine blockierende Rueckfrage (braucht Stefans Antwort, bevor der
|
||||||
|
# Task fertig ist)? Dann pausiert die App die Projekt-Queue und leitet die
|
||||||
|
# naechste Eingabe als Antwort weiter, statt sie als neuen Auftrag anzustellen.
|
||||||
|
awaiting_reply: bool = False
|
||||||
# Echo der project_id die dieser Turn hatte. Bridge nutzt sie damit die
|
# Echo der project_id die dieser Turn hatte. Bridge nutzt sie damit die
|
||||||
# ausgehende Chat-Bubble sauber getaggt in der richtigen Thread-Bahn der
|
# ausgehende Chat-Bubble sauber getaggt in der richtigen Thread-Bahn der
|
||||||
# UI landet.
|
# UI landet.
|
||||||
@@ -722,7 +726,7 @@ async def chat(body: ChatIn, background: BackgroundTasks):
|
|||||||
# Sync-Aufruf im Executor damit wir den Event-Loop nicht blocken —
|
# Sync-Aufruf im Executor damit wir den Event-Loop nicht blocken —
|
||||||
# chat() macht HTTP-Calls (Proxy) die 30-60s dauern koennen.
|
# chat() macht HTTP-Calls (Proxy) die 30-60s dauern koennen.
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
reply, answered_by, speak, converse = await loop.run_in_executor(
|
reply, answered_by, speak, converse, awaiting_reply = await loop.run_in_executor(
|
||||||
None,
|
None,
|
||||||
lambda: a.chat(
|
lambda: a.chat(
|
||||||
body.message, source=body.source, project_id=pid,
|
body.message, source=body.source, project_id=pid,
|
||||||
@@ -747,6 +751,7 @@ async def chat(body: ChatIn, background: BackgroundTasks):
|
|||||||
answered_by=answered_by,
|
answered_by=answered_by,
|
||||||
speak=speak,
|
speak=speak,
|
||||||
converse=converse,
|
converse=converse,
|
||||||
|
awaiting_reply=awaiting_reply,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
_project_pending[pid] = [
|
_project_pending[pid] = [
|
||||||
|
|||||||
@@ -1585,6 +1585,10 @@ class ARIABridge:
|
|||||||
# Nach dieser Antwort 30s weiterlauschen? Steuert das Gespraechs-
|
# Nach dieser Antwort 30s weiterlauschen? Steuert das Gespraechs-
|
||||||
# Fenster in der App (Skill/Einzelaktion=False, Konversation=True).
|
# Fenster in der App (Skill/Einzelaktion=False, Konversation=True).
|
||||||
"converse": bool(payload.get("converse", True)) if isinstance(payload, dict) else True,
|
"converse": bool(payload.get("converse", True)) if isinstance(payload, dict) else True,
|
||||||
|
# Stellt ARIA eine blockierende Rueckfrage? Dann pausiert die App
|
||||||
|
# die Projekt-Queue und leitet die naechste Eingabe als Antwort auf
|
||||||
|
# DIESE Rueckfrage weiter, statt sie als neuen Auftrag anzustellen.
|
||||||
|
"awaiting_reply": bool(payload.get("awaiting_reply", False)) if isinstance(payload, dict) else False,
|
||||||
},
|
},
|
||||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||||
})
|
})
|
||||||
@@ -1948,6 +1952,9 @@ class ARIABridge:
|
|||||||
# Nach der Antwort 30s weiterlauschen (Gespraech) oder direkt zurueck aufs
|
# Nach der Antwort 30s weiterlauschen (Gespraech) oder direkt zurueck aufs
|
||||||
# Wake-Word? Einzelaktionen/Skills = False. Reicht die App aus.
|
# Wake-Word? Einzelaktionen/Skills = False. Reicht die App aus.
|
||||||
converse = data.get("converse", True)
|
converse = data.get("converse", True)
|
||||||
|
# Stellt ARIA eine blockierende Rueckfrage? Dann pausiert die App die
|
||||||
|
# Projekt-Queue und leitet die naechste Eingabe als Antwort weiter.
|
||||||
|
awaiting_reply = bool(data.get("awaiting_reply", False))
|
||||||
|
|
||||||
# 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
|
||||||
@@ -2017,7 +2024,8 @@ class ARIABridge:
|
|||||||
await self._process_core_response(reply, {"projectId": turn_project_id,
|
await self._process_core_response(reply, {"projectId": turn_project_id,
|
||||||
"answeredBy": answered_by,
|
"answeredBy": answered_by,
|
||||||
"speak": speak,
|
"speak": speak,
|
||||||
"converse": converse})
|
"converse": converse,
|
||||||
|
"awaiting_reply": awaiting_reply})
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("[brain] _process_core_response Fehler")
|
logger.exception("[brain] _process_core_response Fehler")
|
||||||
await self._emit_activity("idle", "", project_id=project_id)
|
await self._emit_activity("idle", "", project_id=project_id)
|
||||||
|
|||||||
+89
-7
@@ -320,6 +320,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="diag-pending-attachments" style="display:none;padding:6px 10px;background:#1E1E2E;border-radius:6px 6px 0 0;margin-bottom:-4px;display:flex;gap:6px;flex-wrap:wrap;align-items:center;">
|
<div id="diag-pending-attachments" style="display:none;padding:6px 10px;background:#1E1E2E;border-radius:6px 6px 0 0;margin-bottom:-4px;display:flex;gap:6px;flex-wrap:wrap;align-items:center;">
|
||||||
</div>
|
</div>
|
||||||
|
<div id="diag-queue-banner" style="display:none;padding:6px 10px;background:#2A2410;border:1px solid #4A3F14;border-radius:6px;margin-bottom:6px;"></div>
|
||||||
<div class="input-row">
|
<div class="input-row">
|
||||||
<label class="btn secondary" style="padding:6px 10px;cursor:pointer;font-size:14px;" title="Datei anhaengen">
|
<label class="btn secondary" style="padding:6px 10px;cursor:pointer;font-size:14px;" title="Datei anhaengen">
|
||||||
📎
|
📎
|
||||||
@@ -1425,6 +1426,12 @@
|
|||||||
let diagQueueStatus = {};
|
let diagQueueStatus = {};
|
||||||
let diagProjectsCache = [];
|
let diagProjectsCache = [];
|
||||||
let diagShowHiddenStrip = false; // versteckte Projekte im Streifen zeigen?
|
let diagShowHiddenStrip = false; // versteckte Projekte im Streifen zeigen?
|
||||||
|
// ── Pro-Kontext-Queue + Zustandsautomat (spiegelt die App) ──
|
||||||
|
const diagCtxStates = {}; // pid → 'idle' | 'running' | 'awaiting_reply'
|
||||||
|
const diagCtxQueues = {}; // pid → [{id,text}]
|
||||||
|
const diagDrafts = JSON.parse(localStorage.getItem('diag_ctx_drafts') || '{}');
|
||||||
|
let diagQid = 0;
|
||||||
|
const diagState = (pid) => diagCtxStates[pid] || 'idle';
|
||||||
|
|
||||||
function updateChatVisibilityByFocus() {
|
function updateChatVisibilityByFocus() {
|
||||||
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
|
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
|
||||||
@@ -1438,10 +1445,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function switchDiagFocus(id) {
|
function switchDiagFocus(id) {
|
||||||
focusedContextId = id || '';
|
const nextId = id || '';
|
||||||
|
// Pro-Kontext-Textfeld-Entwuerfe: Feldinhalt dem verlassenen Kontext
|
||||||
|
// zuordnen, Entwurf des neuen laden.
|
||||||
|
if (nextId !== focusedContextId) {
|
||||||
|
const input = document.getElementById('chat-input');
|
||||||
|
if (input) {
|
||||||
|
diagDrafts[focusedContextId] = input.value;
|
||||||
|
input.value = diagDrafts[nextId] || '';
|
||||||
|
}
|
||||||
|
localStorage.setItem('diag_ctx_drafts', JSON.stringify(diagDrafts));
|
||||||
|
}
|
||||||
|
focusedContextId = nextId;
|
||||||
localStorage.setItem('diag_focused_context_id', focusedContextId);
|
localStorage.setItem('diag_focused_context_id', focusedContextId);
|
||||||
updateChatVisibilityByFocus();
|
updateChatVisibilityByFocus();
|
||||||
renderContextStrip();
|
renderContextStrip();
|
||||||
|
renderDiagQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderContextStrip() {
|
function renderContextStrip() {
|
||||||
@@ -1983,6 +2002,15 @@
|
|||||||
projectId: p.projectId || '',
|
projectId: p.projectId || '',
|
||||||
answeredBy: p.answeredBy || '',
|
answeredBy: p.answeredBy || '',
|
||||||
});
|
});
|
||||||
|
// ── Pro-Kontext-Queue-Automat: auf ARIAs Antwort reagieren ──
|
||||||
|
if (sender === 'aria') {
|
||||||
|
const apid = p.projectId || '';
|
||||||
|
const st = diagState(apid);
|
||||||
|
if (st === 'running' || st === 'awaiting_reply') {
|
||||||
|
if (p.awaiting_reply) { diagCtxStates[apid] = 'awaiting_reply'; renderDiagQueue(); }
|
||||||
|
else advanceDiagQueue(apid);
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (msg.type === 'chat_message_deleted') {
|
if (msg.type === 'chat_message_deleted') {
|
||||||
@@ -2179,16 +2207,64 @@
|
|||||||
renderDiagPending();
|
renderDiagPending();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Pro-Kontext-Queue: Helfer (spiegelt die App) ──
|
||||||
|
function actuallySendDiag(pid, text) {
|
||||||
|
addChat('sent', text, 'via RVS', { projectId: pid });
|
||||||
|
send({ action: 'test_rvs', text, projectId: pid });
|
||||||
|
diagCtxStates[pid] = 'running';
|
||||||
|
renderDiagQueue();
|
||||||
|
}
|
||||||
|
function advanceDiagQueue(pid) {
|
||||||
|
const q = diagCtxQueues[pid] || [];
|
||||||
|
if (q.length === 0) { diagCtxStates[pid] = 'idle'; renderDiagQueue(); return; }
|
||||||
|
const next = q.shift();
|
||||||
|
actuallySendDiag(pid, next.text);
|
||||||
|
}
|
||||||
|
function enqueueOrSendDiag(pid, text) {
|
||||||
|
if (diagState(pid) === 'running') {
|
||||||
|
(diagCtxQueues[pid] = diagCtxQueues[pid] || []).push({ id: 'q' + (++diagQid), text });
|
||||||
|
renderDiagQueue();
|
||||||
|
} else {
|
||||||
|
// idle ODER awaiting_reply → sofort (bei awaiting = Antwort auf Rueckfrage).
|
||||||
|
actuallySendDiag(pid, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function removeDiagQueuedFocused(id) {
|
||||||
|
const pid = focusedContextId;
|
||||||
|
diagCtxQueues[pid] = (diagCtxQueues[pid] || []).filter(it => it.id !== id);
|
||||||
|
renderDiagQueue();
|
||||||
|
}
|
||||||
|
function renderDiagQueue() {
|
||||||
|
const el = document.getElementById('diag-queue-banner');
|
||||||
|
if (!el) return;
|
||||||
|
const st = diagState(focusedContextId);
|
||||||
|
const q = diagCtxQueues[focusedContextId] || [];
|
||||||
|
if (st !== 'awaiting_reply' && q.length === 0) { el.style.display = 'none'; el.innerHTML = ''; return; }
|
||||||
|
el.style.display = 'block';
|
||||||
|
let html = '';
|
||||||
|
if (st === 'awaiting_reply') {
|
||||||
|
html += '<div style="color:#FFD60A;font-weight:600;margin-bottom:4px;">❓ ARIA fragt nach — deine Eingabe beantwortet das</div>';
|
||||||
|
}
|
||||||
|
if (q.length) {
|
||||||
|
html += q.map(it =>
|
||||||
|
'<span style="display:inline-block;background:#1E1E2E;border:1px solid #4A3F14;border-radius:10px;padding:2px 8px;margin:2px;font-size:12px;color:#FFD60A;">⏸ ' +
|
||||||
|
escapeHtml(it.text.slice(0, 40)) +
|
||||||
|
' <a href="#" style="color:#FF6B6B;text-decoration:none;" onclick="removeDiagQueuedFocused(\'' + it.id + '\');return false;">✕</a></span>'
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
el.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
function testRVS() {
|
function testRVS() {
|
||||||
const input = document.getElementById('chat-input');
|
const input = document.getElementById('chat-input');
|
||||||
const text = input.value.trim();
|
const text = input.value.trim();
|
||||||
if (!text && diagPendingFiles.length === 0) return;
|
if (!text && diagPendingFiles.length === 0) return;
|
||||||
if (diagPendingFiles.length > 0) sendDiagAttachments();
|
if (diagPendingFiles.length > 0) sendDiagAttachments();
|
||||||
if (text) {
|
if (text) {
|
||||||
// Multi-Threading: mit fokussierter Kontext-ID senden.
|
// Draft dieses Kontexts leeren + Queue-Automat entscheidet senden/anstellen.
|
||||||
// Bridge routet an /chat body.project_id — Brain queued per Kontext.
|
diagDrafts[focusedContextId] = '';
|
||||||
addChat('sent', text, 'via RVS', { projectId: focusedContextId });
|
localStorage.setItem('diag_ctx_drafts', JSON.stringify(diagDrafts));
|
||||||
send({ action: 'test_rvs', text, projectId: focusedContextId });
|
enqueueOrSendDiag(focusedContextId, text);
|
||||||
}
|
}
|
||||||
input.value = '';
|
input.value = '';
|
||||||
}
|
}
|
||||||
@@ -2675,8 +2751,8 @@
|
|||||||
const input = document.getElementById('chat-input-fs');
|
const input = document.getElementById('chat-input-fs');
|
||||||
const text = input.value.trim();
|
const text = input.value.trim();
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
addChat('sent', text, 'via RVS');
|
// Ueber denselben Queue-Automaten wie der Haupt-Chat (fokussierter Kontext).
|
||||||
send({ action: 'test_rvs', text });
|
enqueueOrSendDiag(focusedContextId, text);
|
||||||
input.value = '';
|
input.value = '';
|
||||||
}
|
}
|
||||||
// Escape schliesst Vollbild-Chat
|
// Escape schliesst Vollbild-Chat
|
||||||
@@ -3650,6 +3726,12 @@
|
|||||||
}
|
}
|
||||||
document.getElementById('chat-input').addEventListener('keydown', (e) => chatInputKeydown(e, testRVS));
|
document.getElementById('chat-input').addEventListener('keydown', (e) => chatInputKeydown(e, testRVS));
|
||||||
document.getElementById('chat-input-fs').addEventListener('keydown', (e) => chatInputKeydown(e, testRVSFS));
|
document.getElementById('chat-input-fs').addEventListener('keydown', (e) => chatInputKeydown(e, testRVSFS));
|
||||||
|
// Entwurf des zuletzt fokussierten Kontexts ins Feld laden + Queue-Banner init.
|
||||||
|
(function initDiagDraft() {
|
||||||
|
const input = document.getElementById('chat-input');
|
||||||
|
if (input && diagDrafts[focusedContextId]) input.value = diagDrafts[focusedContextId];
|
||||||
|
renderDiagQueue();
|
||||||
|
})();
|
||||||
|
|
||||||
// Escape schliesst Lightbox
|
// Escape schliesst Lightbox
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user