mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-27 20:32:54 +02:00
feat(mana/web): AI tier selector dropdown in PillNavigation
Quick-access dropdown in the bottom navigation bar for toggling LLM
tiers without navigating to the full Settings page. Follows the same
PillDropdown pattern as the existing theme variant selector.
Three files changed:
packages/shared-ui/src/navigation/types.ts
Add showAiTierSelector, aiTierItems, currentAiTierLabel to
PillNavigationProps. Same shape as the existing theme variant
and language switcher props.
packages/shared-ui/src/navigation/PillNavigation.svelte
Destructure the three new props (defaults: false, [], 'KI').
Render a PillDropdown with icon="cpu" between the theme
variant selector and the theme toggle button.
apps/mana/apps/web/src/routes/(app)/+layout.svelte
Import llmSettingsState, updateLlmSettings, tierLabel, type
LlmTier from @mana/shared-llm. Import isLocalLlmSupported,
getLocalLlmStatus, loadLocalLlm from @mana/local-llm.
Build aiTierItems as a $derived array of PillDropdownItem:
- Three tier toggles: Browser (Gemma 4), Server (Gemma 4),
Cloud (Gemini). Each shows active checkmark when enabled.
Clicking toggles the tier in/out of allowedTiers. Browser
toggle hidden when WebGPU isn't available.
- Browser model status line: "✓ Modell geladen" (disabled,
green) or "Lade... X%" (disabled, progress) or "Modell
laden (~500 MB)" (clickable, triggers loadLocalLlm).
Only shown when browser tier is enabled.
- Divider + "KI-Einstellungen" link to /settings for the
full configuration (cloud consent, behavior toggles, etc.)
Build currentAiTierLabel as privacy-sorted first-active-tier
short name: "Browser" or "Server" or "Cloud" or "Aus".
Wire all three to PillNavigation via showAiTierSelector={true}
+ {aiTierItems} + {currentAiTierLabel}.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0f7ab60397
commit
a9956c0009
3 changed files with 97 additions and 0 deletions
|
|
@ -39,6 +39,8 @@
|
|||
import { linkLocalStore, linkMutations } from '@mana/shared-links';
|
||||
import { manaStore } from '$lib/data/local-store';
|
||||
import { startLlmQueue, stopLlmQueue } from '$lib/llm-queue';
|
||||
import { llmSettingsState, updateLlmSettings, tierLabel, type LlmTier } from '@mana/shared-llm';
|
||||
import { isLocalLlmSupported, getLocalLlmStatus, loadLocalLlm } from '@mana/local-llm';
|
||||
import {
|
||||
startMemoroLlmWatcher,
|
||||
stopMemoroLlmWatcher,
|
||||
|
|
@ -162,6 +164,73 @@
|
|||
);
|
||||
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
|
||||
|
||||
// ── AI Tier Selector (PillNav dropdown) ─────────────────
|
||||
const webgpuSupported = isLocalLlmSupported();
|
||||
const localLlmStatus = getLocalLlmStatus();
|
||||
const llmSettings = $derived(llmSettingsState.current);
|
||||
|
||||
function toggleAiTier(tier: LlmTier) {
|
||||
const current = llmSettings.allowedTiers;
|
||||
const next = current.includes(tier)
|
||||
? current.filter((t: LlmTier) => t !== tier)
|
||||
: [...current, tier];
|
||||
updateLlmSettings({ allowedTiers: next });
|
||||
}
|
||||
|
||||
const TIER_TOGGLE_LIST: Array<{ tier: LlmTier; shortLabel: string }> = [
|
||||
{ tier: 'browser', shortLabel: 'Browser (Gemma 4)' },
|
||||
{ tier: 'mana-server', shortLabel: 'Server (Gemma 4)' },
|
||||
{ tier: 'cloud', shortLabel: 'Cloud (Gemini)' },
|
||||
];
|
||||
|
||||
let aiTierItems = $derived<PillDropdownItem[]>([
|
||||
// Tier toggles
|
||||
...TIER_TOGGLE_LIST.filter((t) => t.tier !== 'browser' || webgpuSupported).map((t) => ({
|
||||
id: `ai-tier-${t.tier}`,
|
||||
label: t.shortLabel,
|
||||
active: llmSettings.allowedTiers.includes(t.tier),
|
||||
onClick: () => toggleAiTier(t.tier),
|
||||
})),
|
||||
// Browser model status / load button
|
||||
...(llmSettings.allowedTiers.includes('browser') && webgpuSupported
|
||||
? [
|
||||
{
|
||||
id: 'ai-browser-status',
|
||||
label:
|
||||
localLlmStatus.current.state === 'ready'
|
||||
? '✓ Modell geladen'
|
||||
: localLlmStatus.current.state === 'downloading'
|
||||
? `Lade… ${((localLlmStatus.current as { progress: number }).progress * 100).toFixed(0)}%`
|
||||
: 'Modell laden (~500 MB)',
|
||||
disabled: localLlmStatus.current.state === 'ready',
|
||||
onClick:
|
||||
localLlmStatus.current.state !== 'ready' ? () => void loadLocalLlm() : undefined,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// Divider + settings link
|
||||
{ id: 'ai-divider', label: '', divider: true },
|
||||
{
|
||||
id: 'ai-settings',
|
||||
label: 'KI-Einstellungen',
|
||||
icon: 'settings',
|
||||
onClick: () => goto('/settings'),
|
||||
},
|
||||
]);
|
||||
|
||||
let currentAiTierLabel = $derived.by(() => {
|
||||
const active = llmSettings.allowedTiers;
|
||||
if (active.length === 0) return 'Aus';
|
||||
// Show the first (privacy-sorted) tier's short name
|
||||
const sorted = [...active].sort(
|
||||
(a, b) =>
|
||||
TIER_TOGGLE_LIST.findIndex((t) => t.tier === a) -
|
||||
TIER_TOGGLE_LIST.findIndex((t) => t.tier === b)
|
||||
);
|
||||
const first = TIER_TOGGLE_LIST.find((t) => t.tier === sorted[0]);
|
||||
return first ? first.shortLabel.split(' (')[0] : 'KI';
|
||||
});
|
||||
|
||||
// ── User / Guest awareness ──────────────────────────────
|
||||
let userEmail = $derived(
|
||||
authStore.isAuthenticated ? authStore.user?.email || $_('nav.menu') : ''
|
||||
|
|
@ -639,6 +708,9 @@
|
|||
loginHref="/login"
|
||||
primaryColor="#6366f1"
|
||||
showAppSwitcher={true}
|
||||
showAiTierSelector={true}
|
||||
{aiTierItems}
|
||||
{currentAiTierLabel}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue