@geminilight/mindos 0.6.63 → 0.6.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/_standalone/.mindos-build-version +1 -1
  2. package/_standalone/.next/BUILD_ID +1 -1
  3. package/_standalone/.next/app-path-routes-manifest.json +21 -21
  4. package/_standalone/.next/build-manifest.json +2 -2
  5. package/_standalone/.next/cache/.previewinfo +1 -1
  6. package/_standalone/.next/cache/.rscinfo +1 -1
  7. package/_standalone/.next/cache/config.json +3 -3
  8. package/_standalone/.next/prerender-manifest.json +3 -3
  9. package/_standalone/.next/server/app/.well-known/agent-card.json/route_client-reference-manifest.js +1 -1
  10. package/_standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/_standalone/.next/server/app/_global-error.html +2 -2
  12. package/_standalone/.next/server/app/_global-error.rsc +1 -1
  13. package/_standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  14. package/_standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  15. package/_standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  16. package/_standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  17. package/_standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  18. package/_standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  19. package/_standalone/.next/server/app/_not-found/page.js +1 -1
  20. package/_standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  21. package/_standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  22. package/_standalone/.next/server/app/agents/[agentKey]/page.js +1 -1
  23. package/_standalone/.next/server/app/agents/[agentKey]/page.js.nft.json +1 -1
  24. package/_standalone/.next/server/app/agents/[agentKey]/page_client-reference-manifest.js +1 -1
  25. package/_standalone/.next/server/app/agents/page.js +1 -1
  26. package/_standalone/.next/server/app/agents/page.js.nft.json +1 -1
  27. package/_standalone/.next/server/app/agents/page_client-reference-manifest.js +1 -1
  28. package/_standalone/.next/server/app/api/a2a/agents/route_client-reference-manifest.js +1 -1
  29. package/_standalone/.next/server/app/api/a2a/delegations/route_client-reference-manifest.js +1 -1
  30. package/_standalone/.next/server/app/api/a2a/discover/route_client-reference-manifest.js +1 -1
  31. package/_standalone/.next/server/app/api/a2a/route_client-reference-manifest.js +1 -1
  32. package/_standalone/.next/server/app/api/acp/config/route_client-reference-manifest.js +1 -1
  33. package/_standalone/.next/server/app/api/acp/detect/route_client-reference-manifest.js +1 -1
  34. package/_standalone/.next/server/app/api/acp/install/route_client-reference-manifest.js +1 -1
  35. package/_standalone/.next/server/app/api/acp/registry/route_client-reference-manifest.js +1 -1
  36. package/_standalone/.next/server/app/api/acp/session/route_client-reference-manifest.js +1 -1
  37. package/_standalone/.next/server/app/api/agent-activity/route_client-reference-manifest.js +1 -1
  38. package/_standalone/.next/server/app/api/agents/copy-skill/route.js.nft.json +1 -1
  39. package/_standalone/.next/server/app/api/agents/copy-skill/route_client-reference-manifest.js +1 -1
  40. package/_standalone/.next/server/app/api/agents/custom/detect/route_client-reference-manifest.js +1 -1
  41. package/_standalone/.next/server/app/api/agents/custom/route_client-reference-manifest.js +1 -1
  42. package/_standalone/.next/server/app/api/ask/route.js +48 -42
  43. package/_standalone/.next/server/app/api/ask/route.js.nft.json +1 -1
  44. package/_standalone/.next/server/app/api/ask/route_client-reference-manifest.js +1 -1
  45. package/_standalone/.next/server/app/api/ask-sessions/route_client-reference-manifest.js +1 -1
  46. package/_standalone/.next/server/app/api/auth/route_client-reference-manifest.js +1 -1
  47. package/_standalone/.next/server/app/api/backlinks/route.js.nft.json +1 -1
  48. package/_standalone/.next/server/app/api/backlinks/route_client-reference-manifest.js +1 -1
  49. package/_standalone/.next/server/app/api/bootstrap/route.js.nft.json +1 -1
  50. package/_standalone/.next/server/app/api/bootstrap/route_client-reference-manifest.js +1 -1
  51. package/_standalone/.next/server/app/api/changes/route.js.nft.json +1 -1
  52. package/_standalone/.next/server/app/api/changes/route_client-reference-manifest.js +1 -1
  53. package/_standalone/.next/server/app/api/export/route.js.nft.json +1 -1
  54. package/_standalone/.next/server/app/api/export/route_client-reference-manifest.js +1 -1
  55. package/_standalone/.next/server/app/api/extract-pdf/route_client-reference-manifest.js +1 -1
  56. package/_standalone/.next/server/app/api/file/import/route.js +1 -1
  57. package/_standalone/.next/server/app/api/file/import/route.js.nft.json +1 -1
  58. package/_standalone/.next/server/app/api/file/import/route_client-reference-manifest.js +1 -1
  59. package/_standalone/.next/server/app/api/file/raw/route.js.nft.json +1 -1
  60. package/_standalone/.next/server/app/api/file/raw/route_client-reference-manifest.js +1 -1
  61. package/_standalone/.next/server/app/api/file/route.js.nft.json +1 -1
  62. package/_standalone/.next/server/app/api/file/route_client-reference-manifest.js +1 -1
  63. package/_standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  64. package/_standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  65. package/_standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  66. package/_standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  67. package/_standalone/.next/server/app/api/graph/route.js.nft.json +1 -1
  68. package/_standalone/.next/server/app/api/graph/route_client-reference-manifest.js +1 -1
  69. package/_standalone/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  70. package/_standalone/.next/server/app/api/inbox/route.js.nft.json +1 -1
  71. package/_standalone/.next/server/app/api/inbox/route_client-reference-manifest.js +1 -1
  72. package/_standalone/.next/server/app/api/init/route.js.nft.json +1 -1
  73. package/_standalone/.next/server/app/api/init/route_client-reference-manifest.js +1 -1
  74. package/_standalone/.next/server/app/api/mcp/agents/route.js +1 -1
  75. package/_standalone/.next/server/app/api/mcp/agents/route.js.nft.json +1 -1
  76. package/_standalone/.next/server/app/api/mcp/agents/route_client-reference-manifest.js +1 -1
  77. package/_standalone/.next/server/app/api/mcp/install/route_client-reference-manifest.js +1 -1
  78. package/_standalone/.next/server/app/api/mcp/install-skill/route_client-reference-manifest.js +1 -1
  79. package/_standalone/.next/server/app/api/mcp/restart/route_client-reference-manifest.js +1 -1
  80. package/_standalone/.next/server/app/api/mcp/status/route_client-reference-manifest.js +1 -1
  81. package/_standalone/.next/server/app/api/mcp/uninstall/route_client-reference-manifest.js +1 -1
  82. package/_standalone/.next/server/app/api/monitoring/route.js.nft.json +1 -1
  83. package/_standalone/.next/server/app/api/monitoring/route_client-reference-manifest.js +1 -1
  84. package/_standalone/.next/server/app/api/recent-files/route.js.nft.json +1 -1
  85. package/_standalone/.next/server/app/api/recent-files/route_client-reference-manifest.js +1 -1
  86. package/_standalone/.next/server/app/api/restart/route_client-reference-manifest.js +1 -1
  87. package/_standalone/.next/server/app/api/search/route.js.nft.json +1 -1
  88. package/_standalone/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
  89. package/_standalone/.next/server/app/api/settings/list-models/route.js +1 -1
  90. package/_standalone/.next/server/app/api/settings/list-models/route_client-reference-manifest.js +1 -1
  91. package/_standalone/.next/server/app/api/settings/reset-token/route_client-reference-manifest.js +1 -1
  92. package/_standalone/.next/server/app/api/settings/route.js +1 -1
  93. package/_standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  94. package/_standalone/.next/server/app/api/settings/route_client-reference-manifest.js +1 -1
  95. package/_standalone/.next/server/app/api/settings/test-key/route.js +1 -1
  96. package/_standalone/.next/server/app/api/settings/test-key/route_client-reference-manifest.js +1 -1
  97. package/_standalone/.next/server/app/api/setup/check-path/route_client-reference-manifest.js +1 -1
  98. package/_standalone/.next/server/app/api/setup/check-port/route_client-reference-manifest.js +1 -1
  99. package/_standalone/.next/server/app/api/setup/generate-token/route_client-reference-manifest.js +1 -1
  100. package/_standalone/.next/server/app/api/setup/ls/route_client-reference-manifest.js +1 -1
  101. package/_standalone/.next/server/app/api/setup/route.js +1 -1
  102. package/_standalone/.next/server/app/api/setup/route_client-reference-manifest.js +1 -1
  103. package/_standalone/.next/server/app/api/skills/route.js +1 -1
  104. package/_standalone/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
  105. package/_standalone/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
  106. package/_standalone/.next/server/app/api/tree-version/route.js.nft.json +1 -1
  107. package/_standalone/.next/server/app/api/tree-version/route_client-reference-manifest.js +1 -1
  108. package/_standalone/.next/server/app/api/uninstall/route_client-reference-manifest.js +1 -1
  109. package/_standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  110. package/_standalone/.next/server/app/api/update-check/route_client-reference-manifest.js +1 -1
  111. package/_standalone/.next/server/app/api/update-status/route_client-reference-manifest.js +1 -1
  112. package/_standalone/.next/server/app/api/workflows/route.js.nft.json +1 -1
  113. package/_standalone/.next/server/app/api/workflows/route_client-reference-manifest.js +1 -1
  114. package/_standalone/.next/server/app/changes/page.js +1 -1
  115. package/_standalone/.next/server/app/changes/page.js.nft.json +1 -1
  116. package/_standalone/.next/server/app/changes/page_client-reference-manifest.js +1 -1
  117. package/_standalone/.next/server/app/echo/[segment]/page.js +2 -2
  118. package/_standalone/.next/server/app/echo/[segment]/page.js.nft.json +1 -1
  119. package/_standalone/.next/server/app/echo/[segment]/page_client-reference-manifest.js +1 -1
  120. package/_standalone/.next/server/app/echo/page.js +1 -1
  121. package/_standalone/.next/server/app/echo/page.js.nft.json +1 -1
  122. package/_standalone/.next/server/app/echo/page_client-reference-manifest.js +1 -1
  123. package/_standalone/.next/server/app/explore/page.js +1 -1
  124. package/_standalone/.next/server/app/explore/page.js.nft.json +1 -1
  125. package/_standalone/.next/server/app/explore/page_client-reference-manifest.js +1 -1
  126. package/_standalone/.next/server/app/help/page.js +1 -1
  127. package/_standalone/.next/server/app/help/page.js.nft.json +1 -1
  128. package/_standalone/.next/server/app/help/page_client-reference-manifest.js +1 -1
  129. package/_standalone/.next/server/app/inbox/history/page.js +1 -1
  130. package/_standalone/.next/server/app/inbox/history/page.js.nft.json +1 -1
  131. package/_standalone/.next/server/app/inbox/history/page_client-reference-manifest.js +1 -1
  132. package/_standalone/.next/server/app/login/page.js +1 -1
  133. package/_standalone/.next/server/app/login/page.js.nft.json +1 -1
  134. package/_standalone/.next/server/app/login/page_client-reference-manifest.js +1 -1
  135. package/_standalone/.next/server/app/page.js +1 -1
  136. package/_standalone/.next/server/app/page.js.nft.json +1 -1
  137. package/_standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  138. package/_standalone/.next/server/app/setup/page.js +2 -2
  139. package/_standalone/.next/server/app/setup/page.js.nft.json +1 -1
  140. package/_standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  141. package/_standalone/.next/server/app/trash/page.js +3 -3
  142. package/_standalone/.next/server/app/trash/page.js.nft.json +1 -1
  143. package/_standalone/.next/server/app/trash/page_client-reference-manifest.js +1 -1
  144. package/_standalone/.next/server/app/view/[...path]/page.js +2 -2
  145. package/_standalone/.next/server/app/view/[...path]/page.js.nft.json +1 -1
  146. package/_standalone/.next/server/app/view/[...path]/page_client-reference-manifest.js +1 -1
  147. package/_standalone/.next/server/app/wiki/page.js +1 -1
  148. package/_standalone/.next/server/app/wiki/page.js.nft.json +1 -1
  149. package/_standalone/.next/server/app/wiki/page_client-reference-manifest.js +1 -1
  150. package/_standalone/.next/server/app-paths-manifest.json +21 -21
  151. package/_standalone/.next/server/chunks/122.js +222 -0
  152. package/_standalone/.next/server/chunks/3113.js +52 -0
  153. package/_standalone/.next/server/chunks/6539.js +1 -1
  154. package/_standalone/.next/server/chunks/8388.js +2 -2
  155. package/_standalone/.next/server/chunks/953.js +3 -3
  156. package/_standalone/.next/server/chunks/9787.js +2 -0
  157. package/_standalone/.next/server/pages/500.html +2 -2
  158. package/_standalone/.next/server/server-reference-manifest.js +1 -1
  159. package/_standalone/.next/server/server-reference-manifest.json +1 -1
  160. package/_standalone/.next/static/chunks/1001-99da82ec8d8c136f.js +1 -0
  161. package/_standalone/.next/static/chunks/5149-4d828886dda479fa.js +1 -0
  162. package/_standalone/.next/static/chunks/{5581-82e5db227f8e9393.js → 5581-c671163a2fe1b312.js} +2 -2
  163. package/_standalone/.next/static/chunks/6636-53238eff89503f03.js +6 -0
  164. package/_standalone/.next/static/chunks/6757-1c1a89720fdda8f0.js +1 -0
  165. package/_standalone/.next/static/chunks/7129-20e9d2463a9da646.js +1 -0
  166. package/_standalone/.next/static/chunks/{3674-be69a8b858ceacdd.js → 7294-cac25d97869afadc.js} +1 -1
  167. package/_standalone/.next/static/chunks/8225-21e5cebc3731ddf0.js +1 -0
  168. package/_standalone/.next/static/chunks/8520-b51810e66293ceb8.js +22 -0
  169. package/_standalone/.next/static/chunks/9207-dc9c31b351a2ed78.js +1 -0
  170. package/_standalone/.next/static/chunks/app/agents/[agentKey]/{page-b0dabe793500383d.js → page-2f5cf97e03dc1cc9.js} +1 -1
  171. package/_standalone/.next/static/chunks/app/agents/{page-1f1ac330c8177cf6.js → page-50eac58d511dcc6e.js} +1 -1
  172. package/_standalone/.next/static/chunks/app/echo/[segment]/page-2a00f4686adf3885.js +11 -0
  173. package/_standalone/.next/static/chunks/app/{layout-50a6b1164ee98ab9.js → layout-2cb7a6602d2e5d5f.js} +62 -58
  174. package/_standalone/.next/static/chunks/app/{page-73802bd31d7f6c9f.js → page-5ab911b2226f6ff7.js} +1 -1
  175. package/_standalone/.next/static/chunks/app/setup/page-907b7c57fad2292b.js +1 -0
  176. package/_standalone/.next/static/chunks/app/trash/page-11a511b065ea84c2.js +1 -0
  177. package/_standalone/.next/static/chunks/app/view/[...path]/{page-808f39963bf04715.js → page-26e47dd4c533a58c.js} +2 -2
  178. package/_standalone/.next/static/css/67e7918f5ed7d147.css +1 -0
  179. package/_standalone/.next/trace +65 -65
  180. package/_standalone/__tests__/api/ask-attachments.test.ts +194 -0
  181. package/_standalone/__tests__/api/settings.test.ts +16 -12
  182. package/_standalone/__tests__/api/setup.test.ts +11 -9
  183. package/_standalone/__tests__/api/test-key.test.ts +0 -10
  184. package/_standalone/__tests__/components/UpdateToast.test.ts +344 -0
  185. package/_standalone/__tests__/core/context.test.ts +48 -426
  186. package/_standalone/__tests__/lib/pi-skills.test.ts +4 -4
  187. package/_standalone/__tests__/lib/settings-ai-client.test.ts +32 -12
  188. package/_standalone/__tests__/setup.ts +5 -5
  189. package/_standalone/components/ask/AskContent.tsx +70 -40
  190. package/_standalone/components/ask/AskHeader.tsx +8 -1
  191. package/_standalone/components/ask/MessageList.tsx +37 -3
  192. package/_standalone/components/ask/ProviderModelCapsule.tsx +51 -129
  193. package/_standalone/components/settings/AiTab.tsx +270 -347
  194. package/_standalone/components/settings/CustomProviderFields.tsx +121 -0
  195. package/_standalone/components/settings/CustomProvidersCard.tsx +2 -2
  196. package/_standalone/components/settings/KnowledgeTab.tsx +6 -20
  197. package/_standalone/components/settings/McpAgentInstall.tsx +7 -2
  198. package/_standalone/components/settings/Primitives.tsx +48 -104
  199. package/_standalone/components/settings/ProviderModal.tsx +38 -221
  200. package/_standalone/components/settings/SettingsContent.tsx +5 -12
  201. package/_standalone/components/settings/TestButton.tsx +64 -0
  202. package/_standalone/components/settings/types.ts +3 -12
  203. package/_standalone/components/settings/useCustomProviderForm.ts +132 -0
  204. package/_standalone/components/setup/StepAI.tsx +3 -3
  205. package/_standalone/components/shared/ModelInput.tsx +18 -4
  206. package/_standalone/components/shared/ProviderSelect.tsx +126 -134
  207. package/_standalone/hooks/useAskChat.ts +97 -13
  208. package/_standalone/hooks/useAskPanel.ts +17 -1
  209. package/_standalone/lib/settings-ai-client.ts +17 -8
  210. package/_standalone/tsconfig.tsbuildinfo +1 -1
  211. package/app/app/api/ask/route.ts +124 -44
  212. package/app/app/api/mcp/agents/route.ts +3 -3
  213. package/app/app/api/settings/list-models/route.ts +15 -26
  214. package/app/app/api/settings/route.ts +14 -59
  215. package/app/app/api/settings/test-key/route.ts +47 -12
  216. package/app/app/api/setup/route.ts +36 -18
  217. package/app/app/api/skills/route.ts +1 -1
  218. package/app/app/layout.tsx +5 -3
  219. package/app/components/HomeContent.tsx +11 -0
  220. package/app/components/UpdateToast.tsx +255 -0
  221. package/app/components/ask/AskContent.tsx +70 -40
  222. package/app/components/ask/AskHeader.tsx +8 -1
  223. package/app/components/ask/MessageList.tsx +37 -3
  224. package/app/components/ask/ProviderModelCapsule.tsx +51 -129
  225. package/app/components/settings/AiTab.tsx +270 -347
  226. package/app/components/settings/CustomProviderFields.tsx +121 -0
  227. package/app/components/settings/CustomProvidersCard.tsx +2 -2
  228. package/app/components/settings/KnowledgeTab.tsx +6 -20
  229. package/app/components/settings/McpAgentInstall.tsx +7 -2
  230. package/app/components/settings/Primitives.tsx +48 -104
  231. package/app/components/settings/ProviderModal.tsx +38 -221
  232. package/app/components/settings/SettingsContent.tsx +5 -12
  233. package/app/components/settings/TestButton.tsx +64 -0
  234. package/app/components/settings/types.ts +3 -12
  235. package/app/components/settings/useCustomProviderForm.ts +132 -0
  236. package/app/components/setup/StepAI.tsx +3 -3
  237. package/app/components/shared/ModelInput.tsx +18 -4
  238. package/app/components/shared/ProviderSelect.tsx +126 -134
  239. package/app/hooks/useAskChat.ts +97 -13
  240. package/app/hooks/useAskPanel.ts +17 -1
  241. package/app/lib/agent/context.ts +65 -0
  242. package/app/lib/agent/providers.ts +25 -0
  243. package/app/lib/agent/tools.ts +1 -1
  244. package/app/lib/custom-endpoints.ts +129 -29
  245. package/app/lib/i18n/modules/settings.ts +20 -0
  246. package/app/lib/pi-integration/skills.ts +16 -4
  247. package/app/lib/settings-ai-client.ts +17 -8
  248. package/app/lib/settings.ts +64 -90
  249. package/app/lib/types.ts +4 -0
  250. package/package.json +1 -1
  251. package/_standalone/.next/server/chunks/530.js +0 -218
  252. package/_standalone/.next/server/chunks/9007.js +0 -2
  253. package/_standalone/.next/server/chunks/9137.js +0 -52
  254. package/_standalone/.next/static/chunks/1309-373ade1b40aea186.js +0 -1
  255. package/_standalone/.next/static/chunks/3165-9189a38fd9ebf6f2.js +0 -1
  256. package/_standalone/.next/static/chunks/4587-5d06728133fff222.js +0 -1
  257. package/_standalone/.next/static/chunks/6261-5ce86db54b19ae46.js +0 -1
  258. package/_standalone/.next/static/chunks/6636-9bbc90fb3b8731fe.js +0 -6
  259. package/_standalone/.next/static/chunks/7637-904b0a381dc3ec02.js +0 -1
  260. package/_standalone/.next/static/chunks/8520-84e607f33c409f91.js +0 -22
  261. package/_standalone/.next/static/chunks/9207-9a4a1a1ede4f8e6e.js +0 -1
  262. package/_standalone/.next/static/chunks/app/echo/[segment]/page-bc5e104eb7ae6327.js +0 -11
  263. package/_standalone/.next/static/chunks/app/setup/page-79acb0baf38184c6.js +0 -1
  264. package/_standalone/.next/static/chunks/app/trash/page-d040db56863da504.js +0 -1
  265. package/_standalone/.next/static/css/1287672978833d07.css +0 -1
  266. package/_standalone/lib/agent/context.ts +0 -403
  267. /package/_standalone/.next/static/{X86rF8dKEO0InosOw4a2_ → eIlwbGas1iRGonlPyEwj7}/_buildManifest.js +0 -0
  268. /package/_standalone/.next/static/{X86rF8dKEO0InosOw4a2_ → eIlwbGas1iRGonlPyEwj7}/_ssgManifest.js +0 -0
