feat(projects): Threads im Hauptchat verankert (Stefan-Konzept)
Projekte sind benannte Thema-Bündel die voice-gesteuert via Brain-Tools
geöffnet/verlassen werden. Default-Mode bleibt der Hauptthread — Projekte
sind eine optionale Bühne. Anchored-not-replaced: App-Open landet immer
im Hauptchat, Projekte sind nur sichtbar wenn aktiv betreten.
Brain:
- projects.py: CRUD + Fuzzy-Find + Active-State-Pointer
(/shared/config/projects.json + active_project.txt).
- conversation.py: Turn.project_id-Feld + window(project_id) Filter.
- agent.py: 6 Meta-Tools — project_create / _enter / _exit / _list /
_summary / _end. chat() liest aktive Projekt-ID, taggt User+Assistant-
Turns damit, filtert das LLM-Window auf Projekt-Kontext und ergaenzt
den System-Prompt um den aktiven Projekt-Hinweis. touch_project pflegt
last_activity_at + turn_count.
- main.py: REST-Endpoints /projects/{status,list,create,switch,
{id}/end,{id}/archive, PATCH /{id}}.
Bridge + RVS:
- aria_bridge.py: project_changed Event-Propagation Brain → RVS-Broadcast
damit App + Diagnostic ihre Banner refreshen.
- rvs/server.js: project_changed in ALLOWED_TYPES.
App:
- brainApi.ts: Project-Type + 6 API-Methoden.
- ProjectsBrowser.tsx (neue Komponente, ~340 Zeilen): Status-Header,
Hauptchat als Erster-Eintrag, Projekt-Liste mit Aktiv-Marker, Long-Press
zum Editieren, Modals fuer Neu/Edit/End/Archiv.
- ChatScreen.tsx: Banner unterhalb des Status-Bars zeigt aktives Projekt
oder „Hauptchat" — Tap öffnet ProjectsBrowser als Modal. Aktive Projekt-
Info wird bei Mount + bei project_changed-Events refreshed.
- SettingsScreen.tsx: Neue Section 📁 „Projekte" zeigt ProjectsBrowser inline.
Diagnostic:
- Neue Sektion im Brain-Tab mit Liste, Aktiv-Marker, Beenden/Archivieren
pro Zeile, Modal fuer Neu. Lädt automatisch bei Brain-Tab + bei
project_changed-Event-Broadcast.
Was bewusst NICHT drin ist (Folgeschritte):
- Per-Message Filter im Chat-Verlauf (zeigt aktuell alle Bubbles, Banner
zeigt Kontext) — App müsste Chat-History per project_id filtern.
- Files-by-Project Tagging.
- Inline-Collapse-Bloecke im Chat-Verlauf.
- Sub-Projekte (Stefan-Entscheidung: weglassen, „Mama-tauglich").
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -992,6 +992,41 @@
|
||||
|
||||
<!-- Alte Sessions-Sicherung entfernt — aria-core ist raus. -->
|
||||
|
||||
<!-- Projekte — Threads-im-Hauptchat-Konzept -->
|
||||
<div class="settings-section">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
|
||||
<h2 style="margin:0;">📁 Projekte</h2>
|
||||
<div>
|
||||
<button class="btn secondary" onclick="loadProjects()" style="padding:4px 10px;font-size:11px;">🔄 Aktualisieren</button>
|
||||
<button class="btn" onclick="openCreateProjectModal()" style="padding:4px 10px;font-size:11px;">+ Neues Projekt</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:11px;color:#8888AA;margin-bottom:8px;">
|
||||
Projekte bündeln zusammengehörige Turns als Block im Hauptchat. Stefan sagt zu ARIA
|
||||
„lass uns ein Projekt anlegen" oder klickt hier auf „+ Neues Projekt". Aktives Projekt:
|
||||
<span id="project-active-label" style="color:#34C759;font-weight:600;">(wird geladen...)</span>
|
||||
</div>
|
||||
<div id="project-list" class="card" style="padding:0;">
|
||||
<div style="padding:14px;color:#8888AA;font-size:12px;">Lade Projekte...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Neues-Projekt Modal -->
|
||||
<div id="project-create-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:1000;align-items:center;justify-content:center;">
|
||||
<div style="background:#15151E;padding:20px;border-radius:8px;min-width:340px;max-width:90vw;">
|
||||
<h3 style="margin-top:0;color:#E0E0F0;">Neues Projekt</h3>
|
||||
<label style="display:block;color:#8888AA;font-size:12px;margin-bottom:4px;">Name</label>
|
||||
<input type="text" id="project-create-name" placeholder="z.B. Frankreich-Urlaub"
|
||||
style="width:100%;box-sizing:border-box;background:#0A0A14;color:#E0E0F0;border:1px solid #2A2A3E;padding:8px;border-radius:4px;font-size:14px;margin-bottom:10px;">
|
||||
<label style="display:block;color:#8888AA;font-size:12px;margin-bottom:4px;">Beschreibung (optional)</label>
|
||||
<textarea id="project-create-desc" placeholder="1 Satz worum's geht. Hilft beim Wiederfinden."
|
||||
style="width:100%;box-sizing:border-box;background:#0A0A14;color:#E0E0F0;border:1px solid #2A2A3E;padding:8px;border-radius:4px;font-size:13px;height:60px;resize:vertical;margin-bottom:14px;"></textarea>
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end;">
|
||||
<button class="btn secondary" onclick="closeCreateProjectModal()" style="padding:6px 14px;font-size:12px;">Abbrechen</button>
|
||||
<button class="btn primary" onclick="submitCreateProject()" style="padding:6px 14px;font-size:12px;">Anlegen + aktivieren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
|
||||
@@ -1543,6 +1578,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'project_changed') {
|
||||
// ARIA hat in einem Tool-Call ein Projekt erstellt/betreten/verlassen/beendet.
|
||||
// Liste neu laden falls sichtbar.
|
||||
loadProjects();
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'voice_id_delete_response') {
|
||||
const p = msg.payload || msg;
|
||||
if (p.removed) {
|
||||
@@ -2694,6 +2736,117 @@
|
||||
send({ action: 'voice_id_delete' });
|
||||
}
|
||||
|
||||
// ── Projekte ────────────────────────────────────────────
|
||||
async function loadProjects() {
|
||||
const listEl = document.getElementById('project-list');
|
||||
const activeLabel = document.getElementById('project-active-label');
|
||||
try {
|
||||
const r = await fetch('/api/brain/projects/status');
|
||||
const status = await r.json();
|
||||
const projects = status.projects || [];
|
||||
const activeId = status.active_id || '';
|
||||
activeLabel.textContent = status.active ? status.active.name : '💬 Hauptchat';
|
||||
activeLabel.style.color = status.active ? '#34C759' : '#8888AA';
|
||||
|
||||
const rows = [];
|
||||
// Hauptchat-Eintrag
|
||||
rows.push(`
|
||||
<div onclick="switchProject('')" style="cursor:pointer;padding:12px 14px;border-bottom:1px solid #1E1E2E;${!activeId ? 'background:rgba(52,199,89,0.08);border-left:3px solid #34C759;' : ''}">
|
||||
<div style="color:${!activeId ? '#34C759' : '#E0E0F0'};font-weight:600;">💬 Hauptchat ${!activeId ? '<span style="font-size:10px;font-weight:800;">✓ AKTIV</span>' : ''}</div>
|
||||
<div style="color:#555570;font-size:11px;margin-top:2px;">Standard-Verlauf, keine Projekt-Zuordnung</div>
|
||||
</div>`);
|
||||
for (const p of projects) {
|
||||
const isActive = p.id === activeId;
|
||||
const since = p.last_activity_at ? new Date(p.last_activity_at * 1000).toLocaleString('de-DE') : '?';
|
||||
const ended = p.status === 'ended';
|
||||
rows.push(`
|
||||
<div style="padding:12px 14px;border-bottom:1px solid #1E1E2E;${isActive ? 'background:rgba(52,199,89,0.08);border-left:3px solid #34C759;' : ''}">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px;">
|
||||
<div onclick="switchProject('${p.id}')" style="cursor:pointer;flex:1;">
|
||||
<div style="color:${isActive ? '#34C759' : '#E0E0F0'};font-weight:600;">
|
||||
📁 ${escapeHtml(p.name)}
|
||||
${ended ? '<span style="color:#FFD60A;font-size:10px;font-weight:700;margin-left:6px;background:rgba(255,214,10,0.15);padding:2px 6px;border-radius:3px;">beendet</span>' : ''}
|
||||
${isActive ? '<span style="color:#34C759;font-size:10px;font-weight:800;margin-left:6px;">✓ AKTIV</span>' : ''}
|
||||
</div>
|
||||
${p.description ? `<div style="color:#8888AA;font-size:12px;margin-top:2px;">${escapeHtml(p.description)}</div>` : ''}
|
||||
<div style="color:#555570;font-size:11px;margin-top:4px;">${p.turn_count} Turns · zuletzt ${since}</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:4px;">
|
||||
${!ended ? `<button class="btn secondary" onclick="endProject('${p.id}', '${escapeHtmlAttr(p.name)}')" style="padding:3px 8px;font-size:10px;" title="Projekt beenden">⏹</button>` : ''}
|
||||
<button class="btn secondary" onclick="archiveProject('${p.id}', '${escapeHtmlAttr(p.name)}')" style="padding:3px 8px;font-size:10px;color:#E55C5C;" title="Archivieren">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`);
|
||||
}
|
||||
if (projects.length === 0) {
|
||||
rows.push('<div style="padding:18px;color:#555570;font-size:12px;text-align:center;">Noch keine Projekte. „+ Neues Projekt" oder sag ARIA „lass uns ein Projekt anlegen".</div>');
|
||||
}
|
||||
listEl.innerHTML = rows.join('');
|
||||
} catch (e) {
|
||||
listEl.innerHTML = `<div style="padding:14px;color:#FF6E6E;font-size:12px;">Fehler: ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function switchProject(projectId) {
|
||||
try {
|
||||
await fetch('/api/brain/projects/switch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ project_id: projectId }),
|
||||
});
|
||||
loadProjects();
|
||||
} catch (e) { alert('Wechsel fehlgeschlagen: ' + e.message); }
|
||||
}
|
||||
|
||||
async function endProject(id, name) {
|
||||
if (!confirm(`Projekt "${name}" beenden?\n\nBleibt sichtbar, aktiv ist dann der Hauptchat.`)) return;
|
||||
try {
|
||||
await fetch(`/api/brain/projects/${encodeURIComponent(id)}/end`, { method: 'POST' });
|
||||
loadProjects();
|
||||
} catch (e) { alert('Beenden fehlgeschlagen: ' + e.message); }
|
||||
}
|
||||
|
||||
async function archiveProject(id, name) {
|
||||
if (!confirm(`Projekt "${name}" archivieren?\n\nVerschwindet aus der Liste.`)) return;
|
||||
try {
|
||||
await fetch(`/api/brain/projects/${encodeURIComponent(id)}/archive`, { method: 'POST' });
|
||||
loadProjects();
|
||||
} catch (e) { alert('Archivieren fehlgeschlagen: ' + e.message); }
|
||||
}
|
||||
|
||||
function openCreateProjectModal() {
|
||||
document.getElementById('project-create-name').value = '';
|
||||
document.getElementById('project-create-desc').value = '';
|
||||
document.getElementById('project-create-modal').style.display = 'flex';
|
||||
setTimeout(() => document.getElementById('project-create-name').focus(), 50);
|
||||
}
|
||||
|
||||
function closeCreateProjectModal() {
|
||||
document.getElementById('project-create-modal').style.display = 'none';
|
||||
}
|
||||
|
||||
async function submitCreateProject() {
|
||||
const name = document.getElementById('project-create-name').value.trim();
|
||||
const description = document.getElementById('project-create-desc').value.trim();
|
||||
if (!name) { alert('Name darf nicht leer sein.'); return; }
|
||||
try {
|
||||
await fetch('/api/brain/projects/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, description }),
|
||||
});
|
||||
closeCreateProjectModal();
|
||||
loadProjects();
|
||||
} catch (e) { alert('Anlegen fehlgeschlagen: ' + e.message); }
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
}
|
||||
function escapeHtmlAttr(str) {
|
||||
return String(str).replace(/['"\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function deleteXttsVoice(name) {
|
||||
if (!confirm(`Stimme "${name}" endgueltig loeschen?`)) return;
|
||||
send({ action: 'xtts_delete_voice', name });
|
||||
@@ -3450,6 +3603,7 @@
|
||||
loadBrainMemoryList();
|
||||
refreshImportFiles();
|
||||
loadMetrics();
|
||||
loadProjects();
|
||||
} else if (tab === 'files') {
|
||||
loadFiles();
|
||||
} else if (tab === 'skills') {
|
||||
|
||||
Reference in New Issue
Block a user