File size: 5,461 Bytes
9d2d895 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | /**
* Quick Settings — Web-only user preferences for AI pipeline and map behavior.
* Desktop (Tauri) manages AI config via its own settings window.
*
* TODO: Migrate panel visibility, sources, and language selector into this
* settings hub once the UI is extended with additional sections.
*/
import { isDesktopRuntime } from './runtime';
const STORAGE_KEY_BROWSER_MODEL = 'wm-ai-flow-browser-model';
const STORAGE_KEY_CLOUD_LLM = 'wm-ai-flow-cloud-llm';
const STORAGE_KEY_MAP_NEWS_FLASH = 'wm-map-news-flash';
const STORAGE_KEY_HEADLINE_MEMORY = 'wm-headline-memory';
const STORAGE_KEY_BADGE_ANIMATION = 'wm-badge-animation';
const STORAGE_KEY_STREAM_QUALITY = 'wm-stream-quality';
const EVENT_NAME = 'ai-flow-changed';
const STREAM_QUALITY_EVENT = 'stream-quality-changed';
export interface AiFlowSettings {
browserModel: boolean;
cloudLlm: boolean;
mapNewsFlash: boolean;
headlineMemory: boolean;
badgeAnimation: boolean;
}
function readBool(key: string, defaultValue: boolean): boolean {
try {
const raw = localStorage.getItem(key);
if (raw === null) return defaultValue;
return raw === 'true';
} catch {
return defaultValue;
}
}
function writeBool(key: string, value: boolean): void {
try {
localStorage.setItem(key, String(value));
} catch {
// Quota or private-browsing; silently ignore
}
}
const STORAGE_KEY_MAP: Record<keyof AiFlowSettings, string> = {
browserModel: STORAGE_KEY_BROWSER_MODEL,
cloudLlm: STORAGE_KEY_CLOUD_LLM,
mapNewsFlash: STORAGE_KEY_MAP_NEWS_FLASH,
headlineMemory: STORAGE_KEY_HEADLINE_MEMORY,
badgeAnimation: STORAGE_KEY_BADGE_ANIMATION,
};
const DEFAULTS: AiFlowSettings = {
browserModel: false,
cloudLlm: true,
mapNewsFlash: true,
headlineMemory: false,
badgeAnimation: false,
};
export function getAiFlowSettings(): AiFlowSettings {
return {
browserModel: readBool(STORAGE_KEY_BROWSER_MODEL, DEFAULTS.browserModel),
cloudLlm: readBool(STORAGE_KEY_CLOUD_LLM, DEFAULTS.cloudLlm),
mapNewsFlash: readBool(STORAGE_KEY_MAP_NEWS_FLASH, DEFAULTS.mapNewsFlash),
headlineMemory: readBool(STORAGE_KEY_HEADLINE_MEMORY, DEFAULTS.headlineMemory),
badgeAnimation: readBool(STORAGE_KEY_BADGE_ANIMATION, DEFAULTS.badgeAnimation),
};
}
/**
* Effective Headline Memory state. Headline Memory implementation requires
* a local embeddings model in the ML worker, so on web it can only function
* when the Browser Local Model parent toggle is also enabled — otherwise
* we'd silently download/run an ML model the user opted out of via the
* parent toggle. The persisted value is preserved (the settings UI reads
* `getAiFlowSettings().headlineMemory` for the raw value) so re-enabling
* Browser Local Model restores the user's prior Headline Memory choice.
*
* The Browser Local Model toggle is web-only — `preferences-content.ts`
* skips rendering it on desktop, and `App.ts` initializes the ML worker
* unconditionally on desktop. So the parent gate must be skipped on
* desktop, otherwise Headline Memory would be silently dead on every
* desktop install (the hidden web key never flips to true).
*/
export function isHeadlineMemoryEnabled(): boolean {
const headline = readBool(STORAGE_KEY_HEADLINE_MEMORY, DEFAULTS.headlineMemory);
if (!headline) return false;
if (isDesktopRuntime()) return true;
const browser = readBool(STORAGE_KEY_BROWSER_MODEL, DEFAULTS.browserModel);
return browser;
}
export function setAiFlowSetting(key: keyof AiFlowSettings, value: boolean): void {
writeBool(STORAGE_KEY_MAP[key], value);
window.dispatchEvent(new CustomEvent(EVENT_NAME, { detail: { key } }));
}
export function isAnyAiProviderEnabled(): boolean {
const s = getAiFlowSettings();
return s.cloudLlm || s.browserModel;
}
export function subscribeAiFlowChange(cb: (changedKey?: keyof AiFlowSettings) => void): () => void {
const handler = (e: Event) => {
const detail = (e as CustomEvent).detail as { key?: keyof AiFlowSettings } | undefined;
cb(detail?.key);
};
window.addEventListener(EVENT_NAME, handler);
return () => window.removeEventListener(EVENT_NAME, handler);
}
// ── Stream Quality ──
export type StreamQuality = 'auto' | 'small' | 'medium' | 'large' | 'hd720';
export const STREAM_QUALITY_OPTIONS: { value: StreamQuality; label: string }[] = [
{ value: 'auto', label: 'Auto' },
{ value: 'small', label: 'Low (360p)' },
{ value: 'medium', label: 'Medium (480p)' },
{ value: 'large', label: 'High (480p+)' },
{ value: 'hd720', label: 'HD (720p)' },
];
export function getStreamQuality(): StreamQuality {
try {
const raw = localStorage.getItem(STORAGE_KEY_STREAM_QUALITY);
if (raw && ['auto', 'small', 'medium', 'large', 'hd720'].includes(raw)) return raw as StreamQuality;
} catch { /* ignore */ }
return 'auto';
}
export function setStreamQuality(quality: StreamQuality): void {
try {
localStorage.setItem(STORAGE_KEY_STREAM_QUALITY, quality);
} catch { /* ignore */ }
window.dispatchEvent(new CustomEvent(STREAM_QUALITY_EVENT, { detail: { quality } }));
}
export function subscribeStreamQualityChange(cb: (quality: StreamQuality) => void): () => void {
const handler = (e: Event) => {
const detail = (e as CustomEvent).detail as { quality: StreamQuality };
cb(detail.quality);
};
window.addEventListener(STREAM_QUALITY_EVENT, handler);
return () => window.removeEventListener(STREAM_QUALITY_EVENT, handler);
}
|