Files
MyGoNavi/frontend/src/utils/aiComposerNotice.ts
2026-06-22 15:12:42 +08:00

223 lines
7.2 KiB
TypeScript

import { t as catalogTranslate } from '../i18n/catalog';
import type { AIChatReadinessIssue } from '../components/ai/aiChatReadiness';
import { formatAIChatProviderIssueLabels } from '../components/ai/aiChatReadiness';
export type AIComposerNoticeTone = 'warning' | 'error';
export type AIComposerNoticeAction = 'open-settings' | 'reload-models';
export interface AIComposerNotice {
tone: AIComposerNoticeTone;
title: string;
description: string;
action?: {
key: AIComposerNoticeAction;
label: string;
};
}
export type AIComposerNoticeDescriptor =
| { kind: 'missing_provider' }
| { kind: 'missing_model' }
| { kind: 'provider_incomplete'; issues?: AIChatReadinessIssue[] }
| { kind: 'model_fetch_failed'; detail?: string | number | boolean | null | undefined };
export type AIComposerNoticeTranslator = (
key: string,
params?: Record<string, string | number | boolean | null | undefined>
) => string;
const catalogTranslateEn = (
key: string,
params?: Record<string, string | number | boolean | null | undefined>,
) => catalogTranslate('en-US', key, params);
const translateWithFallback = (
t: AIComposerNoticeTranslator | undefined,
key: string,
fallback: string,
params?: Record<string, string | number | boolean | null | undefined>,
): string => {
if (!t) {
return fallback;
}
const translated = t(key, params);
return translated && translated !== key ? translated : fallback;
};
const getProviderFallbackLabel = (t?: AIComposerNoticeTranslator): string =>
translateWithFallback(
t,
'ai_chat.input.status.provider_fallback_name',
catalogTranslateEn('ai_chat.input.status.provider_fallback_name'),
);
const getIssueSeparator = (t?: AIComposerNoticeTranslator): string =>
translateWithFallback(
t,
'ai_chat.input.status.issue.separator',
catalogTranslateEn('ai_chat.input.status.issue.separator'),
);
const joinIssueLabels = (labels: string[], t?: AIComposerNoticeTranslator): string =>
labels.join(getIssueSeparator(t));
const buildNoticeAction = (
key: AIComposerNoticeAction,
labelKey: string,
fallbackLabel: string,
t?: AIComposerNoticeTranslator,
): AIComposerNotice['action'] => ({
key,
label: translateWithFallback(t, labelKey, fallbackLabel),
});
export const buildMissingProviderNotice = (t?: AIComposerNoticeTranslator): AIComposerNotice => ({
tone: 'warning',
title: translateWithFallback(
t,
'ai_chat.composer_notice.missing_provider.title',
catalogTranslateEn('ai_chat.composer_notice.missing_provider.title'),
),
description: translateWithFallback(
t,
'ai_chat.composer_notice.missing_provider.description',
catalogTranslateEn('ai_chat.composer_notice.missing_provider.description'),
),
action: buildNoticeAction(
'open-settings',
'ai_chat.input.status.action.open_settings',
catalogTranslateEn('ai_chat.input.status.action.open_settings'),
t,
),
});
export const buildMissingModelNotice = (t?: AIComposerNoticeTranslator): AIComposerNotice => ({
tone: 'warning',
title: translateWithFallback(
t,
'ai_chat.composer_notice.missing_model.title',
catalogTranslateEn('ai_chat.composer_notice.missing_model.title'),
),
description: translateWithFallback(
t,
'ai_chat.composer_notice.missing_model.description',
catalogTranslateEn('ai_chat.composer_notice.missing_model.description'),
),
action: buildNoticeAction(
'reload-models',
'ai_chat.input.status.action.reload_models',
catalogTranslateEn('ai_chat.input.status.action.reload_models'),
t,
),
});
export const buildIncompleteProviderNotice = (
issues: AIChatReadinessIssue[] = [],
t?: AIComposerNoticeTranslator,
): AIComposerNotice => {
const filteredIssues = issues.filter((issue) => issue !== 'missing_selected_model');
const missingLabels = formatAIChatProviderIssueLabels(filteredIssues, t);
const providerFallbackLabel = getProviderFallbackLabel(t);
const issuesText = joinIssueLabels(missingLabels, t);
const fallbackTitle = missingLabels.length > 0
? catalogTranslateEn('ai_chat.input.status.provider_incomplete.title', {
provider: catalogTranslateEn('ai_chat.input.status.provider_fallback_name'),
issues: joinIssueLabels(
formatAIChatProviderIssueLabels(filteredIssues),
undefined,
),
})
: catalogTranslateEn('ai_chat.input.status.provider_fallback_name');
return {
tone: 'error',
title: translateWithFallback(
t,
'ai_chat.input.status.provider_incomplete.title',
fallbackTitle,
{
provider: providerFallbackLabel,
issues: issuesText,
},
),
description: translateWithFallback(
t,
'ai_chat.input.status.provider_incomplete.description',
catalogTranslateEn('ai_chat.input.status.provider_incomplete.description'),
),
action: buildNoticeAction(
'open-settings',
'ai_chat.input.status.action.fix_provider',
catalogTranslateEn('ai_chat.input.status.action.fix_provider'),
t,
),
};
};
export function buildModelFetchFailedNotice(
t: AIComposerNoticeTranslator,
error?: string | number | boolean | null | undefined,
): AIComposerNotice;
export function buildModelFetchFailedNotice(
error?: string | number | boolean | null | undefined,
): AIComposerNotice;
export function buildModelFetchFailedNotice(
tOrError?: AIComposerNoticeTranslator | string | number | boolean | null,
error?: string | number | boolean | null | undefined,
): AIComposerNotice {
const hasTranslator = typeof tOrError === 'function';
const t = hasTranslator ? tOrError : undefined;
const rawDetail = hasTranslator ? error : tOrError;
const detail = String(rawDetail ?? '').trim();
return {
tone: 'error',
title: translateWithFallback(
t,
'ai_chat.composer_notice.model_fetch_failed.title',
catalogTranslateEn('ai_chat.composer_notice.model_fetch_failed.title'),
),
description: t
? detail
? translateWithFallback(
t,
'ai_chat.composer_notice.model_fetch_failed.detail_description',
catalogTranslateEn('ai_chat.composer_notice.model_fetch_failed.detail_description', { detail }),
{ detail },
)
: translateWithFallback(
t,
'ai_chat.composer_notice.model_fetch_failed.default_description',
catalogTranslateEn('ai_chat.composer_notice.model_fetch_failed.default_description'),
)
: detail || catalogTranslateEn('ai_chat.composer_notice.model_fetch_failed.default_description'),
action: buildNoticeAction(
'reload-models',
'ai_chat.input.status.action.reload_models',
catalogTranslateEn('ai_chat.input.status.action.reload_models'),
t,
),
};
}
export const buildAIComposerNotice = (
t: AIComposerNoticeTranslator,
descriptor: AIComposerNoticeDescriptor | null
): AIComposerNotice | null => {
if (!descriptor) {
return null;
}
if (descriptor.kind === 'missing_provider') {
return buildMissingProviderNotice(t);
}
if (descriptor.kind === 'missing_model') {
return buildMissingModelNotice(t);
}
if (descriptor.kind === 'provider_incomplete') {
return buildIncompleteProviderNotice(descriptor.issues, t);
}
return buildModelFetchFailedNotice(t, descriptor.detail);
};