@@ -1,56 +1,38 @@
1
1
  'use client';
2
2
 
3
- import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
4
- import { AlertCircle, Loader2, Sparkles, Bot, Monitor, ExternalLink, RotateCcw, Check, Zap, X } from 'lucide-react';
5
- import type { AiSettings, AgentSettings, ProviderConfig, SettingsData, AiTabProps } from './types';
6
- import { Field, Select, Input, EnvBadge, ApiKeyInput, Toggle, SettingCard, SettingRow } from './Primitives';
3
+ import { useState, useRef, useCallback, useEffect } from 'react';
4
+ import { AlertCircle, Sparkles, Bot, Monitor, ExternalLink, RotateCcw, Trash2, X } from 'lucide-react';
5
+ import type { AiTabProps } from './types';
6
+ import { Field, Select, Input, PasswordInput, EnvBadge, Toggle, SettingCard, SettingRow } from './Primitives';
7
7
  import { useLocale } from '@/lib/stores/locale-store';
8
- import { type ProviderId, PROVIDER_PRESETS, isProviderId, getApiKeyEnvVar, getDefaultBaseUrl } from '@/lib/agent/providers';
8
+ import { type ProviderId, PROVIDER_PRESETS, ALL_PROVIDER_IDS, getApiKeyEnvVar, getDefaultBaseUrl } from '@/lib/agent/providers';
9
9
  import ProviderSelect from '@/components/shared/ProviderSelect';
