import { useEffect, useState } from 'react' import { api, ApiError } from '../api/client' import { DEFAULT_AI_CREDENTIALS, getApiKeyStatus, isMaskedKey, normalizeProviderSettings, normalizeSupportedProvider, PROVIDER_KEY_LABELS, PROVIDER_OPTIONS, PROVIDER_ORDER, type ProviderApiKeys, type ProviderId, } from '../lib/aiCredentials' import type { ThreadsAccountAiSettingsData } from '../types/api' import { Badge, Button, ErrorText, Field, Input, SectionTitle, Select, SuccessText } from './ui' type AccountAiSettingsProps = { accountId: string compact?: boolean } function aiSettingsPath(accountId: string) { return `/api/v1/threads-accounts/${encodeURIComponent(accountId)}/ai-settings` } function parseConfigured(raw: Record | undefined): Record { const base = getApiKeyStatus({}) if (!raw) return base for (const provider of PROVIDER_ORDER) { const value = raw[provider] base[provider] = value === true || value === 'true' } return base } export function AccountAiSettings({ accountId, compact }: AccountAiSettingsProps) { const [settings, setSettings] = useState(null) const [keyInputs, setKeyInputs] = useState({}) const [configured, setConfigured] = useState>(() => getApiKeyStatus({})) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [error, setError] = useState('') const [message, setMessage] = useState('') const load = async () => { if (!accountId) return setLoading(true) setError('') try { const data = await api.get(aiSettingsPath(accountId), { auth: true }) setSettings({ ...data, ...normalizeProviderSettings({ provider: data.provider, model: data.model, research_provider: data.research_provider, research_model: data.research_model, }), }) setConfigured(parseConfigured(data.api_keys_configured)) setKeyInputs(data.api_keys ?? {}) } catch (e) { setError(e instanceof ApiError ? e.message : '載入 AI 設定失敗') } finally { setLoading(false) } } useEffect(() => { load().catch(() => undefined) }, [accountId]) const save = async () => { if (!accountId || !settings) return setSaving(true) setError('') setMessage('') try { const apiKeys: ProviderApiKeys = {} for (const provider of PROVIDER_ORDER) { const trimmed = keyInputs[provider]?.trim() if (!trimmed || isMaskedKey(trimmed)) continue apiKeys[provider] = trimmed } const normalized = normalizeProviderSettings({ provider: settings.provider, model: settings.model, research_provider: settings.research_provider, research_model: settings.research_model, }) const data = await api.put( aiSettingsPath(accountId), { provider: normalized.provider, model: normalized.model, research_provider: normalized.research_provider, research_model: normalized.research_model, api_keys: apiKeys, }, { auth: true }, ) setSettings({ ...data, ...normalizeProviderSettings({ provider: data.provider, model: data.model, research_provider: data.research_provider, research_model: data.research_model, }), }) setConfigured(parseConfigured(data.api_keys_configured)) setKeyInputs(data.api_keys ?? {}) setMessage('AI 設定已儲存') } catch (e) { setError(e instanceof ApiError ? e.message : '儲存失敗') } finally { setSaving(false) } } const provider = normalizeSupportedProvider(settings?.provider) || DEFAULT_AI_CREDENTIALS.provider const researchProvider = normalizeSupportedProvider(settings?.research_provider ?? provider) const providerOption = PROVIDER_OPTIONS.find((item) => item.value === provider) const researchOption = PROVIDER_OPTIONS.find((item) => item.value === researchProvider) const currentProviderConfigured = configured[provider] if (!accountId) { return

請先從頂部選擇 Threads 經營帳號。

} return (
{!compact ? (
AI API Key

同一登入帳號下的所有經營帳號共用。8D 分析、產文等任務會讀取這組 provider 與 key。

) : null} {loading ? (

載入 AI 設定…

) : settings ? ( <>
{currentProviderConfigured ? '主要 Provider 已設定 key' : '主要 Provider 尚未設定 key'}
{PROVIDER_ORDER.map((providerId) => { const meta = PROVIDER_KEY_LABELS[providerId] const isConfigured = configured[providerId] return (
{meta.label} {isConfigured ? '已設定' : '未設定'}

{meta.hint}

setKeyInputs((prev) => ({ ...prev, [providerId]: e.target.value })) } placeholder={isConfigured ? '留空則保留現有 key' : '貼上 API key'} autoComplete="off" /> {meta.docsUrl ? ( 取得 {meta.label} API key → ) : null}
) })}

Key 只存在後端資料庫,綁定此登入帳號。留空或維持遮罩值不會覆寫已儲存的 key。

) : (

無法載入設定。

)}
) }