@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.
- package/_standalone/.mindos-build-version +1 -1
- package/_standalone/.next/BUILD_ID +1 -1
- package/_standalone/.next/app-path-routes-manifest.json +21 -21
- package/_standalone/.next/build-manifest.json +2 -2
- package/_standalone/.next/cache/.previewinfo +1 -1
- package/_standalone/.next/cache/.rscinfo +1 -1
- package/_standalone/.next/cache/config.json +3 -3
- package/_standalone/.next/prerender-manifest.json +3 -3
- package/_standalone/.next/server/app/.well-known/agent-card.json/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/_global-error.html +2 -2
- package/_standalone/.next/server/app/_global-error.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/_standalone/.next/server/app/_not-found/page.js +1 -1
- package/_standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/agents/[agentKey]/page.js +1 -1
- package/_standalone/.next/server/app/agents/[agentKey]/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/agents/[agentKey]/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/agents/page.js +1 -1
- package/_standalone/.next/server/app/agents/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/agents/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/agents/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/delegations/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/discover/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/config/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/detect/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/install/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/registry/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/session/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agent-activity/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agents/copy-skill/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/agents/copy-skill/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agents/custom/detect/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agents/custom/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/ask/route.js +48 -42
- package/_standalone/.next/server/app/api/ask/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/ask/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/ask-sessions/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/auth/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/backlinks/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/backlinks/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/bootstrap/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/bootstrap/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/changes/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/changes/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/export/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/export/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/extract-pdf/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/file/import/route.js +1 -1
- package/_standalone/.next/server/app/api/file/import/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/file/import/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/file/raw/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/file/raw/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/file/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/file/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/files/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/git/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/graph/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/graph/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/inbox/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/inbox/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/init/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/init/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/agents/route.js +1 -1
- package/_standalone/.next/server/app/api/mcp/agents/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/mcp/agents/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/install/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/install-skill/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/restart/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/status/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/uninstall/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/monitoring/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/monitoring/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/recent-files/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/recent-files/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/restart/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/search/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/list-models/route.js +1 -1
- package/_standalone/.next/server/app/api/settings/list-models/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/reset-token/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/route.js +1 -1
- package/_standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/settings/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/test-key/route.js +1 -1
- package/_standalone/.next/server/app/api/settings/test-key/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/check-path/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/check-port/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/generate-token/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/ls/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/route.js +1 -1
- package/_standalone/.next/server/app/api/setup/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/skills/route.js +1 -1
- package/_standalone/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/tree-version/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/tree-version/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/uninstall/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/update-check/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/update-status/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/workflows/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/workflows/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/changes/page.js +1 -1
- package/_standalone/.next/server/app/changes/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/changes/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/echo/[segment]/page.js +2 -2
- package/_standalone/.next/server/app/echo/[segment]/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/echo/[segment]/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/echo/page.js +1 -1
- package/_standalone/.next/server/app/echo/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/echo/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/explore/page.js +1 -1
- package/_standalone/.next/server/app/explore/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/explore/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/help/page.js +1 -1
- package/_standalone/.next/server/app/help/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/help/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/inbox/history/page.js +1 -1
- package/_standalone/.next/server/app/inbox/history/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/inbox/history/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/login/page.js +1 -1
- package/_standalone/.next/server/app/login/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/page.js +1 -1
- package/_standalone/.next/server/app/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/setup/page.js +2 -2
- package/_standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/trash/page.js +3 -3
- package/_standalone/.next/server/app/trash/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/trash/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/view/[...path]/page.js +2 -2
- package/_standalone/.next/server/app/view/[...path]/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/view/[...path]/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/wiki/page.js +1 -1
- package/_standalone/.next/server/app/wiki/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/wiki/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app-paths-manifest.json +21 -21
- package/_standalone/.next/server/chunks/122.js +222 -0
- package/_standalone/.next/server/chunks/3113.js +52 -0
- package/_standalone/.next/server/chunks/6539.js +1 -1
- package/_standalone/.next/server/chunks/8388.js +2 -2
- package/_standalone/.next/server/chunks/953.js +3 -3
- package/_standalone/.next/server/chunks/9787.js +2 -0
- package/_standalone/.next/server/pages/500.html +2 -2
- package/_standalone/.next/server/server-reference-manifest.js +1 -1
- package/_standalone/.next/server/server-reference-manifest.json +1 -1
- package/_standalone/.next/static/chunks/1001-99da82ec8d8c136f.js +1 -0
- package/_standalone/.next/static/chunks/5149-4d828886dda479fa.js +1 -0
- package/_standalone/.next/static/chunks/{5581-82e5db227f8e9393.js → 5581-c671163a2fe1b312.js} +2 -2
- package/_standalone/.next/static/chunks/6636-53238eff89503f03.js +6 -0
- package/_standalone/.next/static/chunks/6757-1c1a89720fdda8f0.js +1 -0
- package/_standalone/.next/static/chunks/7129-20e9d2463a9da646.js +1 -0
- package/_standalone/.next/static/chunks/{3674-be69a8b858ceacdd.js → 7294-cac25d97869afadc.js} +1 -1
- package/_standalone/.next/static/chunks/8225-21e5cebc3731ddf0.js +1 -0
- package/_standalone/.next/static/chunks/8520-b51810e66293ceb8.js +22 -0
- package/_standalone/.next/static/chunks/9207-dc9c31b351a2ed78.js +1 -0
- package/_standalone/.next/static/chunks/app/agents/[agentKey]/{page-b0dabe793500383d.js → page-2f5cf97e03dc1cc9.js} +1 -1
- package/_standalone/.next/static/chunks/app/agents/{page-1f1ac330c8177cf6.js → page-50eac58d511dcc6e.js} +1 -1
- package/_standalone/.next/static/chunks/app/echo/[segment]/page-2a00f4686adf3885.js +11 -0
- package/_standalone/.next/static/chunks/app/{layout-50a6b1164ee98ab9.js → layout-2cb7a6602d2e5d5f.js} +62 -58
- package/_standalone/.next/static/chunks/app/{page-73802bd31d7f6c9f.js → page-5ab911b2226f6ff7.js} +1 -1
- package/_standalone/.next/static/chunks/app/setup/page-907b7c57fad2292b.js +1 -0
- package/_standalone/.next/static/chunks/app/trash/page-11a511b065ea84c2.js +1 -0
- package/_standalone/.next/static/chunks/app/view/[...path]/{page-808f39963bf04715.js → page-26e47dd4c533a58c.js} +2 -2
- package/_standalone/.next/static/css/67e7918f5ed7d147.css +1 -0
- package/_standalone/.next/trace +65 -65
- package/_standalone/__tests__/api/ask-attachments.test.ts +194 -0
- package/_standalone/__tests__/api/settings.test.ts +16 -12
- package/_standalone/__tests__/api/setup.test.ts +11 -9
- package/_standalone/__tests__/api/test-key.test.ts +0 -10
- package/_standalone/__tests__/components/UpdateToast.test.ts +344 -0
- package/_standalone/__tests__/core/context.test.ts +48 -426
- package/_standalone/__tests__/lib/pi-skills.test.ts +4 -4
- package/_standalone/__tests__/lib/settings-ai-client.test.ts +32 -12
- package/_standalone/__tests__/setup.ts +5 -5
- package/_standalone/components/ask/AskContent.tsx +70 -40
- package/_standalone/components/ask/AskHeader.tsx +8 -1
- package/_standalone/components/ask/MessageList.tsx +37 -3
- package/_standalone/components/ask/ProviderModelCapsule.tsx +51 -129
- package/_standalone/components/settings/AiTab.tsx +270 -347
- package/_standalone/components/settings/CustomProviderFields.tsx +121 -0
- package/_standalone/components/settings/CustomProvidersCard.tsx +2 -2
- package/_standalone/components/settings/KnowledgeTab.tsx +6 -20
- package/_standalone/components/settings/McpAgentInstall.tsx +7 -2
- package/_standalone/components/settings/Primitives.tsx +48 -104
- package/_standalone/components/settings/ProviderModal.tsx +38 -221
- package/_standalone/components/settings/SettingsContent.tsx +5 -12
- package/_standalone/components/settings/TestButton.tsx +64 -0
- package/_standalone/components/settings/types.ts +3 -12
- package/_standalone/components/settings/useCustomProviderForm.ts +132 -0
- package/_standalone/components/setup/StepAI.tsx +3 -3
- package/_standalone/components/shared/ModelInput.tsx +18 -4
- package/_standalone/components/shared/ProviderSelect.tsx +126 -134
- package/_standalone/hooks/useAskChat.ts +97 -13
- package/_standalone/hooks/useAskPanel.ts +17 -1
- package/_standalone/lib/settings-ai-client.ts +17 -8
- package/_standalone/tsconfig.tsbuildinfo +1 -1
- package/app/app/api/ask/route.ts +124 -44
- package/app/app/api/mcp/agents/route.ts +3 -3
- package/app/app/api/settings/list-models/route.ts +15 -26
- package/app/app/api/settings/route.ts +14 -59
- package/app/app/api/settings/test-key/route.ts +47 -12
- package/app/app/api/setup/route.ts +36 -18
- package/app/app/api/skills/route.ts +1 -1
- package/app/app/layout.tsx +5 -3
- package/app/components/HomeContent.tsx +11 -0
- package/app/components/UpdateToast.tsx +255 -0
- package/app/components/ask/AskContent.tsx +70 -40
- package/app/components/ask/AskHeader.tsx +8 -1
- package/app/components/ask/MessageList.tsx +37 -3
- package/app/components/ask/ProviderModelCapsule.tsx +51 -129
- package/app/components/settings/AiTab.tsx +270 -347
- package/app/components/settings/CustomProviderFields.tsx +121 -0
- package/app/components/settings/CustomProvidersCard.tsx +2 -2
- package/app/components/settings/KnowledgeTab.tsx +6 -20
- package/app/components/settings/McpAgentInstall.tsx +7 -2
- package/app/components/settings/Primitives.tsx +48 -104
- package/app/components/settings/ProviderModal.tsx +38 -221
- package/app/components/settings/SettingsContent.tsx +5 -12
- package/app/components/settings/TestButton.tsx +64 -0
- package/app/components/settings/types.ts +3 -12
- package/app/components/settings/useCustomProviderForm.ts +132 -0
- package/app/components/setup/StepAI.tsx +3 -3
- package/app/components/shared/ModelInput.tsx +18 -4
- package/app/components/shared/ProviderSelect.tsx +126 -134
- package/app/hooks/useAskChat.ts +97 -13
- package/app/hooks/useAskPanel.ts +17 -1
- package/app/lib/agent/context.ts +65 -0
- package/app/lib/agent/providers.ts +25 -0
- package/app/lib/agent/tools.ts +1 -1
- package/app/lib/custom-endpoints.ts +129 -29
- package/app/lib/i18n/modules/settings.ts +20 -0
- package/app/lib/pi-integration/skills.ts +16 -4
- package/app/lib/settings-ai-client.ts +17 -8
- package/app/lib/settings.ts +64 -90
- package/app/lib/types.ts +4 -0
- package/package.json +1 -1
- package/_standalone/.next/server/chunks/530.js +0 -218
- package/_standalone/.next/server/chunks/9007.js +0 -2
- package/_standalone/.next/server/chunks/9137.js +0 -52
- package/_standalone/.next/static/chunks/1309-373ade1b40aea186.js +0 -1
- package/_standalone/.next/static/chunks/3165-9189a38fd9ebf6f2.js +0 -1
- package/_standalone/.next/static/chunks/4587-5d06728133fff222.js +0 -1
- package/_standalone/.next/static/chunks/6261-5ce86db54b19ae46.js +0 -1
- package/_standalone/.next/static/chunks/6636-9bbc90fb3b8731fe.js +0 -6
- package/_standalone/.next/static/chunks/7637-904b0a381dc3ec02.js +0 -1
- package/_standalone/.next/static/chunks/8520-84e607f33c409f91.js +0 -22
- package/_standalone/.next/static/chunks/9207-9a4a1a1ede4f8e6e.js +0 -1
- package/_standalone/.next/static/chunks/app/echo/[segment]/page-bc5e104eb7ae6327.js +0 -11
- package/_standalone/.next/static/chunks/app/setup/page-79acb0baf38184c6.js +0 -1
- package/_standalone/.next/static/chunks/app/trash/page-d040db56863da504.js +0 -1
- package/_standalone/.next/static/css/1287672978833d07.css +0 -1
- package/_standalone/lib/agent/context.ts +0 -403
- /package/_standalone/.next/static/{X86rF8dKEO0InosOw4a2_ → eIlwbGas1iRGonlPyEwj7}/_buildManifest.js +0 -0
- /package/_standalone/.next/static/{X86rF8dKEO0InosOw4a2_ → eIlwbGas1iRGonlPyEwj7}/_ssgManifest.js +0 -0
|
@@ -1,128 +1,52 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { X, AlertCircle, Loader2 } from 'lucide-react';
|
|
3
|
+
import { X } from 'lucide-react';
|
|
5
4
|
import { useLocale } from '@/lib/stores/locale-store';
|
|
6
5
|
import { type Messages } from '@/lib/i18n';
|
|
7
|
-
import { type CustomProvider
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import
|
|
6
|
+
import { type CustomProvider } from '@/lib/custom-endpoints';
|
|
7
|
+
import { useCustomProviderForm } from './useCustomProviderForm';
|
|
8
|
+
import CustomProviderFields from './CustomProviderFields';
|
|
9
|
+
import { TestButton } from './TestButton';
|
|
11
10
|
|
|
12
11
|
interface ProviderModalProps {
|
|
13
12
|
isOpen: boolean;
|
|
14
13
|
onClose: () => void;
|
|
15
14
|
onSave: (provider: CustomProvider) => void;
|
|
16
15
|
initialProvider?: CustomProvider;
|
|
16
|
+
existingNames?: string[];
|
|
17
17
|
t: Messages;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Modal wrapper — renders nothing when closed, remounts inner form
|
|
22
|
+
* on open so hook state resets cleanly.
|
|
23
|
+
*/
|
|
22
24
|
export default function ProviderModal({
|
|
23
|
-
isOpen,
|
|
24
|
-
onClose,
|
|
25
|
-
onSave,
|
|
26
|
-
initialProvider,
|
|
27
|
-
t,
|
|
25
|
+
isOpen, onClose, onSave, initialProvider, existingNames, t,
|
|
28
26
|
}: ProviderModalProps) {
|
|
29
|
-
|
|
30
|
-
const [name, setName] = useState('');
|
|
31
|
-
const [baseProviderId, setBaseProviderId] = useState<ProviderId>('openai');
|
|
32
|
-
const [apiKey, setApiKey] = useState('');
|
|
33
|
-
const [model, setModel] = useState('');
|
|
34
|
-
const [baseUrl, setBaseUrl] = useState('');
|
|
35
|
-
const [showApiKey, setShowApiKey] = useState(false);
|
|
36
|
-
const [testState, setTestState] = useState<TestState>('idle');
|
|
37
|
-
const [testError, setTestError] = useState('');
|
|
38
|
-
const [isSaving, setIsSaving] = useState(false);
|
|
39
|
-
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
if (initialProvider) {
|
|
42
|
-
setName(initialProvider.name);
|
|
43
|
-
setBaseProviderId(initialProvider.baseProviderId);
|
|
44
|
-
setApiKey(initialProvider.apiKey === '***set***' ? '' : initialProvider.apiKey);
|
|
45
|
-
setModel(initialProvider.model);
|
|
46
|
-
setBaseUrl(initialProvider.baseUrl);
|
|
47
|
-
} else {
|
|
48
|
-
setName('');
|
|
49
|
-
setBaseProviderId('openai');
|
|
50
|
-
setApiKey('');
|
|
51
|
-
setModel('');
|
|
52
|
-
setBaseUrl('');
|
|
53
|
-
}
|
|
54
|
-
setShowApiKey(false);
|
|
55
|
-
setTestState('idle');
|
|
56
|
-
setTestError('');
|
|
57
|
-
}, [initialProvider, isOpen]);
|
|
58
|
-
|
|
59
|
-
const handleTest = useCallback(async () => {
|
|
60
|
-
if (!name.trim() || !baseUrl.trim() || !model.trim()) {
|
|
61
|
-
setTestError('Name, base URL, and model are required');
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
setTestState('testing');
|
|
66
|
-
setTestError('');
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const res = await fetch('/api/settings/test-key', {
|
|
70
|
-
method: 'POST',
|
|
71
|
-
headers: { 'Content-Type': 'application/json' },
|
|
72
|
-
body: JSON.stringify({
|
|
73
|
-
customProviderId: initialProvider?.id,
|
|
74
|
-
apiKey,
|
|
75
|
-
model,
|
|
76
|
-
baseUrl,
|
|
77
|
-
baseProviderId,
|
|
78
|
-
}),
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const json = await res.json();
|
|
82
|
-
|
|
83
|
-
if (json.ok) {
|
|
84
|
-
setTestState('ok');
|
|
85
|
-
} else {
|
|
86
|
-
setTestState('error');
|
|
87
|
-
setTestError(json.error || 'Test failed');
|
|
88
|
-
}
|
|
89
|
-
} catch (err) {
|
|
90
|
-
setTestState('error');
|
|
91
|
-
setTestError('Network error');
|
|
92
|
-
}
|
|
93
|
-
}, [name, baseUrl, model, apiKey, baseProviderId, initialProvider?.id]);
|
|
94
|
-
|
|
95
|
-
const handleSave = useCallback(async () => {
|
|
96
|
-
if (!name.trim() || !baseUrl.trim() || !model.trim()) {
|
|
97
|
-
setTestError('Name, base URL, and model are required');
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
setIsSaving(true);
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
const provider: CustomProvider = {
|
|
105
|
-
id: initialProvider?.id || generateCustomProviderId(),
|
|
106
|
-
name: name.trim(),
|
|
107
|
-
baseProviderId,
|
|
108
|
-
apiKey,
|
|
109
|
-
model: model.trim(),
|
|
110
|
-
baseUrl: baseUrl.trim(),
|
|
111
|
-
};
|
|
27
|
+
if (!isOpen) return null;
|
|
112
28
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
29
|
+
return (
|
|
30
|
+
<ProviderModalInner
|
|
31
|
+
key={initialProvider?.id ?? '__new__'}
|
|
32
|
+
onClose={onClose}
|
|
33
|
+
onSave={onSave}
|
|
34
|
+
initialProvider={initialProvider}
|
|
35
|
+
existingNames={existingNames}
|
|
36
|
+
t={t}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
118
40
|
|
|
119
|
-
|
|
41
|
+
function ProviderModalInner({
|
|
42
|
+
onClose, onSave, initialProvider, existingNames, t,
|
|
43
|
+
}: Omit<ProviderModalProps, 'isOpen'>) {
|
|
44
|
+
const { locale } = useLocale();
|
|
45
|
+
const form = useCustomProviderForm({ initial: initialProvider, onSave, locale, existingNames });
|
|
120
46
|
|
|
121
|
-
const preset = PROVIDER_PRESETS[baseProviderId];
|
|
122
|
-
const displayName = locale === 'zh' ? preset.nameZh : preset.name;
|
|
123
47
|
const title = initialProvider
|
|
124
|
-
?
|
|
125
|
-
:
|
|
48
|
+
? (locale === 'zh' ? '编辑 Provider' : 'Edit Provider')
|
|
49
|
+
: (locale === 'zh' ? '添加 Provider' : 'Add Provider');
|
|
126
50
|
|
|
127
51
|
return (
|
|
128
52
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={onClose}>
|
|
@@ -130,138 +54,31 @@ export default function ProviderModal({
|
|
|
130
54
|
className="bg-card border border-border rounded-lg shadow-lg p-6 max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto"
|
|
131
55
|
onClick={e => e.stopPropagation()}
|
|
132
56
|
>
|
|
133
|
-
{/* Header */}
|
|
134
57
|
<div className="flex items-center justify-between mb-4">
|
|
135
58
|
<h2 className="font-semibold text-foreground">{title}</h2>
|
|
136
|
-
<button
|
|
137
|
-
type="button"
|
|
138
|
-
onClick={onClose}
|
|
139
|
-
className="p-1 hover:bg-muted rounded transition-colors"
|
|
140
|
-
>
|
|
59
|
+
<button type="button" onClick={onClose} className="p-1 hover:bg-muted rounded transition-colors">
|
|
141
60
|
<X size={16} />
|
|
142
61
|
</button>
|
|
143
62
|
</div>
|
|
144
63
|
|
|
145
|
-
{
|
|
146
|
-
<div className="space-y-3">
|
|
147
|
-
{/* Name */}
|
|
148
|
-
<Field
|
|
149
|
-
label={t.settings?.customProviders?.modal?.fieldName ?? 'Name'}
|
|
150
|
-
hint={t.settings?.customProviders?.modal?.fieldNameHint}
|
|
151
|
-
>
|
|
152
|
-
<Input
|
|
153
|
-
value={name}
|
|
154
|
-
onChange={e => setName(e.target.value)}
|
|
155
|
-
placeholder="Company GPT-4"
|
|
156
|
-
/>
|
|
157
|
-
</Field>
|
|
158
|
-
|
|
159
|
-
{/* Protocol */}
|
|
160
|
-
<div>
|
|
161
|
-
<label className="text-xs font-medium text-foreground block mb-2">
|
|
162
|
-
{t.settings?.customProviders?.modal?.fieldProtocol ?? 'Protocol'}
|
|
163
|
-
</label>
|
|
164
|
-
<p className="text-2xs text-muted-foreground mb-2">
|
|
165
|
-
{t.settings?.customProviders?.modal?.fieldProtocolHint}
|
|
166
|
-
</p>
|
|
167
|
-
<ProviderSelect
|
|
168
|
-
value={baseProviderId}
|
|
169
|
-
onChange={id => id !== 'skip' && setBaseProviderId(id as ProviderId)}
|
|
170
|
-
compact
|
|
171
|
-
/>
|
|
172
|
-
</div>
|
|
173
|
-
|
|
174
|
-
{/* Base URL */}
|
|
175
|
-
<Field
|
|
176
|
-
label={t.settings?.customProviders?.modal?.fieldBaseUrl ?? 'Base URL'}
|
|
177
|
-
hint={t.settings?.customProviders?.modal?.fieldBaseUrlHint}
|
|
178
|
-
>
|
|
179
|
-
<Input
|
|
180
|
-
value={baseUrl}
|
|
181
|
-
onChange={e => setBaseUrl(e.target.value)}
|
|
182
|
-
placeholder="https://api.example.com/v1"
|
|
183
|
-
/>
|
|
184
|
-
</Field>
|
|
185
|
-
|
|
186
|
-
{/* API Key */}
|
|
187
|
-
<Field
|
|
188
|
-
label={t.settings?.customProviders?.modal?.fieldApiKey ?? 'API Key'}
|
|
189
|
-
hint={t.settings?.customProviders?.modal?.fieldApiKeyHint}
|
|
190
|
-
>
|
|
191
|
-
<div className="flex gap-2 items-center">
|
|
192
|
-
<Input
|
|
193
|
-
type={showApiKey ? 'text' : 'password'}
|
|
194
|
-
value={apiKey}
|
|
195
|
-
onChange={e => setApiKey(e.target.value)}
|
|
196
|
-
placeholder="••••••••"
|
|
197
|
-
/>
|
|
198
|
-
<button
|
|
199
|
-
type="button"
|
|
200
|
-
onClick={() => setShowApiKey(!showApiKey)}
|
|
201
|
-
className="text-xs text-muted-foreground hover:text-foreground transition-colors px-2 py-1"
|
|
202
|
-
>
|
|
203
|
-
{showApiKey ? 'Hide' : 'Show'}
|
|
204
|
-
</button>
|
|
205
|
-
</div>
|
|
206
|
-
</Field>
|
|
207
|
-
|
|
208
|
-
{/* Model */}
|
|
209
|
-
<Field
|
|
210
|
-
label={t.settings?.customProviders?.modal?.fieldModel ?? 'Model'}
|
|
211
|
-
hint={t.settings?.customProviders?.modal?.fieldModelHint}
|
|
212
|
-
>
|
|
213
|
-
<Input
|
|
214
|
-
value={model}
|
|
215
|
-
onChange={e => setModel(e.target.value)}
|
|
216
|
-
placeholder="gpt-4-turbo"
|
|
217
|
-
/>
|
|
218
|
-
</Field>
|
|
219
|
-
|
|
220
|
-
{/* Error message */}
|
|
221
|
-
{testError && testState !== 'ok' && (
|
|
222
|
-
<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">
|
|
223
|
-
<AlertCircle size={13} className="shrink-0 mt-0.5" />
|
|
224
|
-
<span>{testError}</span>
|
|
225
|
-
</div>
|
|
226
|
-
)}
|
|
227
|
-
|
|
228
|
-
{/* Success message */}
|
|
229
|
-
{testState === 'ok' && (
|
|
230
|
-
<div className="flex items-center gap-2 text-xs text-success bg-success/10 border border-success/20 rounded-lg px-3 py-2">
|
|
231
|
-
<span>✓ {t.settings?.customProviders?.modal?.success ?? 'Connected'}</span>
|
|
232
|
-
</div>
|
|
233
|
-
)}
|
|
234
|
-
</div>
|
|
64
|
+
<CustomProviderFields form={form} t={t} locale={locale} layout="full" />
|
|
235
65
|
|
|
236
|
-
{/* Buttons */}
|
|
237
66
|
<div className="flex gap-2 mt-6">
|
|
238
67
|
<button
|
|
239
68
|
type="button"
|
|
240
69
|
onClick={onClose}
|
|
241
|
-
|
|
242
|
-
className="flex-1 px-3 py-2 text-sm rounded border border-border text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors disabled:opacity-40"
|
|
70
|
+
className="flex-1 px-3 py-2 text-sm rounded-lg border border-border text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors"
|
|
243
71
|
>
|
|
244
72
|
{t.settings?.customProviders?.modal?.buttonCancel ?? 'Cancel'}
|
|
245
73
|
</button>
|
|
74
|
+
<TestButton result={form.testResult} disabled={!form.canSave} onTest={form.handleTest} t={t} />
|
|
246
75
|
<button
|
|
247
76
|
type="button"
|
|
248
|
-
onClick={
|
|
249
|
-
disabled={
|
|
250
|
-
className="flex-1 px-
|
|
251
|
-
>
|
|
252
|
-
{testState === 'testing' && <Loader2 size={12} className="animate-spin" />}
|
|
253
|
-
{testState === 'testing'
|
|
254
|
-
? (t.settings?.customProviders?.modal?.validating ?? 'Testing...')
|
|
255
|
-
: (t.settings?.customProviders?.modal?.buttonSave ?? 'Save & Test')}
|
|
256
|
-
</button>
|
|
257
|
-
<button
|
|
258
|
-
type="button"
|
|
259
|
-
onClick={handleSave}
|
|
260
|
-
disabled={isSaving}
|
|
261
|
-
className="flex-1 px-3 py-2 text-sm rounded font-medium bg-[var(--amber)] text-[var(--amber-foreground)] hover:bg-[var(--amber)]/90 transition-colors disabled:opacity-40 inline-flex items-center justify-center gap-1"
|
|
77
|
+
onClick={form.handleSave}
|
|
78
|
+
disabled={!form.canSave}
|
|
79
|
+
className="flex-1 px-4 py-2 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"
|
|
262
80
|
>
|
|
263
|
-
{
|
|
264
|
-
Save
|
|
81
|
+
{locale === 'zh' ? '保存' : 'Save'}
|
|
265
82
|
</button>
|
|
266
83
|
</div>
|
|
267
84
|
</div>
|
|
@@ -121,7 +121,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
|
|
|
121
121
|
await apiFetch('/api/settings', {
|
|
122
122
|
method: 'POST',
|
|
123
123
|
headers: { 'Content-Type': 'application/json' },
|
|
124
|
-
body: JSON.stringify({ ai: d.ai, agent: d.agent, mindRoot: d.mindRoot, webPassword: d.webPassword, authToken: d.authToken
|
|
124
|
+
body: JSON.stringify({ ai: d.ai, agent: d.agent, mindRoot: d.mindRoot, webPassword: d.webPassword, authToken: d.authToken }),
|
|
125
125
|
});
|
|
126
126
|
setStatus('saved');
|
|
127
127
|
window.dispatchEvent(new Event('mindos:settings-changed'));
|
|
@@ -168,7 +168,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
|
|
|
168
168
|
apiFetch('/api/settings', {
|
|
169
169
|
method: 'POST',
|
|
170
170
|
headers: { 'Content-Type': 'application/json' },
|
|
171
|
-
body: JSON.stringify({ ai: d.ai, agent: d.agent, mindRoot: d.mindRoot, webPassword: d.webPassword, authToken: d.authToken
|
|
171
|
+
body: JSON.stringify({ ai: d.ai, agent: d.agent, mindRoot: d.mindRoot, webPassword: d.webPassword, authToken: d.authToken }),
|
|
172
172
|
}).catch(() => {});
|
|
173
173
|
}
|
|
174
174
|
};
|
|
@@ -179,10 +179,6 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
|
|
|
179
179
|
setData(d => d ? { ...d, ai: { ...d.ai, ...patch } } : d);
|
|
180
180
|
}, []);
|
|
181
181
|
|
|
182
|
-
const updateCustomProviders = useCallback((providers: import('@/lib/custom-endpoints').CustomProvider[]) => {
|
|
183
|
-
setData(d => d ? { ...d, customProviders: providers } : d);
|
|
184
|
-
}, []);
|
|
185
|
-
|
|
186
182
|
const updateAgent = useCallback((patch: Partial<AgentSettings>) => {
|
|
187
183
|
setData(d => d ? { ...d, agent: { ...(d.agent ?? {}), ...patch } } : d);
|
|
188
184
|
}, []);
|
|
@@ -190,11 +186,8 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
|
|
|
190
186
|
const restoreFromEnv = useCallback(async () => {
|
|
191
187
|
if (!data) return;
|
|
192
188
|
const defaults: AiSettings = {
|
|
193
|
-
|
|
194
|
-
providers:
|
|
195
|
-
anthropic: { apiKey: '', model: 'claude-sonnet-4-6' },
|
|
196
|
-
openai: { apiKey: '', model: 'gpt-5.4', baseUrl: '' },
|
|
197
|
-
},
|
|
189
|
+
activeProvider: '',
|
|
190
|
+
providers: [],
|
|
198
191
|
};
|
|
199
192
|
setData(d => d ? { ...d, ai: defaults } : d);
|
|
200
193
|
const DEBOUNCE_DELAY = 800;
|
|
@@ -234,7 +227,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
|
|
|
234
227
|
</div>
|
|
235
228
|
) : (
|
|
236
229
|
<>
|
|
237
|
-
{tab === 'ai' && data?.ai && <AiTab data={data} updateAi={updateAi} updateAgent={updateAgent}
|
|
230
|
+
{tab === 'ai' && data?.ai && <AiTab data={data} updateAi={updateAi} updateAgent={updateAgent} t={t} />}
|
|
238
231
|
{tab === 'appearance' && <AppearanceTab font={font} setFont={setFont} fontSize={fontSize} setFontSize={setFontSize} contentWidth={contentWidth} setContentWidth={setContentWidth} dark={dark} setDark={setDark} locale={locale} setLocale={setLocale} t={t} />}
|
|
239
232
|
{tab === 'knowledge' && data && <KnowledgeTab data={data} setData={setData} t={t} />}
|
|
240
233
|
{tab === 'sync' && <SyncTab t={t} />}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { AlertCircle, Loader2, Check, Zap } from 'lucide-react';
|
|
4
|
+
import type { TestResult, ErrorCode } from './useCustomProviderForm';
|
|
5
|
+
import type { AiTabProps } from './types';
|
|
6
|
+
|
|
7
|
+
function errorMessage(t: AiTabProps['t'], code?: ErrorCode): string {
|
|
8
|
+
switch (code) {
|
|
9
|
+
case 'auth_error': return t.settings.ai.testKeyAuthError;
|
|
10
|
+
case 'model_not_found': return t.settings.ai.testKeyModelNotFound;
|
|
11
|
+
case 'rate_limited': return t.settings.ai.testKeyRateLimited;
|
|
12
|
+
case 'network_error': return t.settings.ai.testKeyNetworkError;
|
|
13
|
+
default: return t.settings.ai.testKeyUnknown;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Shared test-connection button used by both built-in and custom provider forms.
|
|
19
|
+
* Shows contextual icon + text based on test result state (idle/testing/ok/error).
|
|
20
|
+
*/
|
|
21
|
+
export function TestButton({
|
|
22
|
+
result, disabled, onTest, t,
|
|
23
|
+
}: {
|
|
24
|
+
result: TestResult;
|
|
25
|
+
disabled: boolean;
|
|
26
|
+
onTest: () => void;
|
|
27
|
+
t: AiTabProps['t'];
|
|
28
|
+
}) {
|
|
29
|
+
const isTesting = result.state === 'testing';
|
|
30
|
+
const isOk = result.state === 'ok';
|
|
31
|
+
const isError = result.state === 'error';
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
disabled={disabled || isTesting}
|
|
37
|
+
onClick={onTest}
|
|
38
|
+
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 ${
|
|
39
|
+
isOk
|
|
40
|
+
? 'bg-success/10 text-success border border-success/20'
|
|
41
|
+
: isError
|
|
42
|
+
? 'bg-destructive/8 text-destructive border border-destructive/20 hover:bg-destructive/12'
|
|
43
|
+
: 'border border-border text-muted-foreground hover:text-foreground hover:border-foreground/20 disabled:opacity-40'
|
|
44
|
+
}`}
|
|
45
|
+
>
|
|
46
|
+
{isTesting ? (
|
|
47
|
+
<Loader2 size={13} className="animate-spin" />
|
|
48
|
+
) : isOk ? (
|
|
49
|
+
<Check size={13} />
|
|
50
|
+
) : isError ? (
|
|
51
|
+
<AlertCircle size={13} />
|
|
52
|
+
) : (
|
|
53
|
+
<Zap size={13} />
|
|
54
|
+
)}
|
|
55
|
+
{isTesting
|
|
56
|
+
? t.settings.ai.testKeyTesting
|
|
57
|
+
: isOk && result.latency != null
|
|
58
|
+
? t.settings.ai.testKeyOk(result.latency)
|
|
59
|
+
: isError
|
|
60
|
+
? errorMessage(t, result.code)
|
|
61
|
+
: t.settings.ai.testKey}
|
|
62
|
+
</button>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
import type { Locale, Messages } from '@/lib/i18n';
|
|
2
2
|
import type React from 'react';
|
|
3
|
-
import type {
|
|
4
|
-
import type { CustomProvider } from '@/lib/custom-endpoints';
|
|
5
|
-
|
|
6
|
-
export interface ProviderConfig {
|
|
7
|
-
apiKey: string;
|
|
8
|
-
model: string;
|
|
9
|
-
baseUrl?: string;
|
|
10
|
-
}
|
|
3
|
+
import type { Provider } from '@/lib/custom-endpoints';
|
|
11
4
|
|
|
12
5
|
export interface AiSettings {
|
|
13
|
-
|
|
14
|
-
providers:
|
|
6
|
+
activeProvider: string;
|
|
7
|
+
providers: Provider[];
|
|
15
8
|
}
|
|
16
9
|
|
|
17
10
|
export interface AgentSettings {
|
|
@@ -31,7 +24,6 @@ export interface SettingsData {
|
|
|
31
24
|
mcpPort?: number;
|
|
32
25
|
envOverrides?: Record<string, boolean>;
|
|
33
26
|
envValues?: Record<string, string>;
|
|
34
|
-
customProviders?: CustomProvider[];
|
|
35
27
|
}
|
|
36
28
|
|
|
37
29
|
export type Tab = 'ai' | 'appearance' | 'knowledge' | 'mcp' | 'sync' | 'update' | 'uninstall';
|
|
@@ -161,7 +153,6 @@ export interface AiTabProps {
|
|
|
161
153
|
data: SettingsData;
|
|
162
154
|
updateAi: (patch: Partial<AiSettings>) => void;
|
|
163
155
|
updateAgent: (patch: Partial<AgentSettings>) => void;
|
|
164
|
-
updateCustomProviders: (providers: CustomProvider[]) => void;
|
|
165
156
|
t: Messages;
|
|
166
157
|
}
|
|
167
158
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from 'react';
|
|
4
|
+
import { type ProviderId } from '@/lib/agent/providers';
|
|
5
|
+
import { type Provider, generateProviderId } from '@/lib/custom-endpoints';
|
|
6
|
+
|
|
7
|
+
export type TestState = 'idle' | 'testing' | 'ok' | 'error';
|
|
8
|
+
export type ErrorCode = 'auth_error' | 'model_not_found' | 'rate_limited' | 'network_error' | 'unknown';
|
|
9
|
+
|
|
10
|
+
export interface TestResult {
|
|
11
|
+
state: TestState;
|
|
12
|
+
latency?: number;
|
|
13
|
+
error?: string;
|
|
14
|
+
code?: ErrorCode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CustomProviderFormState {
|
|
18
|
+
name: string;
|
|
19
|
+
setName: (v: string) => void;
|
|
20
|
+
protocol: ProviderId;
|
|
21
|
+
setProtocol: (v: ProviderId) => void;
|
|
22
|
+
apiKey: string;
|
|
23
|
+
setApiKey: (v: string) => void;
|
|
24
|
+
model: string;
|
|
25
|
+
setModel: (v: string) => void;
|
|
26
|
+
baseUrl: string;
|
|
27
|
+
setBaseUrl: (v: string) => void;
|
|
28
|
+
testResult: TestResult;
|
|
29
|
+
canSave: boolean;
|
|
30
|
+
isDuplicateName: boolean;
|
|
31
|
+
handleTest: () => Promise<void>;
|
|
32
|
+
handleSave: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Shared form state + test/save logic for provider forms.
|
|
37
|
+
* Used by both the inline form (AiTab) and the modal (ProviderModal).
|
|
38
|
+
*/
|
|
39
|
+
export function useCustomProviderForm({
|
|
40
|
+
initial,
|
|
41
|
+
onSave,
|
|
42
|
+
locale,
|
|
43
|
+
existingNames,
|
|
44
|
+
}: {
|
|
45
|
+
initial?: Provider;
|
|
46
|
+
onSave: (provider: Provider) => void;
|
|
47
|
+
locale: string;
|
|
48
|
+
existingNames?: string[];
|
|
49
|
+
}): CustomProviderFormState {
|
|
50
|
+
const [name, setName] = useState(initial?.name ?? '');
|
|
51
|
+
const [protocol, setProtocol] = useState<ProviderId>(initial?.protocol ?? 'openai');
|
|
52
|
+
const [apiKey, setApiKey] = useState(initial?.apiKey ?? '');
|
|
53
|
+
const [model, setModel] = useState(initial?.model ?? '');
|
|
54
|
+
const [baseUrl, setBaseUrl] = useState(initial?.baseUrl ?? '');
|
|
55
|
+
const [testResult, setTestResult] = useState<TestResult>({ state: 'idle' });
|
|
56
|
+
|
|
57
|
+
// Check for duplicate name (exclude the provider being edited)
|
|
58
|
+
const trimmedName = name.trim();
|
|
59
|
+
const isDuplicateName = !!(trimmedName && existingNames?.some(
|
|
60
|
+
n => n.toLowerCase() === trimmedName.toLowerCase(),
|
|
61
|
+
));
|
|
62
|
+
|
|
63
|
+
const canSave = !!(trimmedName && baseUrl.trim() && model.trim() && !isDuplicateName);
|
|
64
|
+
|
|
65
|
+
const handleTest = useCallback(async () => {
|
|
66
|
+
if (!canSave) {
|
|
67
|
+
setTestResult({
|
|
68
|
+
state: 'error',
|
|
69
|
+
error: locale === 'zh' ? '名称、接口地址和模型为必填' : 'Name, base URL, and model are required',
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
setTestResult({ state: 'testing' });
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetch('/api/settings/test-key', {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: { 'Content-Type': 'application/json' },
|
|
78
|
+
body: JSON.stringify(
|
|
79
|
+
initial?.id
|
|
80
|
+
? { provider: initial.id, apiKey, model, baseUrl }
|
|
81
|
+
: { protocol, apiKey, model, baseUrl },
|
|
82
|
+
),
|
|
83
|
+
});
|
|
84
|
+
const json = await res.json();
|
|
85
|
+
if (json.ok) {
|
|
86
|
+
setTestResult({ state: 'ok', latency: json.latency });
|
|
87
|
+
} else {
|
|
88
|
+
setTestResult({ state: 'error', error: json.error || 'Test failed', code: json.code });
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
setTestResult({ state: 'error', code: 'network_error', error: 'Network error' });
|
|
92
|
+
}
|
|
93
|
+
}, [canSave, apiKey, model, baseUrl, protocol, locale, initial?.id]);
|
|
94
|
+
|
|
95
|
+
const handleSave = useCallback(() => {
|
|
96
|
+
if (isDuplicateName) {
|
|
97
|
+
setTestResult({
|
|
98
|
+
state: 'error',
|
|
99
|
+
error: locale === 'zh' ? '名称已存在,请使用其他名称' : 'Name already exists, please use a different name',
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!canSave) {
|
|
104
|
+
setTestResult({
|
|
105
|
+
state: 'error',
|
|
106
|
+
error: locale === 'zh' ? '名称、接口地址和模型为必填' : 'Name, base URL, and model are required',
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
onSave({
|
|
111
|
+
id: initial?.id || generateProviderId(),
|
|
112
|
+
name: name.trim(),
|
|
113
|
+
protocol,
|
|
114
|
+
apiKey,
|
|
115
|
+
model: model.trim(),
|
|
116
|
+
baseUrl: baseUrl.trim(),
|
|
117
|
+
});
|
|
118
|
+
}, [canSave, isDuplicateName, name, protocol, apiKey, model, baseUrl, initial?.id, onSave, locale]);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
name, setName,
|
|
122
|
+
protocol, setProtocol,
|
|
123
|
+
apiKey, setApiKey,
|
|
124
|
+
model, setModel,
|
|
125
|
+
baseUrl, setBaseUrl,
|
|
126
|
+
testResult,
|
|
127
|
+
canSave,
|
|
128
|
+
isDuplicateName,
|
|
129
|
+
handleTest,
|
|
130
|
+
handleSave,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
4
|
import { ChevronDown, ChevronRight, Copy, ExternalLink } from 'lucide-react';
|
|
5
|
-
import { Field, Input,
|
|
5
|
+
import { Field, Input, PasswordInput } from '@/components/settings/Primitives';
|
|
6
6
|
import type { SetupState, SetupMessages, PortStatus, ProviderSetupConfig } from './types';
|
|
7
7
|
import type { ProviderId } from '@/lib/agent/providers';
|
|
8
8
|
import { PROVIDER_PRESETS, isProviderId, getApiKeyEnvVar, getDefaultBaseUrl } from '@/lib/agent/providers';
|
|
@@ -58,7 +58,7 @@ export default function StepAI({ state, update, s, onCopyToken, webPortStatus, m
|
|
|
58
58
|
<div className="space-y-5">
|
|
59
59
|
<ProviderSelect
|
|
60
60
|
value={state.provider}
|
|
61
|
-
onChange={id => update('provider', id)}
|
|
61
|
+
onChange={id => update('provider', id as ProviderId | 'skip')}
|
|
62
62
|
showSkip
|
|
63
63
|
compact
|
|
64
64
|
configuredProviders={configuredProviders}
|
|
@@ -68,7 +68,7 @@ export default function StepAI({ state, update, s, onCopyToken, webPortStatus, m
|
|
|
68
68
|
<div className="space-y-4 pt-2">
|
|
69
69
|
{/* API Key */}
|
|
70
70
|
<Field label={s.apiKey}>
|
|
71
|
-
<
|
|
71
|
+
<PasswordInput
|
|
72
72
|
value={currentConfig.apiKey}
|
|
73
73
|
onChange={v => patchConfig({ apiKey: v })}
|
|
74
74
|
placeholder={currentConfig.apiKeyMask || `${getApiKeyEnvVar(currentProvider as ProviderId) ?? 'API Key'}...`}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
|
3
|
+
import { useState, useRef, useCallback, useEffect, useMemo, type CSSProperties } from 'react';
|
|
4
4
|
import { ChevronDown, Loader2 } from 'lucide-react';
|
|
5
5
|
import { Input } from '@/components/settings/Primitives';
|
|
6
6
|
import { type ProviderId, PROVIDER_PRESETS } from '@/lib/agent/providers';
|
|
@@ -32,7 +32,9 @@ export default function ModelInput({
|
|
|
32
32
|
const [focused, setFocused] = useState(false);
|
|
33
33
|
const [highlightIdx, setHighlightIdx] = useState(-1);
|
|
34
34
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
35
|
+
const inputRowRef = useRef<HTMLDivElement>(null);
|
|
35
36
|
const listRef = useRef<HTMLDivElement>(null);
|
|
37
|
+
const [dropdownStyle, setDropdownStyle] = useState<CSSProperties>({});
|
|
36
38
|
const fetchedRef = useRef(false);
|
|
37
39
|
const fetchVersionRef = useRef(0);
|
|
38
40
|
const loadingRef = useRef(false);
|
|
@@ -107,6 +109,18 @@ export default function ModelInput({
|
|
|
107
109
|
|
|
108
110
|
const showDropdown = open || (focused && models !== null && value.trim().length > 0 && filtered.length > 0);
|
|
109
111
|
|
|
112
|
+
// Compute fixed position for dropdown so it escapes overflow:auto ancestors
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!showDropdown || !inputRowRef.current) return;
|
|
115
|
+
const rect = inputRowRef.current.getBoundingClientRect();
|
|
116
|
+
setDropdownStyle({
|
|
117
|
+
position: 'fixed',
|
|
118
|
+
top: rect.bottom + 4,
|
|
119
|
+
left: rect.left,
|
|
120
|
+
width: rect.width,
|
|
121
|
+
});
|
|
122
|
+
}, [showDropdown]);
|
|
123
|
+
|
|
110
124
|
useEffect(() => { setHighlightIdx(-1); }, [filtered]);
|
|
111
125
|
|
|
112
126
|
useEffect(() => {
|
|
@@ -150,7 +164,7 @@ export default function ModelInput({
|
|
|
150
164
|
|
|
151
165
|
return (
|
|
152
166
|
<div ref={containerRef} className="relative">
|
|
153
|
-
<div className="flex gap-1.5">
|
|
167
|
+
<div ref={inputRowRef} className="flex gap-1.5">
|
|
154
168
|
<Input
|
|
155
169
|
value={value}
|
|
156
170
|
onChange={e => { onChange(e.target.value); if (!open) setFocused(true); }}
|
|
@@ -175,7 +189,7 @@ export default function ModelInput({
|
|
|
175
189
|
</div>
|
|
176
190
|
{error && <p className="text-xs text-error mt-1">{error}</p>}
|
|
177
191
|
{showDropdown && filtered.length > 0 && (
|
|
178
|
-
<div ref={listRef} className="
|
|
192
|
+
<div ref={listRef} style={dropdownStyle} className="fixed z-[9999] max-h-48 overflow-y-auto rounded-lg border border-border bg-popover shadow-lg">
|
|
179
193
|
{filtered.map((m, i) => {
|
|
180
194
|
const isMatch = !value.trim() || m.toLowerCase().includes(value.toLowerCase());
|
|
181
195
|
return (
|
|
@@ -197,7 +211,7 @@ export default function ModelInput({
|
|
|
197
211
|
</div>
|
|
198
212
|
)}
|
|
199
213
|
{open && filtered.length === 0 && (
|
|
200
|
-
<div className="
|
|
214
|
+
<div style={dropdownStyle} className="fixed z-[9999] rounded-lg border border-border bg-popover shadow-lg px-3 py-2 text-xs text-muted-foreground">
|
|
201
215
|
{noModelsLabel}
|
|
202
216
|
</div>
|
|
203
217
|
)}
|