10
10
  import ModelInput from '@/components/shared/ModelInput';
11
- import { type CustomProvider, generateCustomProviderId } from '@/lib/custom-endpoints';
12
- import { ALL_PROVIDER_IDS } from '@/lib/agent/providers';
11
+ import { type Provider, generateProviderId } from '@/lib/custom-endpoints';
12
+ import { useCustomProviderForm, type TestResult } from './useCustomProviderForm';
13
+ import CustomProviderFields from './CustomProviderFields';
14
+ import { TestButton } from './TestButton';
13
15
 
14
- type TestState = 'idle' | 'testing' | 'ok' | 'error';
15
- type ErrorCode = 'auth_error' | 'model_not_found' | 'rate_limited' | 'network_error' | 'unknown';
16
-
17
- interface TestResult {
18
- state: TestState;
19
- latency?: number;
20
- error?: string;
21
- code?: ErrorCode;
22
- }
23
-
24
- function errorMessage(t: AiTabProps['t'], code?: ErrorCode): string {
25
- switch (code) {
26
- case 'auth_error': return t.settings.ai.testKeyAuthError;
27
- case 'model_not_found': return t.settings.ai.testKeyModelNotFound;
28
- case 'rate_limited': return t.settings.ai.testKeyRateLimited;
29
- case 'network_error': return t.settings.ai.testKeyNetworkError;
30
- default: return t.settings.ai.testKeyUnknown;
31
- }
32
- }
33
-
34
- export function AiTab({ data, updateAi, updateAgent, updateCustomProviders, t }: AiTabProps) {
16
+ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
35
17
  const { locale } = useLocale();
36
18
  const env = data.envOverrides ?? {};
37
- const envVal = data.envValues ?? {};
38
- const provider = data.ai.provider;
39
- const preset = isProviderId(provider) ? PROVIDER_PRESETS[provider] : null;
19
+
20
+ // ── Current provider from the unified array ──
21
+ const current = data.ai.providers.find(p => p.id === data.ai.activeProvider);
22
+ const preset = current ? PROVIDER_PRESETS[current.protocol] : null;
40
23
 
41
24
  const [testResult, setTestResult] = useState<Record<string, TestResult>>({});
42
25
  const [customFormOpen, setCustomFormOpen] = useState(false);
43
- const [customEditingId, setCustomEditingId] = useState<string | null>(null);
44
26
  const okTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
45
- const prevProviderRef = useRef(provider);
27
+ const prevProviderRef = useRef(data.ai.activeProvider);
46
28
 
47
29
  useEffect(() => {
48
- if (prevProviderRef.current !== provider) {
49
- prevProviderRef.current = provider;
30
+ if (prevProviderRef.current !== data.ai.activeProvider) {
31
+ prevProviderRef.current = data.ai.activeProvider;
50
32
  setTestResult({});
51
33
  if (okTimerRef.current) { clearTimeout(okTimerRef.current); okTimerRef.current = undefined; }
52
34
  }
53
- }, [provider]);
35
+ }, [data.ai.activeProvider]);
54
36
 
