diff --git a/apps/mana/apps/web/src/lib/modules/companion/pages/AiHomePage.svelte b/apps/mana/apps/web/src/lib/modules/companion/pages/AiHomePage.svelte new file mode 100644 index 000000000..edd311e05 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/companion/pages/AiHomePage.svelte @@ -0,0 +1,231 @@ + + + + +
+
+
+ {proposals.value.length} + wartende Vorschläge +
+
+ {missions.value.length} + aktive Missions +
+
+ +
+

Bereiche öffnen

+
+ {#each shortcuts as s (s.id)} + {@const open = isOpen(s.id)} + + {/each} +
+
+ +
+

+ Jeder Bereich lebt als eigene Page in diesem Carousel. Du kannst mehrere gleichzeitig offen + haben, einzeln schließen oder per „×" in der Header-Leiste beenden. Das Layout merkt sich + deine Auswahl. +

+
+
+
+ + diff --git a/apps/mana/apps/web/src/lib/modules/companion/pages/ChatPage.svelte b/apps/mana/apps/web/src/lib/modules/companion/pages/ChatPage.svelte new file mode 100644 index 000000000..0cdcc4db1 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/companion/pages/ChatPage.svelte @@ -0,0 +1,224 @@ + + + + +
+
+ + +
+ + {#if showList} +
+ {#each conversations.value as c (c.id)} +
+ + +
+ {/each} + {#if conversations.value.length === 0} +

Noch keine Gespräche — klick auf „Neu".

+ {/if} +
+ {:else if activeConversation} + {#key activeConversation.id} + + {/key} + {:else} +
+ +

Mana Companion

+ +
+ {/if} +
+
+ + diff --git a/apps/mana/apps/web/src/lib/modules/companion/pages/HealthPage.svelte b/apps/mana/apps/web/src/lib/modules/companion/pages/HealthPage.svelte new file mode 100644 index 000000000..9cb2b8bad --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/companion/pages/HealthPage.svelte @@ -0,0 +1,179 @@ + + + + +
+
+

Foreground-Runner (dieser Tab)

+
+
Status
+
+ {#if isMissionTickRunning()} + ● aktiv · 60 s Interval + {:else} + ● nicht aktiv + {/if} +
+ {#if lastRunStats} +
Letzter Manual-Tick
+
+ {lastRunStats.at} · {lastRunStats.plansProduced} Plans · + {lastRunStats.errors} Fehler +
+ {/if} +
+ +
+ +
+

Server-Runner (mana-ai)

+

+ Läuft unabhängig vom Browser. Status + Uptime werden von Prometheus gescrapet und auf dem + Status-Dashboard angezeigt. +

+ + status.mana.how öffnen → + +
+ +
+

Datenlage (lokal)

+

+ Alles in diesem Carousel kommt aus IndexedDB. Kein Server-Call außer beim Planner selbst. +

+
+
+
+ + diff --git a/apps/mana/apps/web/src/lib/modules/companion/pages/InsightsPage.svelte b/apps/mana/apps/web/src/lib/modules/companion/pages/InsightsPage.svelte new file mode 100644 index 000000000..3998ed07f --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/companion/pages/InsightsPage.svelte @@ -0,0 +1,314 @@ + + + + +
+
+

Approval-Rate

+ {#if approvalRate === null} +

Noch nicht genug Daten.

+ {:else} +
{approvalRate}%
+

über alle Missions + alle Iterationen

+ {/if} +
+ +
+

AI-Events / Tag (14 Tage)

+
+ {#each perDay as b (b.day)} +
+ 0} + > +
+ {/each} +
+
+ +
+

Pro Mission

+ {#if missionStats.length === 0} +

Keine Missions angelegt.

+ {:else} +
    + {#each missionStats as m (m.id)} +
  • + {m.title} + + {m.approved} + · + {m.rejected} + · + {m.awaiting} + · + {m.failed} + +
  • + {/each} +
+

+ approved · + rejected · + awaiting · + failed +

+ {/if} +
+ +
+

Häufigstes Feedback

+ {#if topFeedback.length === 0} +

Noch keine Freitext-Reviews.

+ {:else} +
    + {#each topFeedback as fb} +
  • + {fb.count}× + "{fb.text}" +
  • + {/each} +
+ {/if} +
+
+
+ + diff --git a/apps/mana/apps/web/src/lib/modules/companion/pages/MissionsPage.svelte b/apps/mana/apps/web/src/lib/modules/companion/pages/MissionsPage.svelte new file mode 100644 index 000000000..105df28c2 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/companion/pages/MissionsPage.svelte @@ -0,0 +1,633 @@ + + + + + {#if mode === 'list'} +
+
+ +
+ {#if missions.value.length === 0} +

+ Keine Missions — lege eine an um die KI dauerhaft für dich arbeiten zu lassen. +

+ {:else} + + {/if} +
+ {:else if mode === 'create'} +
(e.preventDefault(), handleCreate())}> + + + + +
+ Inputs (Kontext für die KI) + +
+
+ Cadence +
+ + + +
+
+
+ +
+
+ {:else if selected} +
+ +

{selected.title}

+
+ + {#if selected.state === 'active'} + + {:else if selected.state === 'paused'} + + {/if} + {#if selected.state !== 'done'} + + {/if} + +
+ +
+
Ziel
+
{selected.objective}
+
Cadence
+
{describeCadence(selected.cadence)} · {describeState(selected.state)}
+
Next
+
{formatRelative(selected.nextRunAt)}
+
Inputs
+
+ {#if selected.inputs.length === 0}—{:else} + {selected.inputs.map((i) => `${i.module}/${i.id}`).join(', ')} + {/if} +
+
+ + {#if selected.conceptMarkdown} +
+ Konzept +
{selected.conceptMarkdown}
+
+ {/if} + +

Iterationen

+ {#if selected.iterations.length === 0} +

Noch keine Iteration gelaufen.

+ {:else} + {#each [...selected.iterations].reverse() as it (it.id)} +
+
+ {new Date(it.startedAt).toLocaleString('de-DE')} + {it.overallStatus} +
+ {#if it.summary}

{it.summary}

{/if} + {#if it.userFeedback} +
{it.userFeedback}
+ {:else if it.overallStatus === 'awaiting-review'} +
(e.preventDefault(), handleFeedback(selected, it.id))} + > + + +
+ {/if} +
+ {/each} + {/if} +
+ {/if} +
+ + diff --git a/apps/mana/apps/web/src/lib/modules/companion/pages/PagePicker.svelte b/apps/mana/apps/web/src/lib/modules/companion/pages/PagePicker.svelte new file mode 100644 index 000000000..f6cb91bc8 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/companion/pages/PagePicker.svelte @@ -0,0 +1,138 @@ + + + +
+
+ Page öffnen + +
+ + {#if availableIds.length === 0} +

Alle Pages sind bereits geöffnet.

+ {:else} + + {/if} +
+ + diff --git a/apps/mana/apps/web/src/lib/modules/companion/pages/PolicyPage.svelte b/apps/mana/apps/web/src/lib/modules/companion/pages/PolicyPage.svelte new file mode 100644 index 000000000..c96dd16ff --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/companion/pages/PolicyPage.svelte @@ -0,0 +1,271 @@ + + + + +
+
+

+ Pro Tool festlegen was passiert wenn die KI es aufruft. + auto führt sofort aus, + propose stagt als Vorschlag zur Freigabe, + deny sperrt das Tool komplett. +

+ {#if hasOverrides} + + {/if} +
+ + {#each grouped as [mod, list] (mod)} +
+

{mod}

+
    + {#each list as t (t.name)} + {@const current = decide(t.name)} + {@const overridden = overrides[t.name] !== undefined} +
  • +
    + {t.name} + {t.description} +
    +
    + {#each ['auto', 'propose', 'deny'] as PolicyDecision[] as d} + + {/each} +
    +
  • + {/each} +
+
+ {/each} +
+
+ + diff --git a/apps/mana/apps/web/src/lib/modules/companion/pages/RitualsPage.svelte b/apps/mana/apps/web/src/lib/modules/companion/pages/RitualsPage.svelte new file mode 100644 index 000000000..1e0e7988d --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/companion/pages/RitualsPage.svelte @@ -0,0 +1,207 @@ + + + + +
+ {#if activeRitual} + + (activeRitual = null)} + onClose={() => (activeRitual = null)} + /> + {:else} +
+ +
+ + {#if showTemplates} +
+ {#each RITUAL_TEMPLATES as t} + + {/each} +
+ {/if} + +
    + {#each rituals.value as r (r.id)} +
  • + + +
  • + {/each} + {#if rituals.value.length === 0 && !showTemplates} +
  • Noch keine Rituale — erstelle eines aus einer Vorlage oben.
  • + {/if} +
+ {/if} +
+
+ + diff --git a/apps/mana/apps/web/src/lib/modules/companion/pages/WorkbenchPage.svelte b/apps/mana/apps/web/src/lib/modules/companion/pages/WorkbenchPage.svelte new file mode 100644 index 000000000..84288ddf3 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/companion/pages/WorkbenchPage.svelte @@ -0,0 +1,290 @@ + + + + +
+
+ + +
+ + {#if buckets.length === 0} +

+ Noch keine AI-Aktivität{moduleFilter ? ` in ${moduleFilter}` : ''}. Sobald eine Mission + läuft und Proposals approved werden, erscheinen hier die Änderungen. +

+ {:else} +
    + {#each buckets as b (b.key)} +
  1. +
    +
    + {formatDate(b.firstTimestamp)} + {formatTime(b.firstTimestamp)} +
    +
    + + {missionTitleById.get(b.missionId) ?? b.missionId} + + {#if b.rationale} +

    {b.rationale}

    + {/if} +
    + +
    +
      + {#each b.events as e (e.meta.id)} +
    • + {e.meta.appId} + {describeEvent(e)} + + + +
    • + {/each} +
    +
  2. + {/each} +
+ {/if} +
+
+ + diff --git a/apps/mana/apps/web/src/lib/modules/companion/pages/page-meta.ts b/apps/mana/apps/web/src/lib/modules/companion/pages/page-meta.ts new file mode 100644 index 000000000..43394ab0e --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/companion/pages/page-meta.ts @@ -0,0 +1,99 @@ +/** + * Static metadata for every page the Companion carousel can render. + * + * Matches the shape modules like `/todo` use in their + * `PAGE_META: Record` — adapted here to the + * Companion's own page set. Colors map loosely to the activity type + * (data: muted, creative: primary-ish, health: green, policy: orange). + */ + +import { Sparkle, ChatCircle, Flag, Notebook, Lightning, Heartbeat } from '@mana/shared-icons'; +import type { Component } from 'svelte'; +import type { CompanionPageId } from '../stores/workbench-settings.svelte'; + +export interface CompanionPageMeta { + id: CompanionPageId; + title: string; + shortLabel: string; + color: string; + icon: Component; + description: string; +} + +export const COMPANION_PAGE_META: Record = { + home: { + id: 'home', + title: 'Companion', + shortLabel: 'Home', + color: '#6B5BFF', + icon: Sparkle, + description: 'Übersicht, schnelle Einstiege, letzte Aktivität.', + }, + chat: { + id: 'chat', + title: 'Chat', + shortLabel: 'Chat', + color: '#3B82F6', + icon: ChatCircle, + description: 'Gespräch mit der KI.', + }, + missions: { + id: 'missions', + title: 'Missions', + shortLabel: 'Missions', + color: '#22C55E', + icon: Flag, + description: 'Langlebige Aufträge an die KI anlegen, pausieren, ausführen.', + }, + workbench: { + id: 'workbench', + title: 'Workbench', + shortLabel: 'Workbench', + color: '#F59E0B', + icon: Notebook, + description: 'Timeline aller KI-Aktivität; rückgängig machen.', + }, + rituals: { + id: 'rituals', + title: 'Rituale', + shortLabel: 'Rituale', + color: '#EC4899', + icon: Lightning, + description: 'Geführte Routinen (Morgen, Abend, …).', + }, + policy: { + id: 'policy', + title: 'Policy', + shortLabel: 'Policy', + color: '#F97316', + icon: Flag, + description: 'Pro Tool festlegen: auto / propose / deny.', + }, + insights: { + id: 'insights', + title: 'Insights', + shortLabel: 'Insights', + color: '#8B5CF6', + icon: Notebook, + description: 'Approval-Raten, Feedback-Muster, Stats pro Mission.', + }, + health: { + id: 'health', + title: 'Health', + shortLabel: 'Health', + color: '#10B981', + icon: Heartbeat, + description: 'Runner-Status, letzter Tick, LLM-Backend.', + }, +}; + +export const ALL_COMPANION_PAGE_IDS: readonly CompanionPageId[] = [ + 'home', + 'chat', + 'missions', + 'workbench', + 'rituals', + 'policy', + 'insights', + 'health', +]; diff --git a/apps/mana/apps/web/src/lib/modules/companion/stores/workbench-settings.svelte.ts b/apps/mana/apps/web/src/lib/modules/companion/stores/workbench-settings.svelte.ts new file mode 100644 index 000000000..b2e285f5a --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/companion/stores/workbench-settings.svelte.ts @@ -0,0 +1,100 @@ +/** + * Companion workbench settings — which pages the user currently has + * open + their widths. Persisted to localStorage so layout survives + * reloads; keep it tiny on purpose (no Dexie round-trip for every + * resize). + */ + +export type CompanionPageId = + | 'home' + | 'chat' + | 'missions' + | 'workbench' + | 'rituals' + | 'policy' + | 'insights' + | 'health'; + +export interface CompanionOpenPage { + id: CompanionPageId; + widthPx: number; + heightPx?: number; + maximized?: boolean; +} + +const STORAGE_KEY = 'companion:openPages'; +const DEFAULT_WIDTH = 520; + +/** Fresh users open the home page with shortcuts to everything else. */ +const DEFAULT_OPEN_PAGES: CompanionOpenPage[] = [{ id: 'home', widthPx: DEFAULT_WIDTH }]; + +function loadOpenPages(): CompanionOpenPage[] { + if (typeof localStorage === 'undefined') return DEFAULT_OPEN_PAGES; + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return DEFAULT_OPEN_PAGES; + const parsed = JSON.parse(raw); + if (!Array.isArray(parsed) || parsed.length === 0) return DEFAULT_OPEN_PAGES; + return parsed as CompanionOpenPage[]; + } catch { + return DEFAULT_OPEN_PAGES; + } +} + +function persist(pages: CompanionOpenPage[]): void { + if (typeof localStorage === 'undefined') return; + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(pages)); + } catch { + // ignore quota / private-mode failures — layout falls back to default next load + } +} + +function createStore() { + let openPages = $state(loadOpenPages()); + + return { + get openPages() { + return openPages; + }, + openPage(id: CompanionPageId) { + if (openPages.some((p) => p.id === id)) return; + openPages = [...openPages, { id, widthPx: DEFAULT_WIDTH }]; + persist(openPages); + }, + closePage(id: CompanionPageId) { + openPages = openPages.filter((p) => p.id !== id); + persist(openPages); + }, + resize(id: CompanionPageId, widthPx: number, heightPx?: number) { + openPages = openPages.map((p) => (p.id === id ? { ...p, widthPx, heightPx } : p)); + persist(openPages); + }, + toggleMaximized(id: CompanionPageId) { + openPages = openPages.map((p) => (p.id === id ? { ...p, maximized: !p.maximized } : p)); + persist(openPages); + }, + moveLeft(id: CompanionPageId) { + const idx = openPages.findIndex((p) => p.id === id); + if (idx <= 0) return; + const next = [...openPages]; + [next[idx - 1], next[idx]] = [next[idx], next[idx - 1]]; + openPages = next; + persist(openPages); + }, + moveRight(id: CompanionPageId) { + const idx = openPages.findIndex((p) => p.id === id); + if (idx === -1 || idx >= openPages.length - 1) return; + const next = [...openPages]; + [next[idx], next[idx + 1]] = [next[idx + 1], next[idx]]; + openPages = next; + persist(openPages); + }, + reset() { + openPages = [...DEFAULT_OPEN_PAGES]; + persist(openPages); + }, + }; +} + +export const companionWorkbenchSettings = createStore(); diff --git a/apps/mana/apps/web/src/routes/(app)/companion/+page.svelte b/apps/mana/apps/web/src/routes/(app)/companion/+page.svelte index 5c1b18967..b7ec0f7dc 100644 --- a/apps/mana/apps/web/src/routes/(app)/companion/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/companion/+page.svelte @@ -1,39 +1,66 @@ + @@ -41,268 +68,51 @@ Companion - Mana -
- - - - -
- {#if activeConversation} - {#key activeConversation.id} - - {/key} - {:else} -
- -

Mana Companion

-

- Dein persoenlicher Assistent. Frag nach deinem Tag, lass Tasks erstellen oder Getraenke - loggen. -

- -
+ {#if pageId === 'home'} + + {:else if pageId === 'chat'} + + {:else if pageId === 'missions'} + + {:else if pageId === 'workbench'} + + {:else if pageId === 'rituals'} + + {:else if pageId === 'policy'} + + {:else if pageId === 'insights'} + + {:else if pageId === 'health'} + {/if} -
-
+ {/snippet} - + {#snippet picker()} + p.id)} + onPick={pick} + onClose={() => (showPicker = false)} + /> + {/snippet} + diff --git a/apps/mana/apps/web/src/routes/(app)/companion/missions/+page.svelte b/apps/mana/apps/web/src/routes/(app)/companion/missions/+page.svelte deleted file mode 100644 index 13966b682..000000000 --- a/apps/mana/apps/web/src/routes/(app)/companion/missions/+page.svelte +++ /dev/null @@ -1,717 +0,0 @@ - - - - - Missions - Companion - - -
- - - {#if showForm} -
(e.preventDefault(), handleCreate())}> - - - - - - -
- Inputs (Kontext für die KI) - -
- -
- Cadence -
- - - -
-
- -
- -
-
- {/if} - -
- - -
- {#if selected} -
-

{selected.title}

-
- - Workbench → - - - {#if selected.state === 'active'} - - {:else if selected.state === 'paused'} - - {/if} - {#if selected.state !== 'done'} - - {/if} - -
-
- -
-
Ziel
-
{selected.objective}
-
Cadence
-
{describeCadence(selected.cadence)}
-
Nächster Run
-
{formatRelative(selected.nextRunAt)}
-
Inputs
-
- {#if selected.inputs.length === 0} - — - {:else} - {selected.inputs.map((i) => `${i.module}/${i.id}`).join(', ')} - {/if} -
-
- - {#if selected.conceptMarkdown} -
-

Konzept

-
{selected.conceptMarkdown}
-
- {/if} - -
-

Iterationen

- {#if selected.iterations.length === 0} -

Noch keine Iteration gelaufen.

- {:else} - {#each [...selected.iterations].reverse() as it (it.id)} -
-
- {new Date(it.startedAt).toLocaleString('de-DE')} - {it.overallStatus} -
- {#if it.summary} -

{it.summary}

- {/if} - {#if it.plan.length > 0} -
    - {#each it.plan as step} -
  • - [{step.status}] - {#if step.summary} - {step.summary} - {:else if step.intent.kind === 'toolCall'} - {step.intent.toolName} - {:else} - Notiz - {/if} -
  • - {/each} -
- {/if} - {#if it.userFeedback} - - {:else if !it.finishedAt || it.overallStatus === 'awaiting-review'} - - {/if} -
- {/each} - {/if} -
- {:else} -

Wähle links eine Mission aus, oder erstelle eine neue.

- {/if} -
-
-
- - diff --git a/apps/mana/apps/web/src/routes/(app)/companion/rituals/+page.svelte b/apps/mana/apps/web/src/routes/(app)/companion/rituals/+page.svelte deleted file mode 100644 index e6b7453a2..000000000 --- a/apps/mana/apps/web/src/routes/(app)/companion/rituals/+page.svelte +++ /dev/null @@ -1,283 +0,0 @@ - - - - Rituale - Mana Companion - - -{#if activeRitual} - (activeRitual = null)} - /> -{:else} -
- - - {#if showTemplates} -
-

Vorlage waehlen

- {#each RITUAL_TEMPLATES as tpl} - - {/each} -
- {/if} - -
- {#each rituals.value as ritual (ritual.id)} -
-
- {ritual.title} - {#if ritual.description} - {ritual.description} - {/if} - - {ritual.trigger === 'morning' - ? 'Morgens' - : ritual.trigger === 'evening' - ? 'Abends' - : 'Manuell'} - · {ritual.status === 'active' ? 'Aktiv' : 'Pausiert'} - -
-
- {#if ritual.status === 'active'} - - - {:else} - - {/if} - -
-
- {:else} -

Noch keine Rituale. Erstelle eins aus einer Vorlage.

- {/each} -
-
-{/if} - - diff --git a/apps/mana/apps/web/src/routes/(app)/companion/workbench/+page.svelte b/apps/mana/apps/web/src/routes/(app)/companion/workbench/+page.svelte deleted file mode 100644 index c736bcc47..000000000 --- a/apps/mana/apps/web/src/routes/(app)/companion/workbench/+page.svelte +++ /dev/null @@ -1,336 +0,0 @@ - - - - - Workbench - Companion - - -
- - - {#if buckets.length === 0} -

- Noch keine AI-Aktivität{moduleFilter ? ` in ${moduleFilter}` : ''}. Sobald eine Mission läuft - und Proposals approved werden, erscheinen die Änderungen hier. -

- {:else} -
    - {#each buckets as b (b.key)} -
  1. -
    -
    - {formatDate(b.firstTimestamp)} - {formatTime(b.firstTimestamp)} -
    -
    - - {missionTitleById.get(b.missionId) ?? b.missionId} - - {#if b.rationale} -

    {b.rationale}

    - {/if} -
    - -
    -
      - {#each b.events as e (e.meta.id)} -
    • - {e.meta.appId} - {describeEvent(e)} - - - -
    • - {/each} -
    -
  2. - {/each} -
- {/if} -
- -