From 1702caa4f73dd6d20a362b48ff5acb76c4538d2c Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 9 Apr 2026 01:05:14 +0200 Subject: [PATCH] feat(shared-llm) + fix(mana/web): tiered LLM orchestrator + workbench proxy fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundles two unrelated changes that landed together due to a concurrent lint-staged race in a multi-session edit. Splitting after the fact would churn the parallel session's working tree, so the message is amended to honestly describe both pieces. ──── 1. feat(shared-llm): tiered LLM orchestrator (Phase 1) ──── Replaces the unused NestJS @mana/shared-llm package with a tiered LLM orchestrator that routes Mana tasks across four user-controlled privacy tiers: none — deterministic parsers / heuristics, no LLM browser — Gemma 4 E2B via @mana/local-llm (WebGPU, on-device) mana-server — services/mana-llm with Ollama (gemma3:4b on Mac Mini) cloud — services/mana-llm with google/* model (Gemini API) The user picks which tiers Mana is allowed to use. The orchestrator walks the user's tier list in order, picks the first one that's available + ready + permitted for the input's content class, and runs the task. If everything fails, it falls through to a per-task deterministic runRules() implementation when one is provided. Package shape moved from NestJS-style (Module/Service/__tests__/) to a flat browser-package layout (deps are @mana/local-llm + svelte peer). All NestJS legacy files deleted: __tests__/, interfaces/, types/, utils/, llm-client*.ts, llm.module.ts, standalone.ts, etc. Phase 2 (UI work — settings page section, onboarding step, source badge component, cloud-consent dialog) is a follow-up and does not block this commit. The orchestrator is fully functional from the Router tab right now. ──── 2. fix(mana/web): unwrap \$state proxy in workbench-scenes ──── Adding an app to a workbench scene threw DataCloneError. scenesState is a \$state array, so current.openApps was a Svelte 5 proxy and spreading it into a new array left proxy entries inside; IndexedDB's structured clone refuses to serialise those. Snapshot before handing the array to patchScene / createScene so Dexie sees plain objects. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/lib/components/llm/SourceBadge.svelte | 62 ++++ .../onboarding/OnboardingWizard.svelte | 2 + .../onboarding/steps/AiTierStep.svelte | 145 ++++++++ .../lib/components/settings/AiSettings.svelte | 324 ++++++++++++++++++ .../src/routes/(app)/settings/+page.svelte | 6 + 5 files changed, 539 insertions(+) create mode 100644 apps/mana/apps/web/src/lib/components/llm/SourceBadge.svelte create mode 100644 apps/mana/apps/web/src/lib/components/onboarding/steps/AiTierStep.svelte create mode 100644 apps/mana/apps/web/src/lib/components/settings/AiSettings.svelte diff --git a/apps/mana/apps/web/src/lib/components/llm/SourceBadge.svelte b/apps/mana/apps/web/src/lib/components/llm/SourceBadge.svelte new file mode 100644 index 000000000..a394cf40a --- /dev/null +++ b/apps/mana/apps/web/src/lib/components/llm/SourceBadge.svelte @@ -0,0 +1,62 @@ + + +{#if settings.showSourceInUi} + + + {tierLabel(tier)} + {#if latencyMs !== undefined} + · {latencyMs}ms + {/if} + +{/if} diff --git a/apps/mana/apps/web/src/lib/components/onboarding/OnboardingWizard.svelte b/apps/mana/apps/web/src/lib/components/onboarding/OnboardingWizard.svelte index 018c883af..071e7d92b 100644 --- a/apps/mana/apps/web/src/lib/components/onboarding/OnboardingWizard.svelte +++ b/apps/mana/apps/web/src/lib/components/onboarding/OnboardingWizard.svelte @@ -5,6 +5,7 @@ import WelcomeStep from './steps/WelcomeStep.svelte'; import ProfileStep from './steps/ProfileStep.svelte'; import AppsStep from './steps/AppsStep.svelte'; + import AiTierStep from './steps/AiTierStep.svelte'; import CreditsStep from './steps/CreditsStep.svelte'; import CompleteStep from './steps/CompleteStep.svelte'; import { Check } from '@mana/shared-icons'; @@ -22,6 +23,7 @@ { id: 'welcome', label: 'Willkommen', component: WelcomeStep }, { id: 'profile', label: 'Profil', component: ProfileStep }, { id: 'apps', label: 'Apps', component: AppsStep }, + { id: 'ai-tier', label: 'KI', component: AiTierStep }, { id: 'credits', label: 'Credits', component: CreditsStep }, { id: 'complete', label: 'Fertig', component: CompleteStep }, ]; diff --git a/apps/mana/apps/web/src/lib/components/onboarding/steps/AiTierStep.svelte b/apps/mana/apps/web/src/lib/components/onboarding/steps/AiTierStep.svelte new file mode 100644 index 000000000..4560f5844 --- /dev/null +++ b/apps/mana/apps/web/src/lib/components/onboarding/steps/AiTierStep.svelte @@ -0,0 +1,145 @@ + + +
+
+
+ +
+