55
37
  useEffect(() => () => { if (okTimerRef.current) clearTimeout(okTimerRef.current); }, []);
56
38
 
@@ -59,15 +41,17 @@ export function AiTab({ data, updateAi, updateAgent, updateCustomProviders, t }:
59
41
  try { localStorage.setItem('mindos-reconnect-retries', String(v)); } catch (err) { console.warn("[AiTab] localStorage setItem reconnectRetries failed:", err); }
60
42
  }, [data.agent?.reconnectRetries]);
61
43
 
62
- const handleTestKey = useCallback(async (providerName: ProviderId) => {
63
- const prov = data.ai.providers?.[providerName] ?? {} as ProviderConfig;
64
- setTestResult(prev => ({ ...prev, [providerName]: { state: 'testing' } }));
44
+ // ── Test key for the current provider ──
45
+ const handleTestKey = useCallback(async () => {
46
+ if (!current) return;
47
+ const pid = current.id;
48
+ setTestResult(prev => ({ ...prev, [pid]: { state: 'testing' } }));
65
49
 
66
50
  try {
67
- const body: Record<string, string> = { provider: providerName };
68
- if (prov.apiKey) body.apiKey = prov.apiKey;
69
- if (prov.model) body.model = prov.model;
70
- if (prov.baseUrl) body.baseUrl = prov.baseUrl;
51
+ const body: Record<string, string> = { provider: current.protocol };
52
+ if (current.apiKey) body.apiKey = current.apiKey;
53
+ if (current.model) body.model = current.model;
54
+ if (current.baseUrl) body.baseUrl = current.baseUrl;
71
55
 
72
56
  const res = await fetch('/api/settings/test-key', {
73
57
  method: 'POST',
@@ -77,79 +61,87 @@ export function AiTab({ data, updateAi, updateAgent, updateCustomProviders, t }:
77
61
  const json = await res.json();
78
62
 
79
63
  if (json.ok) {
80
- setTestResult(prev => ({ ...prev, [providerName]: { state: 'ok', latency: json.latency } }));
64
+ setTestResult(prev => ({ ...prev, [pid]: { state: 'ok', latency: json.latency } }));
81
65
  if (okTimerRef.current) clearTimeout(okTimerRef.current);
82
66
  okTimerRef.current = setTimeout(() => {
83
- setTestResult(prev => ({ ...prev, [providerName]: { state: 'idle' } }));
67
+ setTestResult(prev => ({ ...prev, [pid]: { state: 'idle' } }));
84
68
  }, 8000);
85
69
  } else {
86
70
  setTestResult(prev => ({
87
71
  ...prev,
88
- [providerName]: { state: 'error', error: json.error, code: json.code },
72
+ [pid]: { state: 'error', error: json.error, code: json.code },
89
73
  }));
90
74
  }
91
75
  } catch {
92
76
  setTestResult(prev => ({
93
77
  ...prev,
94
- [providerName]: { state: 'error', code: 'network_error', error: 'Network error' },
78
+ [pid]: { state: 'error', code: 'network_error', error: 'Network error' },
95
79
  }));
96
80
  }
97
- }, [data.ai.providers]);
81
+ }, [current]);
98
82
 
99
- const patchProvider = useCallback((name: ProviderId, patch: Partial<ProviderConfig>) => {
83
+ // ── Patch any field on the current provider (auto-save) ──
84
+ const patchProvider = useCallback((patch: Partial<Provider>) => {
85
+ if (!current) return;
100
86
  if ('apiKey' in patch) {
101
- setTestResult(prev => ({ ...prev, [name]: { state: 'idle' } }));
87
+ setTestResult(prev => ({ ...prev, [current.id]: { state: 'idle' } }));
102
88
  }
103
89
  updateAi({
104
- providers: {
105
- ...data.ai.providers,
106
- [name]: { ...data.ai.providers?.[name], ...patch },
107
- },
90
+ providers: data.ai.providers.map(p => p.id === current.id ? { ...p, ...patch } : p),
108
91
  });
109
- }, [data.ai.providers, updateAi]);
92
+ }, [current, data.ai.providers, updateAi]);
110
93
 
111
- const currentConfig = data.ai.providers?.[provider] ?? { apiKey: '', model: '', baseUrl: '' };
112
- const envKeyName = preset ? getApiKeyEnvVar(provider) : undefined;
113
- const activeApiKey = currentConfig.apiKey;
94
+ // ── Env key detection ──
95
+ const envKeyName = current ? getApiKeyEnvVar(current.protocol) : undefined;
114
96
  const activeEnvKey = envKeyName ? env[envKeyName] : false;
115
97
  const hasFallbackKey = !!preset?.apiKeyFallback;
116
- const missingApiKey = !activeApiKey && !activeEnvKey && !hasFallbackKey;
117
98
 
118
- const configuredProviders = new Set(
119
- Object.entries(data.ai.providers ?? {})
120
- .filter(([id, cfg]) => (cfg && cfg.apiKey) || PROVIDER_PRESETS[id as ProviderId]?.apiKeyFallback)
121
- .map(([id]) => id as ProviderId),
122
- );
123
-
124
- const resetProvider = useCallback((name: ProviderId) => {
125
- setTestResult(prev => ({ ...prev, [name]: { state: 'idle' } }));
99
+ // ── Reset provider (clear fields to defaults) ──
100
+ const resetProvider = useCallback(() => {
101
+ if (!current) return;
102
+ const defaults = PROVIDER_PRESETS[current.protocol];
103
+ setTestResult(prev => ({ ...prev, [current.id]: { state: 'idle' } }));
126
104
  updateAi({
127
- providers: {
128
- ...data.ai.providers,
129
- [name]: { apiKey: '', model: '', baseUrl: '' },
130
- },
105
+ providers: data.ai.providers.map(p => p.id === current.id ? {
106
+ ...p,
107
+ apiKey: '',
108
+ model: '',
109
+ baseUrl: defaults?.fixedBaseUrl ?? '',
110
+ } : p),
131
111
  });
132
- }, [data.ai.providers, updateAi]);
112
+ }, [current, data.ai.providers, updateAi]);
133
113
 
134
- const customProviders = data.customProviders ?? [];
135
- const editingCustomProvider = useMemo(
136
- () => customEditingId ? customProviders.find(p => p.id === customEditingId) : null,
137
- [customEditingId, customProviders],
138
- );
139
- const handleSaveCustom = useCallback((cp: CustomProvider) => {
140
- const updated = customEditingId
141
- ? customProviders.map(p => p.id === customEditingId ? cp : p)
142
- : [...customProviders, cp];
143
- updateCustomProviders(updated);
114
+ // ── Delete provider ──
115
+ const deleteProvider = useCallback(() => {
116
+ if (!current) return;
117
+ const remaining = data.ai.providers.filter(p => p.id !== current.id);
118
+ const fallbackId = remaining.length > 0 ? remaining[0].id : '';
119
+ updateAi({
120
+ activeProvider: fallbackId,
121
+ providers: remaining,
122
+ });
123
+ setTestResult(prev => { const n = { ...prev }; delete n[current.id]; return n; });
124
+ }, [current, data.ai.providers, updateAi]);
125
+
126
+ // ── Save handler for the "Add Provider" form ──
127
+ const handleSaveNew = useCallback((formProvider: Provider) => {
128
+ // The form uses `protocol` directly now (no mapping needed)
129
+ const newProvider: Provider = {
130
+ id: formProvider.id || generateProviderId(),
131
+ name: formProvider.name,
132
+ protocol: formProvider.protocol,
133
+ apiKey: formProvider.apiKey,
134
+ model: formProvider.model,
135
+ baseUrl: formProvider.baseUrl,
136
+ };
137
+ updateAi({
138
+ activeProvider: newProvider.id,
139
+ providers: [...data.ai.providers, newProvider],
140
+ });
144
141
  setCustomFormOpen(false);
145
- setCustomEditingId(null);
146
- }, [customEditingId, customProviders, updateCustomProviders]);
147
- const handleDeleteCustom = useCallback((id: string) => {
148
- if (customEditingId === id) { setCustomFormOpen(false); setCustomEditingId(null); }
149
- updateCustomProviders(customProviders.filter(p => p.id !== id));
150
- }, [customProviders, updateCustomProviders, customEditingId]);
142
+ }, [data.ai.providers, updateAi]);
151
143
 
152
- const displayName = preset ? (locale === 'zh' ? preset.nameZh : preset.name) : provider;
144
+ const displayName = current?.name ?? (locale === 'zh' ? '未选择' : 'No provider');
153
145
 
154
146
  return (
155
147
  <div className="space-y-4">
@@ -160,45 +152,77 @@ export function AiTab({ data, updateAi, updateAgent, updateCustomProviders, t }:
160
152
  description={displayName}
161
153
  >
162
154
  <ProviderSelect
163
- value={provider}
155
+ value={data.ai.activeProvider as ProviderId}
164
156
  onChange={id => {
165
- if (id !== 'skip') updateAi({ provider: id });
157
+ if (id !== 'skip') updateAi({ activeProvider: id });
166
158
  setCustomFormOpen(false);
167
- setCustomEditingId(null);
168
159
  }}
169
160
  compact
170
- configuredProviders={configuredProviders}
171
- customProviders={customProviders}
172
- onAddCustom={() => { setCustomEditingId(null); setCustomFormOpen(true); }}
173
- onEditCustom={id => { setCustomEditingId(id); setCustomFormOpen(true); }}
174
- onDeleteCustom={handleDeleteCustom}
161
+ customProviders={data.ai.providers}
162
+ onAdd={() => {
163
+ // Open form pre-filled with OpenAI defaults
164
+ const defaultId: ProviderId = 'openai';
165
+ const p = PROVIDER_PRESETS[defaultId];
166
+ const baseName = locale === 'zh' ? p.nameZh : p.name;
167
+ const names = new Set(data.ai.providers.map(cp => cp.name.toLowerCase()));
168
+ let finalName = baseName;
169
+ if (names.has(finalName.toLowerCase())) {
170
+ let n = 2;
171
+ while (names.has(`${baseName} (${n})`.toLowerCase())) n++;
172
+ finalName = `${baseName} (${n})`;
173
+ }
174
+ setCustomFormOpen(true);
175
+ }}
175
176
  />
176
177
 
177
- {/* Inline custom provider form */}
178
+ {/* Add new provider form */}
178
179
  {customFormOpen && (
179
180
  <CustomProviderForm
180
- key={customEditingId ?? 'new'}
181
- initial={editingCustomProvider ?? undefined}
182
- onSave={handleSaveCustom}
183
- onCancel={() => { setCustomFormOpen(false); setCustomEditingId(null); }}
181
+ key="new"
182
+ onSave={handleSaveNew}
183
+ onCancel={() => setCustomFormOpen(false)}
184
184
  t={t}
185
+ existingNames={data.ai.providers.map(p => p.name)}
185
186
  />
186
187
  )}
187
188
 
188
- {/* Provider configuration fields */}
189
- {preset && !customFormOpen && (
189
+ {/* ── Inline config fields for the selected provider ── */}
190
+ {!customFormOpen && current && (
190
191
  <div className="space-y-3 pt-3 border-t border-border">
191
- {/* 1. API Key — most essential, enter first */}
192
+ {/* Name + Protocol (inline, auto-save) */}
193
+ <div className="grid grid-cols-2 gap-3">
194
+ <Field label={locale === 'zh' ? '名称' : 'Name'}>
195
+ <Input
196
+ value={current.name}
197
+ onChange={e => patchProvider({ name: e.target.value })}
198
+ placeholder={locale === 'zh' ? '输入名称' : 'Enter name'}
199
+ />
200
+ </Field>
201
+ <Field label={locale === 'zh' ? '协议' : 'Protocol'}>
202
+ <Select
203
+ value={current.protocol}
204
+ onChange={e => patchProvider({ protocol: e.target.value as ProviderId })}
205
+ >
206
+ {ALL_PROVIDER_IDS.map(id => (
207
+ <option key={id} value={id}>
208
+ {locale === 'zh' ? PROVIDER_PRESETS[id].nameZh : PROVIDER_PRESETS[id].name}
209
+ </option>
210
+ ))}
211
+ </Select>
212
+ </Field>
213
+ </div>
214
+
215
+ {/* API Key */}
192
216
  <Field
193
217
  label={<>{t.settings.ai.apiKey} {envKeyName && <EnvBadge overridden={env[envKeyName]} />}</>}
194
- hint={activeEnvKey ? t.settings.ai.envFieldNote(envKeyName!) : hasFallbackKey ? t.settings.ai.keyOptionalHint : t.settings.ai.keyHint}
218
+ hint={preset && activeEnvKey ? t.settings.ai.envFieldNote(envKeyName!) : preset && hasFallbackKey ? t.settings.ai.keyOptionalHint : undefined}
195
219
  >
196
- <ApiKeyInput
197
- value={currentConfig.apiKey}
198
- onChange={v => patchProvider(provider, { apiKey: v })}
199
- labels={{ change: t.settings.ai.keyChange, cancel: t.settings.ai.keyCancel }}
220
+ <PasswordInput
221
+ value={current.apiKey}
222
+ onChange={v => patchProvider({ apiKey: v })}
223
+ placeholder="sk-..."
200
224
  />
201
- {preset.signupUrl && !currentConfig.apiKey && !activeEnvKey && (
225
+ {preset?.signupUrl && !current.apiKey && !activeEnvKey && (
202
226
  <a
203
227
  href={preset.signupUrl}
204
228
  target="_blank"
@@ -214,58 +238,51 @@ export function AiTab({ data, updateAi, updateAgent, updateCustomProviders, t }:
214
238
  )}
215
239
  </Field>
216
240
 
217
- {/* 2. Base URL — before Model so "List Models" uses the correct endpoint */}
218
- {preset.supportsBaseUrl && (
219
- <Field
220
- label={t.settings.ai.baseUrl}
221
- hint={t.settings.ai.baseUrlHint}
222
- >
241
+ {/* Base URL */}
242
+ {(preset?.supportsBaseUrl || current.baseUrl) && (
243
+ <Field label="Base URL">
223
244
  <Input
224
- value={currentConfig.baseUrl ?? ''}
225
- onChange={e => patchProvider(provider, { baseUrl: e.target.value })}
226
- placeholder={preset.fixedBaseUrl || getDefaultBaseUrl(provider) || 'https://api.openai.com/v1'}
245
+ value={current.baseUrl}
246
+ onChange={e => patchProvider({ baseUrl: e.target.value })}
247
+ placeholder={preset?.fixedBaseUrl || getDefaultBaseUrl(current.protocol) || 'https://api.openai.com/v1'}
227
248
  />
228
249
  </Field>
229
250
  )}
230
251
 
231
- {/* 3. Model — after Base URL so "List Models" queries the right endpoint */}
232
- <Field label={t.settings.ai.model}>
252
+ {/* Model */}
253
+ <Field label={locale === 'zh' ? '模型' : 'Model'}>
233
254
  <ModelInput
234
- value={currentConfig.model}
235
- onChange={v => patchProvider(provider, { model: v })}
236
- placeholder={preset.defaultModel}
237
- provider={provider}
238
- apiKey={currentConfig.apiKey}
255
+ value={current.model}
256
+ onChange={v => patchProvider({ model: v })}
257
+ placeholder={preset?.defaultModel ?? ''}
258
+ provider={current.protocol}
259
+ apiKey={current.apiKey}
239
260
  envKey={!!activeEnvKey}
240
- baseUrl={currentConfig.baseUrl}
241
- supportsListModels={preset.supportsListModels}
261
+ baseUrl={current.baseUrl}
262
+ supportsListModels={!!current.baseUrl?.trim() || !!preset?.supportsListModels}
263
+ allowNoKey={!!current.baseUrl?.trim()}
242
264
  browseLabel={t.settings.ai.listModels}
243
265
  noModelsLabel={t.settings.ai.noModelsFound}
244
266
  />
245
267
  </Field>
246
268
 
247
- {/* 4. Test & Reset after all fields */}
269
+ {/* Test & Reset & Delete */}
248
270
  <ProviderActions
249
- provider={provider}
250
- result={testResult[provider] ?? { state: 'idle' }}
251
- hasKey={!!currentConfig.apiKey}
271
+ provider={current.protocol}
272
+ result={testResult[current.id] ?? { state: 'idle' }}
273
+ hasKey={!!current.apiKey}
252
274
  hasEnv={!!activeEnvKey}
253
- hasConfig={!!(currentConfig.apiKey || currentConfig.model || currentConfig.baseUrl)}
254
- onTest={() => handleTestKey(provider)}
255
- onReset={() => resetProvider(provider)}
275
+ hasConfig={!!(current.apiKey || current.model || current.baseUrl)}
276
+ onTest={handleTestKey}
277
+ onReset={resetProvider}
278
+ onDelete={deleteProvider}
256
279
  t={t}
257
280
  />
258
281
  </div>
259
282
  )}
260
283
 
261
- {/* Inline warnings */}
262
- {missingApiKey && (
263
- <div className="flex items-start gap-2 text-xs text-destructive/80 bg-destructive/8 border border-destructive/20 rounded-lg px-3 py-2.5">
264
- <AlertCircle size={13} className="shrink-0 mt-0.5" />
265
- <span>{t.settings.ai.noApiKey}</span>
266
- </div>
267
- )}
268
- {Object.values(env).some(Boolean) && (
284
+ {/* Env override hint — only when env vars are active */}
285
+ {!customFormOpen && Object.values(env).some(Boolean) && (
269
286
  <div className="flex items-start gap-2 text-xs text-[var(--amber)] bg-[var(--amber-subtle)] border border-[var(--amber)]/20 rounded-lg px-3 py-2.5">
270
287
  <AlertCircle size={13} className="shrink-0 mt-0.5" />
271
288
  <span>{t.settings.ai.envHint}</span>
@@ -356,10 +373,10 @@ export function AiTab({ data, updateAi, updateAgent, updateCustomProviders, t }:
356
373
  );
357
374
  }
358
375
 
359
- /* ── Provider Actions: Test + Reset ── */
376
+ /* ── Provider Actions: Test + Reset + Delete ── */
360
377
 
361
378
  function ProviderActions({
362
- provider, result, hasKey, hasEnv, hasConfig, onTest, onReset, t,
379
+ provider, result, hasKey, hasEnv, hasConfig, onTest, onReset, onDelete, t,
363
380
  }: {
364
381
  provider: ProviderId;
365
382
  result: TestResult;
@@ -367,146 +384,106 @@ function ProviderActions({
367
384
  hasEnv: boolean;
368
385
  hasConfig: boolean;
369
386
  onTest: () => void;
370
- onReset: () => void;
387
+ onReset?: () => void;
388
+ onDelete?: () => void;
371
389
  t: AiTabProps['t'];
372
390
  }) {
373
- const [confirmReset, setConfirmReset] = useState(false);
391
+ const [confirmAction, setConfirmAction] = useState<'reset' | 'delete' | null>(null);
374
392
  const confirmTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
375
393
  const hasFallback = !!PROVIDER_PRESETS[provider]?.apiKeyFallback;
376
394
  const canTest = hasKey || hasEnv || hasFallback;
377
- const isTesting = result.state === 'testing';
378
- const isOk = result.state === 'ok';
379
- const isError = result.state === 'error';
380
395
 
381
396
  useEffect(() => () => { if (confirmTimerRef.current) clearTimeout(confirmTimerRef.current); }, []);
382
397
 
383
- const handleResetClick = () => {
384
- if (confirmReset) {
385
- onReset();
386
- setConfirmReset(false);
398
+ const startConfirm = (action: 'reset' | 'delete') => {
399
+ if (confirmAction === action) {
400
+ if (action === 'reset') onReset?.(); else onDelete?.();
401
+ setConfirmAction(null);
387
402
  if (confirmTimerRef.current) clearTimeout(confirmTimerRef.current);
388
403
  } else {
389
- setConfirmReset(true);
390
- confirmTimerRef.current = setTimeout(() => setConfirmReset(false), 3000);
404
+ setConfirmAction(action);
405
+ if (confirmTimerRef.current) clearTimeout(confirmTimerRef.current);
406
+ confirmTimerRef.current = setTimeout(() => setConfirmAction(null), 3000);
391
407
  }
392
408
  };
393
409
 
410
+ const { locale } = useLocale();
411
+
394
412
  return (
395
413
  <div className="space-y-2 pt-2">
396
414
  <div className="flex items-center justify-between">
397
- {/* Left: Test button */}
398
- <button
399
- type="button"
400
- disabled={!canTest || isTesting}
401
- onClick={onTest}
402
- className={`inline-flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium rounded-lg transition-all duration-200 disabled:cursor-not-allowed ${
403
- isOk
404
- ? 'bg-success/10 text-success border border-success/20'
405
- : isError
406
- ? 'bg-destructive/8 text-destructive border border-destructive/20 hover:bg-destructive/12'
407
- : 'border border-border text-muted-foreground hover:text-foreground hover:border-foreground/20 disabled:opacity-40'
408
- }`}
409
- >
410
- {isTesting ? (
411
- <Loader2 size={13} className="animate-spin" />
412
- ) : isOk ? (
413
- <Check size={13} />
414
- ) : isError ? (
415
- <AlertCircle size={13} />
416
- ) : (
417
- <Zap size={13} />
415
+ <TestButton result={result} disabled={!canTest} onTest={onTest} t={t} />
416
+
417
+ <div className="flex items-center gap-1">
418
+ {/* Reset */}
419
+ {onReset && hasConfig && (
420
+ <button
421
+ type="button"
422
+ onClick={() => startConfirm('reset')}
423
+ onBlur={() => { if (confirmAction === 'reset') { setConfirmAction(null); if (confirmTimerRef.current) clearTimeout(confirmTimerRef.current); } }}
424
+ className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-lg transition-all duration-200 ${
425
+ confirmAction === 'reset'
426
+ ? 'bg-destructive/10 text-destructive border border-destructive/25 font-medium'
427
+ : 'text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted/50'
428
+ }`}
429
+ >
430
+ <RotateCcw size={12} />
431
+ {confirmAction === 'reset'
432
+ ? (locale === 'zh' ? '确认重置?' : 'Confirm?')
433
+ : (locale === 'zh' ? '重置' : 'Reset')}
434
+ </button>
418
435
  )}
419
- {isTesting
420
- ? t.settings.ai.testKeyTesting
421
- : isOk && result.latency != null
422
- ? t.settings.ai.testKeyOk(result.latency)
423
- : isError
424
- ? errorMessage(t, result.code)
425
- : t.settings.ai.testKey}
426
- </button>
427
-
428
- {/* Right: Reset subtle, icon-first, inline confirm */}
429
- {hasConfig && (
430
- <button
431
- type="button"
432
- onClick={handleResetClick}
433
- onBlur={() => { setConfirmReset(false); if (confirmTimerRef.current) clearTimeout(confirmTimerRef.current); }}
434
- className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-lg transition-all duration-200 ${
435
- confirmReset
436
- ? 'bg-destructive/10 text-destructive border border-destructive/25 font-medium'
437
- : 'text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted/50'
438
- }`}
439
- >
440
- <RotateCcw size={12} />
441
- {confirmReset ? t.settings.ai.resetProviderConfirm : t.settings.ai.resetProvider}
442
- </button>
443
- )}
436
+ {/* Delete */}
437
+ {onDelete && (
438
+ <button
439
+ type="button"
440
+ onClick={() => startConfirm('delete')}
441
+ onBlur={() => { if (confirmAction === 'delete') { setConfirmAction(null); if (confirmTimerRef.current) clearTimeout(confirmTimerRef.current); } }}
442
+ className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-lg transition-all duration-200 ${
443
+ confirmAction === 'delete'
444
+ ? 'bg-destructive/10 text-destructive border border-destructive/25 font-medium'
445
+ : 'text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted/50'
446
+ }`}
447
+ >
448
+ <Trash2 size={12} />
449
+ {confirmAction === 'delete'
450
+ ? (locale === 'zh' ? '确认删除?' : 'Confirm?')
451
+ : (locale === 'zh' ? '删除' : 'Delete')}
452
+ </button>
453
+ )}
454
+ </div>
444
455
  </div>
445
456
  </div>
446
457
  );
447
458
  }
448
459
 
449
- /* ── Inline Custom Provider Form (replaces modal) ── */
460
+ /* ── Inline Custom Provider Form (uses shared hook + fields) ── */
450
461
 
451
462
  function CustomProviderForm({
452
- initial, onSave, onCancel, t,
463
+ initial, onSave, onCancel, onDelete, t, existingNames,
453
464
  }: {
454
- initial?: CustomProvider;
455
- onSave: (provider: CustomProvider) => void;
465
+ initial?: Provider;
466
+ onSave: (provider: Provider) => void;
456
467
  onCancel: () => void;
468
+ onDelete?: () => void;
457
469
  t: AiTabProps['t'];
470
+ existingNames: string[];
458
471
  }) {
459
472
  const { locale } = useLocale();
460
- const [name, setName] = useState(initial?.name ?? '');
461
- const [baseProviderId, setBaseProviderId] = useState<ProviderId>(initial?.baseProviderId ?? 'openai');
462
- const [apiKey, setApiKey] = useState(initial?.apiKey === '***set***' ? '' : initial?.apiKey ?? '');
463
- const [model, setModel] = useState(initial?.model ?? '');
464
- const [baseUrl, setBaseUrl] = useState(initial?.baseUrl ?? '');
465
- const [testState, setTestState] = useState<'idle' | 'testing' | 'ok' | 'error'>('idle');
466
- const [testError, setTestError] = useState('');
467
-
468
- const basePreset = PROVIDER_PRESETS[baseProviderId];
469
- const canSave = name.trim() && baseUrl.trim() && model.trim();
470
-
471
- const handleTest = useCallback(async () => {
472
- if (!canSave) { setTestError(locale === 'zh' ? '名称、接口地址和模型为必填' : 'Name, base URL, and model are required'); return; }
473
- setTestState('testing');
474
- setTestError('');
475
- try {
476
- const res = await fetch('/api/settings/test-key', {
477
- method: 'POST',
478
- headers: { 'Content-Type': 'application/json' },
479
- body: JSON.stringify({ apiKey, model, baseUrl, baseProviderId }),
480
- });
481
- const json = await res.json();
482
- if (json.ok) setTestState('ok');
483
- else { setTestState('error'); setTestError(json.error || 'Test failed'); }
484
- } catch {
485
- setTestState('error');
486
- setTestError('Network error');
487
- }
488
- }, [canSave, apiKey, model, baseUrl, baseProviderId, locale]);
489
-
490
- const handleSave = () => {
491
- if (!canSave) { setTestError(locale === 'zh' ? '名称、接口地址和模型为必填' : 'Name, base URL, and model are required'); return; }
492
- onSave({
493
- id: initial?.id || generateCustomProviderId(),
494
- name: name.trim(),
495
- baseProviderId,
496
- apiKey,
497
- model: model.trim(),
498
- baseUrl: baseUrl.trim(),
499
- });
500
- };
473
+ const form = useCustomProviderForm({ initial, onSave, locale, existingNames });
474
+ const [confirmDelete, setConfirmDelete] = useState(false);
475
+ const deleteTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
501
476
 
502
- const formTitle = initial
503
- ? (locale === 'zh' ? '编辑自定义 Provider' : 'Edit Custom Provider')
504
- : (locale === 'zh' ? '添加自定义 Provider' : 'Add Custom Provider');
477
+ useEffect(() => () => { if (deleteTimerRef.current) clearTimeout(deleteTimerRef.current); }, []);
478
+
479
+ const formTitle = initial?.id
480
+ ? (locale === 'zh' ? '编辑 Provider' : 'Edit Provider')
481
+ : (locale === 'zh' ? '添加 Provider' : 'Add Provider');
505
482
 
506
483
  const missingFields: string[] = [];
507
- if (!name.trim()) missingFields.push(locale === 'zh' ? '名称' : 'Name');
508
- if (!baseUrl.trim()) missingFields.push(locale === 'zh' ? '接口地址' : 'Base URL');
509
- if (!model.trim()) missingFields.push(locale === 'zh' ? '模型' : 'Model');
484
+ if (!form.name.trim()) missingFields.push(locale === 'zh' ? '名称' : 'Name');
485
+ if (!form.baseUrl.trim()) missingFields.push(locale === 'zh' ? '接口地址' : 'Base URL');
486
+ if (!form.model.trim()) missingFields.push(locale === 'zh' ? '模型' : 'Model');
510
487
 
511
488
  return (
512
489
  <div className="mt-3 rounded-lg border border-border overflow-hidden">
@@ -524,101 +501,47 @@ function CustomProviderForm({
524
501
  </div>
525
502
 
526
503
  {/* Form body */}
527
- <div className="space-y-3 p-4">
528
- {/* Row 1: Name + Protocol side by side */}
529
- <div className="grid grid-cols-2 gap-3">
530
- <Field label={t.settings?.customProviders?.modal?.fieldName ?? 'Name'}>
531
- <Input
532
- value={name}
533
- onChange={e => setName(e.target.value)}
534
- placeholder={locale === 'zh' ? '公司 GPT-4' : 'Company GPT-4'}
535
- autoFocus
536
- />
537
- </Field>
538
- <Field
539
- label={t.settings?.customProviders?.modal?.fieldProtocol ?? 'Protocol'}
540
- >
541
- <Select
542
- value={baseProviderId}
543
- onChange={e => setBaseProviderId(e.target.value as ProviderId)}
544
- >
545
- {ALL_PROVIDER_IDS.map(id => (
546
- <option key={id} value={id}>
547
- {locale === 'zh' ? PROVIDER_PRESETS[id].nameZh : PROVIDER_PRESETS[id].name}
548
- </option>
549
- ))}
550
- </Select>
551
- </Field>
552
- </div>
553
-
554
- {/* Base URL */}
555
- <Field
556
- label={t.settings?.customProviders?.modal?.fieldBaseUrl ?? 'Base URL'}
557
- hint={t.settings?.customProviders?.modal?.fieldBaseUrlHint}
558
- >
559
- <Input
560
- value={baseUrl}
561
- onChange={e => setBaseUrl(e.target.value)}
562
- placeholder={basePreset.fixedBaseUrl || 'https://api.example.com/v1'}
563
- />
564
- </Field>
565
-
566
- {/* API Key */}
567
- <Field
568
- label={<>{t.settings?.customProviders?.modal?.fieldApiKey ?? 'API Key'} <span className="text-muted-foreground/50 font-normal">{locale === 'zh' ? '(可选)' : '(optional)'}</span></>}
569
- >
570
- <Input
571
- type="password"
572
- value={apiKey}
573
- onChange={e => setApiKey(e.target.value)}
574
- placeholder="sk-..."
575
- />
576
- </Field>
577
-
578
- {/* Model */}
579
- <Field label={t.settings?.customProviders?.modal?.fieldModel ?? 'Model'}>
580
- <ModelInput
581
- value={model}
582
- onChange={setModel}
583
- placeholder={basePreset.defaultModel}
584
- provider={baseProviderId}
585
- apiKey={apiKey}
586
- baseUrl={baseUrl}
587
- supportsListModels={!!baseUrl.trim()}
588
- allowNoKey
589
- browseLabel={t.settings.ai.listModels}
590
- noModelsLabel={t.settings.ai.noModelsFound}
591
- />
592
- </Field>
593
-
594
- {/* Feedback */}
595
- {testError && testState !== 'ok' && (
596
- <div className="flex items-start gap-2 text-xs text-destructive/80 bg-destructive/8 border border-destructive/20 rounded-lg px-3 py-2">
597
- <AlertCircle size={13} className="shrink-0 mt-0.5" />
598
- <span>{testError}</span>
599
- </div>
600
- )}
601
- {testState === 'ok' && (
602
- <div className="flex items-center gap-2 text-xs text-success bg-success/10 border border-success/20 rounded-lg px-3 py-2">
603
- <Check size={13} />
604
- <span>{t.settings?.customProviders?.modal?.success ?? 'Connected'}</span>
605
- </div>
606
- )}
504
+ <div className="p-4">
505
+ <CustomProviderFields form={form} t={t} locale={locale} layout="compact" />
607
506
 
608
507
  {/* Actions */}
609
- <div className="flex items-center gap-2 pt-1">
610
- <button
611
- type="button"
612
- onClick={handleTest}
613
- disabled={!canSave || testState === 'testing'}
614
- className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg border border-border text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
615
- >
616
- {testState === 'testing' ? <Loader2 size={12} className="animate-spin" /> : <Zap size={13} />}
617
- {testState === 'testing' ? t.settings.ai.testKeyTesting : t.settings.ai.testKey}
618
- </button>
508
+ <div className="flex items-center gap-2 pt-4">
509
+ <TestButton result={form.testResult} disabled={!form.canSave} onTest={form.handleTest} t={t} />
510
+
511
+ {/* Delete — only when editing an existing provider */}
512
+ {onDelete && initial?.id && (
513
+ <button
514
+ type="button"
515
+ onClick={() => {
516
+ if (confirmDelete) {
517
+ onDelete();
518
+ if (deleteTimerRef.current) clearTimeout(deleteTimerRef.current);
519
+ } else {
520
+ setConfirmDelete(true);
521
+ deleteTimerRef.current = setTimeout(() => setConfirmDelete(false), 3000);
522
+ }
523
+ }}
524
+ onBlur={() => { setConfirmDelete(false); if (deleteTimerRef.current) clearTimeout(deleteTimerRef.current); }}
525
+ className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-lg transition-all duration-200 ${
526
+ confirmDelete
527
+ ? 'bg-destructive/10 text-destructive border border-destructive/25 font-medium'
528
+ : 'text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted/50'
529
+ }`}
530
+ >
531
+ <Trash2 size={12} />
532
+ {confirmDelete
533
+ ? (locale === 'zh' ? '确认删除?' : 'Confirm?')
534
+ : (locale === 'zh' ? '删除' : 'Delete')}
535
+ </button>
536
+ )}
619
537
 
620
538
  <div className="flex-1">
621
- {!canSave && missingFields.length > 0 && (
539
+ {form.isDuplicateName && (
540
+ <span className="text-2xs text-destructive pl-2">
541
+ {locale === 'zh' ? '名称已存在' : 'Name already exists'}
542
+ </span>
543
+ )}
544
+ {!form.isDuplicateName && !form.canSave && missingFields.length > 0 && (
622
545
  <span className="text-2xs text-muted-foreground/60 pl-2">
623
546
  {locale === 'zh' ? `需要: ${missingFields.join('、')}` : `Required: ${missingFields.join(', ')}`}
624
547
  </span>
@@ -634,8 +557,8 @@ function CustomProviderForm({
634
557
  </button>
635
558
  <button
636
559
  type="button"
637
- onClick={handleSave}
638
- disabled={!canSave}
560
+ onClick={form.handleSave}
561
+ disabled={!form.canSave}
639
562
  className="px-4 py-1.5 text-sm font-medium rounded-lg bg-[var(--amber)] text-[var(--amber-foreground)] hover:bg-[var(--amber)]/90 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
640
563
  >
641
564
  {locale === 'zh' ? '保存' : 'Save'}