Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b2c552a88 | |||
| f51ad1547d | |||
| 2a2700907c | |||
| 93ecbf6c43 |
@@ -79,8 +79,8 @@ android {
|
||||
applicationId "com.ariacockpit"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 10905
|
||||
versionName "0.1.9.5"
|
||||
versionCode 10907
|
||||
versionName "0.1.9.7"
|
||||
// Fallback fuer Libraries mit Product Flavors
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aria-cockpit",
|
||||
"version": "0.1.9.5",
|
||||
"version": "0.1.9.7",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -769,6 +769,7 @@ const ChatScreen: React.FC = () => {
|
||||
timestamp: m.ts || Date.now(),
|
||||
attachments: attachments.length ? attachments : undefined,
|
||||
backupTs: typeof m.ts === 'number' ? m.ts : undefined,
|
||||
projectId: typeof m.project_id === 'string' ? m.project_id : '',
|
||||
...(cmid && { clientMsgId: cmid }),
|
||||
// Server-Bubble = vom Brain verarbeitet → 'delivered' (✓✓)
|
||||
...(role === 'user' && cmid && { deliveryStatus: 'delivered' as const }),
|
||||
@@ -1628,39 +1629,71 @@ const ChatScreen: React.FC = () => {
|
||||
[messages],
|
||||
);
|
||||
|
||||
// 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.
|
||||
// Projekt-Bloecke: alle Nachrichten eines Projekts erscheinen als EIN Block
|
||||
// — auch wenn der User zwischenzeitlich raus + wieder rein gegangen ist.
|
||||
// Dafuer ordnen wir die Nachrichten so um, dass alle Messages eines Projekts
|
||||
// contiguous werden, verankert am LETZTEN Aktivitaets-Timestamp des Projekts.
|
||||
// Hauptchat-Nachrichten bleiben chronologisch dazwischen.
|
||||
const [collapsedProjects, setCollapsedProjects] = useState<Set<string>>(new Set());
|
||||
|
||||
const reorderedMessages = useMemo(() => {
|
||||
// Hauptchat-Nachrichten + Projekt-Gruppen trennen
|
||||
const hauptchat: ChatMessage[] = [];
|
||||
const groups = new Map<string, ChatMessage[]>();
|
||||
for (const m of chatVisibleMessages) {
|
||||
const pid = m.projectId || '';
|
||||
if (!pid) hauptchat.push(m);
|
||||
else {
|
||||
const g = groups.get(pid);
|
||||
if (g) g.push(m); else groups.set(pid, [m]);
|
||||
}
|
||||
}
|
||||
// Events: jede Hauptchat-Bubble + jede Projekt-Gruppe als 1 Event
|
||||
type Event =
|
||||
| { ts: number; kind: 'msg'; m: ChatMessage }
|
||||
| { ts: number; kind: 'group'; msgs: ChatMessage[] };
|
||||
const events: Event[] = [];
|
||||
for (const m of hauptchat) events.push({ ts: m.timestamp, kind: 'msg', m });
|
||||
for (const [, msgs] of groups) {
|
||||
const sorted = [...msgs].sort((a, b) => a.timestamp - b.timestamp);
|
||||
const anchorTs = sorted[sorted.length - 1].timestamp;
|
||||
events.push({ ts: anchorTs, kind: 'group', msgs: sorted });
|
||||
}
|
||||
events.sort((a, b) => a.ts - b.ts);
|
||||
const out: ChatMessage[] = [];
|
||||
for (const e of events) {
|
||||
if (e.kind === 'msg') out.push(e.m);
|
||||
else out.push(...e.msgs);
|
||||
}
|
||||
return out;
|
||||
}, [chatVisibleMessages]);
|
||||
|
||||
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) {
|
||||
for (const m of reorderedMessages) {
|
||||
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]);
|
||||
}, [reorderedMessages]);
|
||||
|
||||
// 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 => {
|
||||
return reorderedMessages.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]);
|
||||
}, [reorderedMessages, collapsedProjects, projectMeta]);
|
||||
|
||||
// Auto-Collapse beim Projekt-Wechsel: altes Projekt einklappen, neues aufklappen.
|
||||
const prevActiveIdRef = useRef<string>('');
|
||||
|
||||
+49
-14
@@ -1005,17 +1005,10 @@ class ARIABridge:
|
||||
cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
|
||||
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."""
|
||||
def _tag_file_to_project(self, file_path: str, project_id: str) -> None:
|
||||
"""Schreibt file_path → project_id in /shared/config/file_projects.json.
|
||||
Best-effort, fail-silent. project_id leer = Eintrag entfernen (Hauptchat)."""
|
||||
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:
|
||||
@@ -1027,15 +1020,35 @@ class ARIABridge:
|
||||
manifest = {}
|
||||
except Exception:
|
||||
manifest = {}
|
||||
manifest[file_path] = active_id
|
||||
if project_id:
|
||||
manifest[file_path] = project_id
|
||||
else:
|
||||
manifest.pop(file_path, None)
|
||||
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)
|
||||
logger.info("[file-project] %s → %s", file_path, project_id or "(main)")
|
||||
except Exception as exc:
|
||||
logger.warning("[file-project] tag failed (%s): %s", file_path, exc)
|
||||
|
||||
def _tag_file_to_active_project(self, file_path: str) -> None:
|
||||
"""Convenience: Brain nach aktivem Projekt fragen + taggen.
|
||||
Wird vom App-Upload-Handler genutzt (dort wissen wir die Projekt-ID
|
||||
noch nicht aus dem Payload — Stefan kann ja zwischen App-Upload und
|
||||
Chat-Send das Projekt gewechselt haben). ARIA-eigene Dateien gehen
|
||||
ueber _tag_file_to_project mit turn_project_id direkt."""
|
||||
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
|
||||
self._tag_file_to_project(file_path, active_id)
|
||||
except Exception as exc:
|
||||
logger.warning("[file-project] active-query failed (%s): %s", file_path, exc)
|
||||
|
||||
async def _broadcast_aria_file(self, file_info: dict) -> None:
|
||||
"""ARIA hat eine Datei fuer den User erstellt — App+Diagnostic informieren."""
|
||||
logger.info("[rvs] ARIA-Datei rausgeben: %s (%s, %dKB)",
|
||||
@@ -1194,7 +1207,15 @@ class ARIABridge:
|
||||
# Der Marker wird aus dem Antworttext entfernt (TTS soll ihn nicht
|
||||
# vorlesen) und parallel als file_from_aria-Event geschickt.
|
||||
text, aria_files, missing_files = self._extract_file_markers(text)
|
||||
# ARIA-Dateien dem aktiven Projekt zuordnen (falls eines aktiv war).
|
||||
# turn_project_id kommt vom Brain mit dem /chat-Response und reflektiert
|
||||
# den Stand NACH dem Turn — passt fuer Dateien die ARIA waehrend des
|
||||
# Turns geschrieben hat (sie sind „im selben Projekt entstanden").
|
||||
turn_pid = (payload.get("projectId") or "").strip() if isinstance(payload, dict) else ""
|
||||
for f in aria_files:
|
||||
server_path = f.get("serverPath")
|
||||
if turn_pid and server_path:
|
||||
self._tag_file_to_project(server_path, turn_pid)
|
||||
await self._broadcast_aria_file(f)
|
||||
# Bei fehlenden Files: User informieren (sonst sieht er nur stille
|
||||
# Verluste — ARIA hat den Marker hingeschrieben aber das File nicht
|
||||
@@ -1216,6 +1237,7 @@ class ARIABridge:
|
||||
"text": display_text,
|
||||
"files": [{"serverPath": f["serverPath"], "name": f["name"],
|
||||
"mimeType": f["mimeType"], "size": f["size"]} for f in aria_files],
|
||||
"project_id": turn_pid,
|
||||
})
|
||||
|
||||
metadata = payload.get("metadata", {})
|
||||
@@ -1488,7 +1510,9 @@ class ARIABridge:
|
||||
asyncio.create_task(self.send_to_core(text, source="app-file+chat"))
|
||||
return True
|
||||
|
||||
async def send_to_core(self, text: str, source: str = "bridge", client_msg_id: Optional[str] = None) -> None:
|
||||
async def send_to_core(self, text: str, source: str = "bridge",
|
||||
client_msg_id: Optional[str] = None,
|
||||
project_id: str = "") -> None:
|
||||
"""Sendet Text an aria-brain (HTTP /chat) und broadcastet die Antwort.
|
||||
|
||||
Nicht-Streaming: wir warten bis Brain fertig ist, dann pushen wir
|
||||
@@ -1499,15 +1523,25 @@ class ARIABridge:
|
||||
brain_url = os.environ.get("BRAIN_URL", "http://aria-brain:8080")
|
||||
url = f"{brain_url}/chat"
|
||||
payload = json.dumps({"message": text, "source": source}).encode("utf-8")
|
||||
logger.info("[brain] chat ← %s '%s'", source, text[:80])
|
||||
logger.info("[brain] chat ← %s '%s' project=%s", source, text[:80], project_id or "(main)")
|
||||
|
||||
# User-Nachricht in chat_backup.jsonl loggen — wird beim App-Reconnect
|
||||
# / Diagnostic-Reload als History-Quelle gelesen. clientMsgId speichern
|
||||
# damit die App beim chat_history_response ihre lokale Bubble
|
||||
# dedupen kann (sonst verschwindet sie nach Offline→Online-Race).
|
||||
# project_id: pre-turn-State (was App geschickt hat). Wenn leer, vom
|
||||
# Brain-Status nachholen — z.B. bei Trigger-Replies oder Diagnostic-Send.
|
||||
entry: dict = {"role": "user", "text": text, "source": source}
|
||||
if client_msg_id:
|
||||
entry["clientMsgId"] = client_msg_id
|
||||
if not project_id:
|
||||
try:
|
||||
with urllib.request.urlopen(f"{brain_url}/projects/status", timeout=3) as r:
|
||||
project_id = (json.loads(r.read()).get("active_id") or "")
|
||||
except Exception:
|
||||
pass
|
||||
if project_id:
|
||||
entry["project_id"] = project_id
|
||||
self._append_chat_backup(entry)
|
||||
|
||||
# agent_activity → thinking. _emit_activity statt direktem _send_to_rvs
|
||||
@@ -1936,6 +1970,7 @@ class ARIABridge:
|
||||
core_text,
|
||||
source="app" + (" [barge-in]" if interrupted else ""),
|
||||
client_msg_id=client_msg_id,
|
||||
project_id=str(payload.get("projectId") or ""),
|
||||
))
|
||||
return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user