ManimCat / frontend /src /lib /settings.ts
Bin29's picture
Sync from main: 68df783 feat: support multimodal studio reference images
d47b053
import type { AIProvider, AIProviderType, SettingsConfig } from '../types/api';
const SETTINGS_KEY = 'manimcat_settings';
const SETTINGS_VERSION_KEY = 'manimcat_settings_version';
const SETTINGS_VERSION = '3';
export const DEFAULT_SETTINGS: SettingsConfig = {
api: {
manimcatApiKey: '',
providers: [],
activeProviderId: null
},
video: {
quality: 'low',
frameRate: 15,
timeout: 1200,
bgm: true
}
};
function getFirstListValue(input: string): string {
return input
.split(/[\n,]+/g)
.map((item) => item.trim())
.find(Boolean) || '';
}
function createProviderId(): string {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
return `provider_${Date.now()}_${Math.random().toString(16).slice(2)}`;
}
function normalizeProviderType(type: unknown): AIProviderType {
return type === 'google' ? 'google' : 'openai';
}
function sanitizeProviders(raw: unknown): AIProvider[] {
if (!Array.isArray(raw)) {
return [];
}
const providers: AIProvider[] = raw
.filter((item) => item && typeof item === 'object')
.map((item, index) => {
const provider = item as Partial<AIProvider>;
const id = typeof provider.id === 'string' && provider.id.trim() ? provider.id.trim() : createProviderId();
const name =
typeof provider.name === 'string' && provider.name.trim() ? provider.name.trim() : `Provider ${index + 1}`;
return {
id,
name,
type: normalizeProviderType(provider.type),
apiUrl: typeof provider.apiUrl === 'string' ? provider.apiUrl : '',
apiKey: typeof provider.apiKey === 'string' ? provider.apiKey : '',
model: typeof provider.model === 'string' ? provider.model : '',
};
});
const seen = new Set<string>();
return providers.map((provider) => {
if (!seen.has(provider.id)) {
seen.add(provider.id);
return provider;
}
const id = createProviderId();
seen.add(id);
return { ...provider, id };
});
}
function createDefaultSettings(): SettingsConfig {
return {
api: { ...DEFAULT_SETTINGS.api },
video: { ...DEFAULT_SETTINGS.video }
};
}
function sanitizeSettings(raw: unknown): SettingsConfig {
if (!raw || typeof raw !== 'object') {
return createDefaultSettings();
}
const parsed = raw as Partial<SettingsConfig>;
const quality = parsed.video?.quality;
const frameRate = parsed.video?.frameRate;
const timeout = parsed.video?.timeout;
const bgm = parsed.video?.bgm;
const providers = sanitizeProviders((parsed.api as unknown as { providers?: unknown } | undefined)?.providers);
const activeProviderIdRaw = (parsed.api as unknown as { activeProviderId?: unknown } | undefined)?.activeProviderId;
const activeProviderId =
typeof activeProviderIdRaw === 'string' && providers.some((provider) => provider.id === activeProviderIdRaw)
? activeProviderIdRaw
: activeProviderIdRaw === null
? null
: providers.length > 0
? providers[0].id
: null;
return {
api: {
manimcatApiKey:
typeof (parsed.api as unknown as { manimcatApiKey?: unknown } | undefined)?.manimcatApiKey === 'string'
? ((parsed.api as unknown as { manimcatApiKey: string }).manimcatApiKey)
: DEFAULT_SETTINGS.api.manimcatApiKey,
providers,
activeProviderId
},
video: {
quality: quality === 'low' || quality === 'medium' || quality === 'high' ? quality : DEFAULT_SETTINGS.video.quality,
frameRate: typeof frameRate === 'number' ? frameRate : DEFAULT_SETTINGS.video.frameRate,
timeout: typeof timeout === 'number' ? timeout : DEFAULT_SETTINGS.video.timeout,
bgm: typeof bgm === 'boolean' ? bgm : DEFAULT_SETTINGS.video.bgm
}
};
}
function splitListInput(input: string | undefined): string[] {
if (!input) {
return [];
}
return input
.split(/[\n,]+/g)
.map((item) => item.trim())
.filter(Boolean);
}
function valueByIndex(values: string[], index: number): string {
if (values.length === 0) {
return '';
}
if (values.length === 1) {
return values[0];
}
return values[index] || '';
}
function guessProviderType(apiUrl: string): AIProviderType {
const normalized = apiUrl.toLowerCase();
if (normalized.includes('generativelanguage.googleapis.com') || normalized.includes('/v1beta/openai')) {
return 'google';
}
return 'openai';
}
function migrateLegacyApiToProviders(legacy: { apiUrl?: string; apiKey?: string; model?: string }): AIProvider[] {
const urls = splitListInput(legacy.apiUrl);
const keys = splitListInput(legacy.apiKey);
const models = splitListInput(legacy.model);
const maxCount = Math.max(urls.length, keys.length, models.length, 0);
if (maxCount === 0) {
return [];
}
const providers: AIProvider[] = [];
for (let i = 0; i < maxCount; i += 1) {
const apiUrl = valueByIndex(urls, i);
const apiKey = valueByIndex(keys, i);
const model = valueByIndex(models, i);
if (!apiUrl && !apiKey && !model) {
continue;
}
providers.push({
id: createProviderId(),
name: maxCount > 1 ? `Provider ${i + 1}` : 'Default Provider',
type: guessProviderType(apiUrl),
apiUrl,
apiKey,
model,
});
}
return providers;
}
function migrateSettings(raw: unknown, fromVersion: string | null): SettingsConfig {
if (!raw || typeof raw !== 'object') {
return createDefaultSettings();
}
const parsed = raw as Record<string, unknown>;
const api = (parsed.api && typeof parsed.api === 'object' ? (parsed.api as Record<string, unknown>) : null);
const isLegacyApiShape = Boolean(api && typeof api.apiUrl === 'string' && !('providers' in api));
const normalizedToV3: unknown = isLegacyApiShape
? {
...parsed,
api: {
manimcatApiKey: typeof api?.manimcatApiKey === 'string' ? api.manimcatApiKey : '',
providers: migrateLegacyApiToProviders({
apiUrl: typeof api?.apiUrl === 'string' ? api.apiUrl : '',
apiKey: typeof api?.apiKey === 'string' ? api.apiKey : '',
model: typeof api?.model === 'string' ? api.model : '',
}),
activeProviderId: null,
},
}
: raw;
const sanitized = sanitizeSettings(normalizedToV3);
// v1 -> v2: 旧默认值为 600 秒,升级为 1200 秒作为新默认层级
if ((fromVersion === null || fromVersion === '1') && sanitized.video.timeout === 600) {
return {
...sanitized,
video: {
...sanitized.video,
timeout: 1200
}
};
}
return sanitized;
}
export function loadSettings(): SettingsConfig {
const saved = localStorage.getItem(SETTINGS_KEY);
if (!saved) {
return createDefaultSettings();
}
const version = localStorage.getItem(SETTINGS_VERSION_KEY);
if (version !== SETTINGS_VERSION) {
try {
const migrated = migrateSettings(JSON.parse(saved), version);
saveSettings(migrated);
return migrated;
} catch {
return createDefaultSettings();
}
}
try {
return sanitizeSettings(JSON.parse(saved));
} catch {
return createDefaultSettings();
}
}
export function saveSettings(settings: SettingsConfig): void {
const sanitized = sanitizeSettings(settings);
localStorage.setItem(SETTINGS_KEY, JSON.stringify(sanitized));
localStorage.setItem(SETTINGS_VERSION_KEY, SETTINGS_VERSION);
const defaultManimcatApiKey = getFirstListValue(sanitized.api.manimcatApiKey);
if (defaultManimcatApiKey) {
localStorage.setItem('manimcat_api_key', defaultManimcatApiKey);
} else {
localStorage.removeItem('manimcat_api_key');
}
}