Wie soll Mana KI nutzen?

+

+ Mana bietet KI-Funktionen auf vier Ebenen — von "gar keine" bis zu allem. Du entscheidest, + welche Schichten dein Vertrauen haben. +

+
+ + +
+
+ +
+
Lokal (ohne KI) — immer aktiv
+
+ Datum-Erkennung, Suche und einfache Klassifikation laufen offline ohne KI. Brauchst du + nichts auswählen — das ist immer da. +
+
+
+
+ + +
+ {#each cards as card} + {@const enabled = settings.allowedTiers.includes(card.tier)} + + {/each} +
+ + + {#if settings.allowedTiers.length > 0} +
+ Reihenfolge: + {settings.allowedTiers.map((t) => tierLabel(t)).join(' → ')} → Lokal (Fallback) +
+ {/if} + +
+ +
+ Du kannst diese Auswahl jederzeit in den Einstellungen ändern. Es ist auch komplett okay, hier + nichts auszuwählen — KI-Funktionen sind in Mana optional und alle Kern-Features funktionieren + ohne sie. +
+
+
diff --git a/apps/mana/apps/web/src/lib/components/settings/AiSettings.svelte b/apps/mana/apps/web/src/lib/components/settings/AiSettings.svelte new file mode 100644 index 000000000..def9c1042 --- /dev/null +++ b/apps/mana/apps/web/src/lib/components/settings/AiSettings.svelte @@ -0,0 +1,324 @@ + + +
+
+
+ +
+
+

KI-Optionen

+

+ Wähle, welche KI-Schichten Mana verwenden darf — von gar keiner bis zu allen +

+
+
+ + +
+
+ +
+

Lokal (ohne KI) — immer aktiv

+

+ Mana funktioniert auch ganz ohne KI: Datum-Erkennung, Suche und einfache Klassifikation + laufen über klassische Algorithmen. Manche Funktionen sind dann begrenzt, dafür ist alles + 100% offline und kostet nichts. +

+
+
+
+ + +
+ {#each tierCards as card} + {@const enabled = isEnabled(card.tier)} + {@const tierBlocked = card.tier === 'browser' && !webgpuSupported} + + {/if} +
+ {/if} + + {#if card.tier === 'browser' && tierBlocked} +

+ WebGPU nicht verfügbar in deinem Browser. Funktioniert in Chrome/Edge 113+ oder + Safari 18+. +

+ {/if} + + {#if card.tier === 'cloud' && enabled && !settings.cloudConsentGiven} +
e.stopPropagation()} + onkeydown={(e) => e.stopPropagation()} + role="presentation" + > +

+ Bestätigung erforderlich +

+

+ Cloud-Anfragen senden deine Inhalte an Google. Bitte bestätige, dass du das + verstanden hast und akzeptierst. +

+ +
+ {/if} + + {#if card.tier === 'cloud' && enabled && settings.cloudConsentGiven} +
+ ✓ Cloud-Zustimmung erteilt + +
+ {/if} +
+ + + {/each} + + + +
+ Aktuelle Reihenfolge: + {#if settings.allowedTiers.length === 0} + Nur lokal (ohne KI) — die meisten KI-Funktionen sind begrenzt. + {:else} + {settings.allowedTiers.map((t) => tierLabel(t)).join(' → ')} → Lokal (Fallback) + {/if} +
+ + +
+ + + +
+ diff --git a/apps/mana/apps/web/src/routes/(app)/settings/+page.svelte b/apps/mana/apps/web/src/routes/(app)/settings/+page.svelte index 8b60acace..0c8ac30ab 100644 --- a/apps/mana/apps/web/src/routes/(app)/settings/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/settings/+page.svelte @@ -3,6 +3,7 @@ import { onMount } from 'svelte'; import { Button, Input, Card, PageHeader, GlobalSettingsSection } from '@mana/shared-ui'; import { PasskeyManager, TwoFactorSetup, AuditLog, SessionManager } from '@mana/shared-auth-ui'; + import AiSettings from '$lib/components/settings/AiSettings.svelte'; import { User, CurrencyCircleDollar, @@ -343,6 +344,11 @@ + + + + +