Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/amintacccp_githubstarsmanager.json
T

1 line
38 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "amintacccp/githubstarsmanager", "files": {"src/components/settings/AIConfigPanel.tsx": {"0ef189fcb0cd9b2e0565c38260dca6f0d01a0fe1": {"t": 1785674304.0150588, "n": 35700, "h": "3110874923e16dc7", "c": "import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';\nimport { Bot, Plus, Edit3, Trash2, Save, X, TestTube, RefreshCw, MessageSquare, Eye, EyeOff, AlertCircle } from 'lucide-react';\nimport { AIConfig, AIApiType, AIReasoningEffort, MiMoPlan } from '../../types';\nimport { useAppStore } from '../../store/useAppStore';\nimport { AIService } from '../../services/aiService';\nimport { buildFinalApiUrl } from '../../utils/apiUrlBuilder';\nimport { SliderInput } from '../ui/SliderInput';\nimport { useDialog } from '../../hooks/useDialog';\n\ninterface AIConfigPanelProps {\n t: (zh: string, en: string) => string;\n}\n\ntype AIFormState = {\n name: string;\n apiType: AIApiType;\n baseUrl: string;\n apiKey: string;\n model: string;\n customPrompt: string;\n useCustomPrompt: boolean;\n concurrency: number;\n reasoningEffort: '' | AIReasoningEffort;\n mimoPlan: MiMoPlan;\n};\n\nconst MIMO_PLAN_ENDPOINTS: Record<MiMoPlan, string> = {\n api: 'https://api.xiaomimimo.com/v1',\n 'token-plan': 'https://token-plan-cn.xiaomimimo.com/v1',\n};\n\nconst DEFAULT_API_ENDPOINTS: Record<AIApiType, string> = {\n openai: 'https://api.openai.com/v1',\n 'openai-responses': 'https://api.openai.com/v1',\n claude: 'https://api.anthropic.com/v1',\n gemini: 'https://generativelanguage.googleapis.com/v1beta',\n deepseek: 'https://api.deepseek.com',\n mimo: MIMO_PLAN_ENDPOINTS.api,\n 'openai-compatible': '',\n};\n\nfunction getEndpointPlaceholder(apiType: AIApiType, mimoPlan: MiMoPlan): string {\n switch (apiType) {\n case 'openai':\n case 'openai-responses':\n return 'https://api.openai.com/v1';\n case 'claude':\n return 'https://api.anthropic.com/v1';\n case 'deepseek':\n return 'https://api.deepseek.com';\n case 'mimo':\n return MIMO_PLAN_ENDPOINTS[mimoPlan];\n case 'openai-compatible':\n return 'https://integrate.api.nvidia.com/v1/chat/completions';\n default:\n return 'https://generativelanguage.googleapis.com/v1beta';\n }\n}\n\nfunction getEndpointHelpText(apiType: AIApiType, t: (zh: string, en: string) => string): string {\n switch (apiType) {\n case 'openai-compatible':\n return t('填写完整的API调用地址,包含完整路径', 'Enter the full API endpoint URL including the complete path');\n case 'gemini':\n return t('只填到 v1beta 即可,路径会自动生成', 'Only include the version prefix v1beta, the path will be generated automatically');\n case 'deepseek':\n return t('填写到域名即可(如 https://api.deepseek.com),路径会自动生成', 'Only include the domain (e.g. https://api.deepseek.com), the path will be generated automatically');\n case 'mimo':\n return t('填写到 /v1 即可(如 https://api.xiaomimimo.com/v1),路径会自动生成', 'Only include up to /v1 (e.g. https://api.xiaomimimo.com/v1), the path will be generated automatically');\n default:\n return t('只填到版本号即可(如 .../v1 或 .../v1beta),不要包含 /chat/completions、/responses、/messages', 'Only include the version prefix (e.g. .../v1 or .../v1beta). Do not include /chat/completions, /responses, or /messages.');\n }\n}\n\nexport const AIConfigPanel: React.FC<AIConfigPanelProps> = ({ t }) => {\n const {\n aiConfigs,\n activeAIConfig,\n language,\n addAIConfig,\n updateAIConfig,\n deleteAIConfig,\n setActiveAIConfig,\n } = useAppStore();\n\n const { toast, confirm } = useDialog();\n\n const [showForm, setShowForm] = useState(false);\n const [editingId, setEditingId] = useState<string | null>(null);\n const [testingId, setTestingId] = useState<string | null>(null);\n const [testingForm, setTestingForm] = useState(false);\n const [showCustomPrompt, setShowCustomPrompt] = useState(false);\n const [showDefaultPrompt, setShowDefaultPrompt] = useState(false);\n const [notification, setNotification] = useState<{ type: 'success' | 'error' | 'info'; message: string } | null>(null);\n const notificationTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n return () => {\n if (notificationTimerRef.current) {\n clearTimeout(notificationTimerRef.current);\n }\n };\n }, []);\n\n const showNotification = useCallback((type: 'success' | 'error' | 'info', message: string) => {\n if (notificationTimerRef.current) {\n clearTimeout(notificationTimerRef.current);\n }\n setNotification({ type, message });\n notificationTimerRef.current = setTimeout(() => setNotification(null), 3000);\n }, []);\n\n const [form, setForm] = useState<AIFormState>({\n name: '',\n apiType: 'openai',\n baseUrl: 'https://api.openai.com/v1',\n apiKey: '',\n model: '',\n customPrompt: '',\n useCustomPrompt: false,\n concurrency: 1,\n reasoningEffort: '',\n mimoPlan: 'api',\n });\n\n // Auto-fill baseUrl when API type changes\n const prevApiTypeRef = useRef<AIApiType>('openai');\n useEffect(() => {\n if (form.apiType !== prevApiTypeRef.current) {\n const nextDefault = DEFAULT_API_ENDPOINTS[form.apiType];\n const prevDefault = DEFAULT_API_ENDPOINTS[prevApiTypeRef.current];\n if (nextDefault) {\n if (form.baseUrl === '' || form.baseUrl === prevDefault) {\n setForm(prev => ({ ...prev, baseUrl: nextDefault }));\n }\n } else if (form.baseUrl === prevDefault) {\n // Clear baseUrl when switching to a type with no default (e.g., openai-compatible)\n setForm(prev => ({ ...prev, baseUrl: '' }));\n }\n prevApiTypeRef.current = form.apiType;\n }\n }, [form.apiType, form.baseUrl]);\n\n // Auto-fill baseUrl when MiMo plan changes\n const prevMimoPlanRef = useRef<MiMoPlan>('api');\n useEffect(() => {\n if (form.apiType === 'mimo' && form.mimoPlan !== prevMimoPlanRef.current) {\n const prevEndpoint = MIMO_PLAN_ENDPOINTS[prevMimoPlanRef.current];\n if (form.baseUrl === '' || form.baseUrl === prevEndpoint) {\n setForm(prev => ({ ...prev, baseUrl: MIMO_PLAN_ENDPOINTS[form.mimoPlan] }));\n }\n prevMimoPlanRef.current = form.mimoPlan;\n }\n }, [form.apiType, form.baseUrl, form.mimoPlan]);\n\n const resetForm = () => {\n setForm({\n name: '',\n apiType: 'openai',\n baseUrl: DEFAULT_API_ENDPOINTS.openai,\n apiKey: '',\n model: '',\n customPrompt: '',\n useCustomPrompt: false,\n concurrency: 1,\n reasoningEffort: '',\n mimoPlan: 'api',\n });\n setShowForm(false);\n setEditingId(null);\n setShowCustomPrompt(false);\n setShowDefaultPrompt(false);\n prevApiTypeRef.current = 'openai';\n prevMimoPlanRef.current = 'api';\n };\n\n const handleSave = () => {\n if (!form.name || !form.baseUrl || !form.apiKey || !form.model) {\n toast(t('请填写所有必填字段', 'Please fill in all required fields'), 'error');\n return;\n }\n\n if (editingId) {\n const existingConfig = aiConfigs.find(c => c.id === editingId);\n if (existingConfig) {\n const updates: Partial<AIConfig> = {\n name: form.name,\n apiType: form.apiType,\n baseUrl: form.baseUrl.replace(/\\/$/, ''),\n apiKey: form.apiKey,\n model: form.model,\n customPrompt: form.customPrompt || undefined,\n useCustomPrompt: form.useCustomPrompt,\n concurrency: form.concurrency,\n reasoningEffort: form.reasoningEffort || undefined,\n mimoPlan: form.apiType === 'mimo' ? form.mimoPlan : undefined,\n isActive: existingConfig.isActive,\n };\n updateAIConfig(editingId, updates);\n }\n } else {\n const config: AIConfig = {\n id: Date.now().toString(),\n name: form.name,\n apiType: form.apiType,\n baseUrl: form.baseUrl.replace(/\\/$/, ''),\n apiKey: form.apiKey,\n model: form.model,\n isActive: false,\n customPrompt: form.customPrompt || undefined,\n useCustomPrompt: form.useCustomPrompt,\n concurrency: form.concurrency,\n reasoningEffort: form.reasoningEffort || undefined,\n mimoPlan: form.apiType === 'mimo' ? form.mimoPlan : undefined,\n };\n addAIConfig(config);\n }\n\n resetForm();\n };\n\n const handleEdit = (config: AIConfig) => {\n // Sync ref to prevent auto-fill effect from overwriting loaded config\n prevApiTypeRef.current = config.apiType || 'openai';\n prevMimoPlanRef.current = config.mimoPlan || 'api';\n setForm({\n name: config.name,\n apiType: config.apiType || 'openai',\n baseUrl: config.baseUrl,\n apiKey: config.apiKey,\n model: config.model,\n customPrompt: config.customPrompt || '',\n useCustomPrompt: config.useCustomPrompt || false,\n concurrency: config.concurrency || 1,\n reasoningEffort: config.reasoningEffort || '',\n mimoPlan: config.mimoPlan || 'api',\n });\n setEditingId(config.id);\n setShowForm(true);\n setShowCustomPrompt(config.useCustomPrompt || false);\n };\n\n const handleTest = async (config: AIConfig) => {\n setTestingId(config.id);\n try {\n const aiService = new AIService(config, language);\n const result = await aiService.testConnection();\n\n if (result.success) {\n toast(t('AI服务连接成功!', 'AI service connection successful!'), 'success');\n } else {\n toast(result.message, 'error');\n }\n } catch (error) {\n console.error('AI test failed:', error);\n toast(t('AI服务测试失败,请检查网络连接和配置。', 'AI service test failed. Please check network connection and configuration.'), 'error');\n } finally {\n setTestingId(null);\n }\n };\n\n const handleTestForm = async () => {\n if (!form.baseUrl || !form.apiKey || !form.model) {\n toast(t('请先填写API端点、API密钥和模型名称', 'Please fill in API Endpoint, API Key and Model Name first'), 'error');\n return;\n }\n\n setTestingForm(true);\n try {\n const tempConfig: AIConfig = {\n id: '' as string,\n name: form.name || 'Test',\n apiType: form.apiType,\n baseUrl: form.baseUrl.replace(/\\/$/, ''),\n apiKey: form.apiKey,\n model: form.model,\n isActive: false,\n customPrompt: form.customPrompt || undefined,\n useCustomPrompt: form.useCustomPrompt,\n concurrency: form.concurrency,\n reasoningEffort: form.reasoningEffort || undefined,\n };\n\n const aiService = new AIService(tempConfig, language);\n const result = await aiService.testConnection();\n\n if (result.success) {\n toast(t('✅ AI服务连接成功!', '✅ AI service connection successful!'), 'success');\n } else {\n toast(result.message, 'error');\n }\n } catch (error) {\n console.error('AI test failed:', error);\n toast(t('AI服务测试失败,请检查网络连接和配置。', 'AI service test failed. Please check network connection and configuration.'), 'error');\n } finally {\n setTestingForm(false);\n }\n };\n\n const defaultPrompt = useMemo(() => {\n if (language === 'zh') {\n return `请分析以下GitHub仓库信息,并只输出合法JSON对象。不要输出思考过程、Markdown、代码块标记、解释或任何额外文本。\n\n要求:\n- summary:中文概述,说明仓库的主要功能和用途,不超过50字。\n 禁止出现“我们被要求”“只输出JSON”“根据仓库信息”“summary/tags/platforms”等提示词复述。\n- tags:3-5个中文应用类型标签,请优先从提供的分类中选择。\n{CATEGORIES_INFO}\n- platforms:只能从 [\"mac\",\"windows\",\"linux\",\"ios\",\"android\",\"docker\",\"web\",\"cli\"] 中选择;无法判断则为 []。\n\n输出格式:\n{\n \"summary\": \"中文概述\",\n \"tags\": [\"标签1\", \"标签2\", \"标签3\"],\n \"platforms\": [\"web\", \"cli\"]\n}\n\n平台线索:\nDockerfile/docker-compose=docker;CLI/命令行/终端=cli;浏览器/前端/API=web;iOS/Swift/Xcode=ios;Android/Kotlin/Gradle=android;macOS/Homebrew=mac;Windows/.exe/MSI=windows;Linux/systemd/apt=linux。\n\n仓库信息:\n{REPO_INFO}`;\n } else {\n return `Please analyze the following GitHub repository information and only output a valid JSON object. Do not output thinking process, Markdown, code block markers, explanations, or any extra text.\n\nRequirements:\n- summary: A concise English overview explaining the main functionality and purpose, no more than 50 words.\n Do not include prompt restatements such as \"asked to\", \"only output JSON\", \"based on repository information\", or \"summary/tags/platforms\".\n- tags: 3-5 English application type tags, please prioritize from the provided categories.\n{CATEGORIES_INFO}\n- platforms: Must only choose from [\"mac\",\"windows\",\"linux\",\"ios\",\"android\",\"docker\",\"web\",\"cli\"]; use [] if unable to determine.\n\nOutput format:\n{\n \"summary\": \"English overview\",\n \"tags\": [\"tag1\", \"tag2\", \"tag3\"],\n \"platforms\": [\"web\", \"cli\"]\n}\n\nPlatform hints:\nDockerfile/docker-compose=docker; CLI/command-line/terminal=cli; browser/frontend/API=web; iOS/Swift/Xcode=ios; Android/Kotlin/Gradle=android; macOS/Homebrew=mac; Windows/.exe/MSI=windows; Linux/systemd/apt=linux.\n\nRepository information:\n{REPO_INFO}`;\n }\n }, [language]);\n\n const isCustomPromptModified = useMemo(() => {\n return form.customPrompt.trim() !== '' && form.customPrompt !== defaultPrompt;\n }, [form.customPrompt, defaultPrompt]);\n\n const isCustomPromptSameAsDefault = useMemo(() => {\n return form.customPrompt === defaultPrompt;\n }, [form.customPrompt, defaultPrompt]);\n\n const handleUseCustomPromptChange = useCallback((checked: boolean) => {\n setForm(prev => {\n const newCustomPrompt = checked && prev.customPrompt.trim() === '' \n ? defaultPrompt \n : prev.customPrompt;\n return { \n ...prev, \n useCustomPrompt: checked,\n customPrompt: newCustomPrompt\n };\n });\n \n if (checked) {\n setShowCustomPrompt(true);\n setShowDefaultPrompt(false);\n if (form.customPrompt.trim() === '') {\n showNotification('info', t('已自动填充默认提示词,您可以进行修改', 'Default prompt auto-filled, you can modify it'));\n }\n } else {\n setShowCustomPrompt(false);\n }\n }, [defaultPrompt, form.customPrompt, showNotification, t]);\n\n const handleToggleDefaultPrompt = useCallback(() => {\n if (showCustomPrompt) {\n showNotification('info', t('请先关闭自定义提示词编辑区域', 'Please close the custom prompt editor first'));\n return;\n }\n setShowDefaultPrompt(prev => !prev);\n }, [showCustomPrompt, showNotification, t]);\n\n const handleRestoreDefaultPrompt = useCallback(async () => {\n if (isCustomPromptSameAsDefault) {\n showNotification('info', t('当前提示词已是默认值', 'Current prompt is already the default'));\n return;\n }\n\n if (isCustomPromptModified) {\n const confirmed = await confirm(\n t('确定要恢复默认提示词吗?', 'Restore Default Prompt?'),\n t('这将覆盖您当前的修改。', 'This will overwrite your current changes.'),\n { type: 'warning' }\n );\n if (!confirmed) return;\n }\n\n setForm(prev => ({ ...prev, customPrompt: defaultPrompt }));\n showNotification('success', t('已恢复默认提示词', 'Default prompt restored'));\n }, [defaultPrompt, isCustomPromptModified, isCustomPromptSameAsDefault, showNotification, t, confirm]);\n\n return (\n <div className=\"space-y-6\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center space-x-3\">\n <Bot className=\"w-6 h-6 text-gray-700 dark:text-text-secondary \" />\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-text-primary\">\n {t('AI服务配置', 'AI Service Configuration')}\n </h3>\n </div>\n <button\n onClick={() => setShowForm(true)}\n className=\"flex items-center space-x-2 px-4 py-2 bg-brand-indigo text-white dark:bg-brand-indigo dark:text-white rounded-lg hover:bg-brand-hover transition-colors\"\n >\n <Plus className=\"w-4 h-4\" />\n <span>{t('添加AI配置', 'Add AI Config')}</span>\n </button>\n </div>\n\n {showForm && (\n <div className=\"p-4 bg-light-bg dark:bg-white/[0.04] rounded-lg border border-black/[0.06] dark:border-white/[0.04]\">\n <h4 className=\"font-medium text-gray-900 dark:text-text-primary mb-4\">\n {editingId ? t('编辑AI配置', 'Edit AI Configuration') : t('添加AI配置', 'Add AI Configuration')}\n </h4>\n \n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4 mb-4\">\n <div>\n <label className=\"block text-sm font-medium text-gray-900 dark:text-text-secondary mb-1\">\n {t('配置名称', 'Configuration Name')} *\n </label>\n <input\n type=\"text\"\n value={form.name}\n onChange={(e) => setForm(prev => ({ ...prev, name: e.target.value }))}\n className=\"w-full px-3 py-2 border border-black/[0.06] dark:border-white/[0.04] rounded-lg bg-white dark:bg-panel-dark text-gray-900 dark:text-text-primary focus:ring-2 focus:ring-brand-violet focus:border-transparent focus:outline-none\"\n placeholder={t('例如: OpenAI GPT-4', 'e.g., OpenAI GPT-4')}\n />\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-gray-900 dark:text-text-secondary mb-1\">\n {t('接口格式', 'API Format')} *\n </label>\n <select\n value={form.apiType}\n onChange={(e) => setForm(prev => ({ ...prev, apiType: e.target.value as AIApiType }))}\n className=\"w-full px-3 py-2 border border-black/[0.06] dark:border-white/[0.04] rounded-lg bg-white dark:bg-panel-dark text-gray-900 dark:text-text-primary focus:ring-2 focus:ring-brand-violet focus:border-transparent focus:outline-none\"\n >\n <option value=\"openai\">OpenAI (Chat Completions)</option>\n <option value=\"openai-responses\">OpenAI (Responses)</option>\n <option value=\"claude\">Claude</option>\n <option value=\"gemini\">Gemini</option>\n <option value=\"deepseek\">DeepSeek</option>\n <option value=\"mimo\">Xiaomi MiMo</option>\n <option value=\"openai-compatible\">OpenAI Compatible (Custom Endpoint)</option>\n </select>\n </div>\n\n {form.apiType === 'mimo' && (\n <div>\n <label className=\"block text-sm font-medium text-gray-900 dark:text-text-secondary mb-1\">\n {t('MiMo 渠道', 'MiMo Channel')} *\n </label>\n <select\n value={form.mimoPlan}\n onChange={(e) => setForm(prev => ({ ...prev, mimoPlan: e.target.value as MiMoPlan }))}\n className=\"w-full px-3 py-2 border border-black/[0.06] dark:border-white/[0.04] rounded-lg bg-white dark:bg-panel-dark text-gray-900 dark:text-text-primary focus:ring-2 focus:ring-brand-violet focus:border-transparent focus:outline-none\"\n >\n <option value=\"api\">{t('API(按量付费)', 'API (Pay-as-you-go)')}</option>\n <option value=\"token-plan\">{t('Token Plan(订阅制)', 'Token Plan (Subscription)')}</option>\n </select>\n <p className=\"text-xs text-gray-500 dark:text-text-tertiary mt-1\">\n {form.mimoPlan === 'api'\n ? t('API Key 以 sk- 开头,端点 api.xiaomimimo.com', 'API Key starts with sk-, endpoint api.xiaomimimo.com')\n : t('API Key 以 tp- 开头,端点 token-plan-cn.xiaomimimo.com', 'API Key starts with tp-, endpoint token-plan-cn.xiaomimimo.com')}\n </p>\n </div>\n )}\n \n <div>\n <label className=\"block text-sm font-medium text-gray-900 dark:text-text-secondary mb-1\">\n {t('API端点', 'API Endpoint')} *\n </label>\n <input\n type=\"url\"\n value={form.baseUrl}\n onChange={(e) => setForm(prev => ({ ...prev, baseUrl: e.target.value }))}\n className=\"w-full px-3 py-2 border border-black/[0.06] dark:border-white/[0.04] rounded-lg bg-white dark:bg-panel-dark text-gray-900 dark:text-text-primary focus:ring-2 focus:ring-brand-violet focus:border-transparent focus:outline-none\"\n placeholder={getEndpointPlaceholder(form.apiType, form.mimoPlan)}\n />\n <p className=\"text-xs text-gray-500 dark:text-text-tertiary mt-1\">\n {getEndpointHelpText(form.apiType, t)}\n </p>\n {form.baseUrl && (\n <p className=\"text-xs text-gray-500 dark:text-text-tertiary mt-1\">\n {t('最终请求地址: ', 'Final request URL: ')}\n <span className=\"font-mono break-all\">\n {buildFinalApiUrl(form.baseUrl, form.apiType)}\n </span>\n </p>\n )}\n </div>\n \n <div>\n <label className=\"block text-sm font-medium text-gray-900 dark:text-text-secondary mb-1\">\n {t('API密钥', 'API Key')} *\n </label>\n <input\n type=\"password\"\n value={form.apiKey}\n onChange={(e) => setForm(prev => ({ ...prev, apiKey: e.target.value }))}\n className=\"w-full px-3 py-2 border border-black/[0.06] dark:border-white/[0.04] rounded-lg bg-white dark:bg-panel-dark text-gray-900 dark:text-text-primary focus:ring-2 focus:ring-brand-violet focus:border-transparent focus:outline-none\"\n placeholder={t('输入API密钥', 'Enter API key')}\n />\n </div>\n \n <div>\n <label className=\"block text-sm font-medium text-gray-900 dark:text-text-secondary mb-1\">\n {t('模型名称', 'Model Name')} *\n </label>\n <input\n type=\"text\"\n value={form.model}\n onChange={(e) => setForm(prev => ({ ...prev, model: e.target.value }))}\n className=\"w-full px-3 py-2 border border-black/[0.06] dark:border-white/[0.04] rounded-lg bg-white dark:bg-panel-dark text-gray-900 dark:text-text-primary focus:ring-2 focus:ring-brand-violet focus:border-transparent focus:outline-none\"\n placeholder=\"gpt-4\"\n />\n </div>\n \n <div>\n <label className=\"block text-sm font-medium text-gray-900 dark:text-text-secondary mb-1\">\n {t('并发数', 'Concurrency')}\n </label>\n <SliderInput\n value={form.concurrency}\n onChange={(v) => setForm(prev => ({ ...prev, concurrency: v }))}\n min={1}\n max={10}\n showMarks={false}\n />\n <p className=\"text-xs text-gray-500 dark:text-text-tertiary mt-1\">\n {t('同时进行AI分析的仓库数量 (1-10)', 'Number of repositories to analyze simultaneously (1-10)')}\n </p>\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-gray-900 dark:text-text-secondary mb-1\">\n {t('推理强度', 'Reasoning Effort')}\n </label>\n <select\n value={form.reasoningEffort}\n onChange={(e) => setForm(prev => ({ ...prev, reasoningEffort: e.target.value as '' | AIReasoningEffort }))}\n className=\"w-full px-3 py-2 border border-black/[0.06] dark:border-white/[0.04] rounded-lg bg-white dark:bg-panel-dark text-gray-900 dark:text-text-primary focus:ring-2 focus:ring-brand-violet focus:border-transparent focus:outline-none\"\n >\n <option value=\"\">{t('默认 / 不传', 'Default / Do not send')}</option>\n <option value=\"none\">{t('none — 不推理', 'none — No reasoning')}</option>\n <option value=\"low\">{t('low — 快速响应', 'low — Quick response')}</option>\n <option value=\"medium\">{t('medium — 均衡模式', 'medium — Balanced')}</option>\n <option value=\"high\">{t('high — 深度推理', 'high — Deep reasoning')}</option>\n <option value=\"xhigh\">{t('xhigh — 最深推理', 'xhigh — Deepest reasoning')}</option>\n </select>\n <p className=\"text-xs text-gray-500 dark:text-text-tertiary mt-1\">\n {t(\n '仅对 OpenAI 兼容接口生效。留空时保持旧模式兼容,不额外传 reasoning。',\n 'Only applies to OpenAI-compatible APIs. Leave empty to preserve legacy behavior and omit reasoning.'\n )}\n </p>\n </div>\n </div>\n\n <div className=\"mb-4\">\n {notification && (\n <div \n className={`mb-3 p-3 rounded-lg flex items-center space-x-2 ${\n notification.type === 'success' \n ? 'bg-gray-100 text-gray-700 dark:bg-white/[0.04] dark:text-text-secondary' \n : notification.type === 'error'\n ? 'bg-gray-100 text-gray-700 dark:bg-white/[0.04] dark:text-text-secondary'\n : 'bg-gray-100 text-gray-700 dark:bg-white/[0.04] dark:text-text-secondary'\n }`}\n >\n {notification.type === 'error' && <AlertCircle className=\"w-4 h-4\" />}\n <span className=\"text-sm\">{notification.message}</span>\n </div>\n )}\n \n <div className=\"flex items-center justify-between mb-2\">\n <div className=\"flex items-center space-x-4\">\n <label className=\"flex items-center space-x-2 cursor-pointer\">\n <input\n type=\"checkbox\"\n checked={form.useCustomPrompt}\n onChange={(e) => handleUseCustomPromptChange(e.target.checked)}\n className=\"w-4 h-4 text-brand-violet bg-light-surface border-black/[0.06] rounded focus:ring-brand-violet dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-white/[0.04] dark:border-white/[0.04]\"\n />\n <span className=\"text-sm font-medium text-gray-900 dark:text-text-secondary\">\n {t('使用自定义提示词', 'Use Custom Prompt')}\n </span>\n </label>\n <button\n type=\"button\"\n onClick={handleToggleDefaultPrompt}\n disabled={showCustomPrompt}\n className={`flex items-center space-x-1 text-sm ${\n showCustomPrompt \n ? 'text-gray-400 dark:text-text-tertiarycursor-not-allowed' \n : 'text-gray-700hover:text-gray-900 dark:text-text-tertiary dark:hover:text-gray-300'\n }`}\n >\n {showDefaultPrompt ? <EyeOff className=\"w-4 h-4\" /> : <Eye className=\"w-4 h-4\" />}\n <span>{showDefaultPrompt ? t('隐藏默认提示词', 'Hide Default Prompt') : t('查看默认提示词', 'View Default Prompt')}</span>\n </button>\n </div>\n {form.useCustomPrompt && (\n <button\n type=\"button\"\n onClick={handleRestoreDefaultPrompt}\n className=\"text-sm text-gray-500 hover:text-gray-700 dark:text-text-tertiary dark:hover:text-text-secondary\"\n >\n {t('恢复默认提示词', 'Restore Default Prompt')}\n </button>\n )}\n </div>\n \n {showDefaultPrompt && !showCustomPrompt && (\n <div className=\"mb-3\">\n <label className=\"block text-xs font-medium text-gray-500 dark:text-text-tertiary mb-1\">\n {t('默认提示词(只读)', 'Default Prompt (Read-only)')}\n </label>\n <pre className=\"w-full px-3 py-2 border border-black/[0.06] dark:border-white/[0.04] rounded-lg bg-light-bg dark:bg-panel-dark text-gray-900 dark:text-text-secondary font-mono text-xs whitespace-pre-wrap overflow-auto max-h-64\">\n {defaultPrompt}\n </pre>\n </div>\n )}\n \n {showCustomPrompt && (\n <div className=\"space-y-2\">\n <div className=\"flex items-center justify-between\">\n <label className=\"block text-xs font-medium text-gray-500 dark:text-text-tertiary\">\n {t('自定义提示词', 'Custom Prompt')}\n {isCustomPromptModified && (\n <span className=\"ml-2 text-gray-700 dark:text-text-secondary \">\n ({t('已修改', 'Modified')})\n </span>\n )}\n {isCustomPromptSameAsDefault && (\n <span className=\"ml-2 text-gray-500 dark:text-text-tertiary\">\n ({t('默认值', 'Default')})\n </span>\n )}\n </label>\n <span className=\"text-xs text-gray-400 dark:text-text-quaternary\">\n {form.customPrompt.length} {t('字符', 'characters')}\n </span>\n </div>\n <textarea\n value={form.customPrompt}\n onChange={(e) => setForm(prev => ({ ...prev, customPrompt: e.target.value }))}\n rows={10}\n className=\"w-full px-3 py-2 border border-black/[0.06] dark:border-white/[0.04] rounded-lg bg-white dark:bg-panel-dark text-gray-900 dark:text-text-primary font-mono text-sm focus:ring-2 focus:ring-brand-violet focus:border-transparent\"\n placeholder={t('在此输入自定义提示词...', 'Enter custom prompt here...')}\n />\n </div>\n )}\n </div>\n\n <div className=\"flex space-x-3\">\n <button\n onClick={handleSave}\n className=\"flex items-center space-x-2 px-4 py-2 bg-brand-indigo text-white dark:bg-brand-indigo dark:text-white rounded-lg hover:bg-brand-hover transition-colors\"\n >\n <Save className=\"w-4 h-4\" />\n <span>{t('保存', 'Save')}</span>\n </button>\n <button\n onClick={handleTestForm}\n disabled={testingForm}\n className=\"flex items-center space-x-2 px-4 py-2 bg-brand-indigo text-white dark:bg-brand-indigo dark:text-white rounded-lg hover:bg-brand-hover transition-colors disabled:opacity-50\"\n >\n {testingForm ? (\n <RefreshCw className=\"w-4 h-4 animate-spin\" />\n ) : (\n <TestTube className=\"w-4 h-4\" />\n )}\n <span>{t('测试连接', 'Test Connection')}</span>\n </button>\n <button\n onClick={resetForm}\n className=\"flex items-center space-x-2 px-4 py-2 bg-light-surface hover:bg-gray-200 dark:bg-white/[0.04] dark:hover:bg-white/[0.08] text-gray-900 dark:text-text-primary rounded-lg border border-black/[0.06] dark:border-white/[0.04] transition-colors\"\n >\n <X className=\"w-4 h-4\" />\n <span>{t('取消', 'Cancel')}</span>\n </button>\n </div>\n </div>\n )}\n\n <div className=\"space-y-3\">\n {aiConfigs.map(config => (\n <div\n key={config.id}\n className={`p-4 rounded-lg border transition-colors ${\n config.id === activeAIConfig\n ? 'border-gray-300 bg-gray-50 dark:border-white/[0.12] dark:bg-white/[0.06]'\n : 'border-black/[0.06] dark:border-white/[0.04] hover:border-black/[0.06] dark:hover:border-white/[0.08]'\n }`}\n >\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center space-x-3\">\n <input\n type=\"radio\"\n name=\"activeAI\"\n checked={config.id === activeAIConfig}\n onChange={() => setActiveAIConfig(config.id)}\n className=\"w-4 h-4 text-gray-700 dark:text-text-secondary bg-light-surface border-black/[0.06] focus:ring-purple-500 dark:focus:ring-purple-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-white/[0.04] dark:border-white/[0.04]\"\n />\n <div>\n <h4 className=\"font-medium text-gray-900 dark:text-text-primary flex items-center\">\n {config.name}\n {config.useCustomPrompt && (\n <span className=\"ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-700 dark:bg-white/[0.04] dark:text-text-secondary\">\n <MessageSquare className=\"w-3 h-3 mr-1\" />\n {t('自定义提示词', 'Custom Prompt')}\n </span>\n )}\n </h4>\n <p className=\"text-sm text-gray-500 dark:text-text-tertiary\">\n {(config.apiType || 'openai').toUpperCase()} • {config.baseUrl} • {config.model} • {t('并发数', 'Concurrency')}: {config.concurrency || 1}\n {config.reasoningEffort ? ` • reasoning: ${config.reasoningEffort}` : ''}\n </p>\n {(config.apiKeyStatus === 'decrypt_failed' || config.apiKeyStatus === 'empty') && (\n <p className=\"mt-1 text-sm text-gray-700 dark:text-text-secondary \">\n {t(\n '存储的 API Key 无法解密或为空,请重新输入并保存该配置。',\n 'The stored API key could not be decrypted or is empty. Please re-enter and save this configuration.'\n )}\n </p>\n )}\n </div>\n </div>\n \n <div className=\"flex items-center space-x-2\">\n <button\n onClick={() => handleTest(config)}\n disabled={testingId === config.id}\n className=\"p-2 rounded-lg bg-gray-100 text-gray-900 dark:bg-white/[0.08] dark:text-text-primary hover:bg-gray-200 dark:hover:bg-white/[0.12] border border-transparent dark:border-white/[0.04] transition-colors disabled:opacity-50\"\n title={t('测试连接', 'Test Connection')}\n >\n {testingId === config.id ? (\n <RefreshCw className=\"w-4 h-4 animate-spin\" />\n ) : (\n <TestTube className=\"w-4 h-4\" />\n )}\n </button>\n <button\n onClick={() => handleEdit(config)}\n className=\"p-2 rounded-lg bg-gray-100 text-gray-900 dark:bg-white/[0.08] dark:text-text-primary hover:bg-gray-200 dark:hover:bg-white/[0.12] border border-transparent dark:border-white/[0.04] transition-colors\"\n title={t('编辑', 'Edit')}\n >\n <Edit3 className=\"w-4 h-4\" />\n </button>\n <button\n onClick={async () => {\n const confirmed = await confirm(\n t('确定要删除这个AI配置吗?', 'Delete AI Configuration?'),\n t('此操作无法撤销。', 'This action cannot be undone.'),\n { type: 'danger', confirmText: t('删除', 'Delete') }\n );\n if (confirmed) {\n if (config.id) {\n deleteAIConfig(config.id);\n } else {\n toast(t('删除失败:配置ID无效', 'Delete failed: Invalid config ID'), 'error');\n }\n }\n }}\n className=\"p-2 rounded-lg bg-gray-100 text-gray-900 dark:bg-white/[0.08] dark:text-text-primary hover:bg-gray-200 dark:hover:bg-white/[0.12] border border-transparent dark:border-white/[0.04] transition-colors\"\n title={t('删除', 'Delete')}\n >\n <Trash2 className=\"w-4 h-4\" />\n </button>\n </div>\n </div>\n </div>\n ))}\n \n {aiConfigs.length === 0 && (\n <div className=\"text-center py-8 text-gray-500 dark:text-text-tertiary\">\n <Bot className=\"w-12 h-12 mx-auto mb-3 opacity-50\" />\n <p>{t('还没有配置AI服务', 'No AI services configured yet')}</p>\n <p className=\"text-sm\">{t('点击上方按钮添加AI配置', 'Click the button above to add AI configuration')}</p>\n </div>\n )}\n </div>\n </div>\n );\n};\n"}}}}