mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:41:09 +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"
|
||||
|
|
|
|||
|
|
@ -241,6 +241,12 @@
|
|||
showLanguageSwitcher?: boolean;
|
||||
/** Show theme toggle (standalone button, hidden if showThemeVariants is true) */
|
||||
showThemeToggle?: boolean;
|
||||
/** Show AI tier selector dropdown */
|
||||
showAiTierSelector?: boolean;
|
||||
/** AI tier dropdown items (each representing a toggleable tier) */
|
||||
aiTierItems?: PillDropdownItem[];
|
||||
/** Current AI tier label, e.g. "Browser" or "Server" */
|
||||
currentAiTierLabel?: string;
|
||||
/** Primary color for active state (CSS custom property or hex) */
|
||||
primaryColor?: string;
|
||||
/** Elements to prepend before nav items (tab groups, dividers, nav items) */
|
||||
|
|
@ -333,6 +339,9 @@
|
|||
themeVariantItems = [],
|
||||
currentThemeVariantLabel = 'Theme',
|
||||
showThemeVariants = false,
|
||||
showAiTierSelector = false,
|
||||
aiTierItems = [],
|
||||
currentAiTierLabel = 'KI',
|
||||
themeMode = 'system',
|
||||
onThemeModeChange,
|
||||
appItems = [],
|
||||
|
|
@ -651,6 +660,16 @@
|
|||
</PillDropdown>
|
||||
{/if}
|
||||
|
||||
<!-- AI Tier Selector -->
|
||||
{#if showAiTierSelector && aiTierItems.length > 0}
|
||||
<PillDropdown
|
||||
items={aiTierItems}
|
||||
direction={dropdownDirection}
|
||||
label={currentAiTierLabel}
|
||||
icon="cpu"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Theme Toggle (only show when not using theme variants dropdown) -->
|
||||
{#if showThemeToggle && onToggleTheme && !showThemeVariants}
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -167,6 +167,12 @@ export interface PillNavigationProps {
|
|||
showLanguageSwitcher?: boolean;
|
||||
/** Show theme toggle */
|
||||
showThemeToggle?: boolean;
|
||||
/** Show AI tier selector dropdown */
|
||||
showAiTierSelector?: boolean;
|
||||
/** AI tier dropdown items (each representing a toggleable tier) */
|
||||
aiTierItems?: PillDropdownItem[];
|
||||
/** Current AI tier label, e.g. "Browser" or "Server" */
|
||||
currentAiTierLabel?: string;
|
||||
/** Primary color for active state */
|
||||
primaryColor?: string;
|
||||
/** Elements to prepend before nav items (tab groups, dividers, nav items) */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue