@nextclaw/ui 0.12.1 → 0.12.2
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/CHANGELOG.md +38 -0
- package/dist/assets/ChannelsList-uKmkpD25.js +8 -0
- package/dist/assets/ChatPage-CslhBPfT.js +43 -0
- package/dist/assets/{DocBrowser-DjcghYGO.js → DocBrowser-C7-1sXqo.js} +1 -1
- package/dist/assets/DocBrowser-DQjtSsY3.js +1 -0
- package/dist/assets/{DocBrowserContext-CLlq7rZQ.js → DocBrowserContext-DN5tjUoS.js} +1 -1
- package/dist/assets/{LogoBadge-D_dOy5U3.js → LogoBadge-DDS1sU_U.js} +1 -1
- package/dist/assets/MarketplacePage-BZQW70ti.js +1 -0
- package/dist/assets/{MarketplacePage-BlIeNn3x.js → MarketplacePage-DE0QjYVv.js} +1 -1
- package/dist/assets/{McpMarketplacePage-mz2_IX1O.js → McpMarketplacePage-CeLvv1xy.js} +1 -1
- package/dist/assets/ModelConfig-D1JtGtQv.js +1 -0
- package/dist/assets/ProviderScopedModelInput-SAJH6nkC.js +1 -0
- package/dist/assets/{ProvidersList-B0RCb_Vg.js → ProvidersList-1rKi3aQT.js} +1 -1
- package/dist/assets/{RemoteAccessPage-CcfQjLtx.js → RemoteAccessPage-bIAKxDky.js} +1 -1
- package/dist/assets/RuntimeConfig-BTk9319O.js +1 -0
- package/dist/assets/SearchConfig-EjeszXbv.js +1 -0
- package/dist/assets/{SecretsConfig-DbiS3txa.js → SecretsConfig-cnAXvREZ.js} +1 -1
- package/dist/assets/{SessionsConfig-CbIOcAp8.js → SessionsConfig-BIXiDaK2.js} +1 -1
- package/dist/assets/{book-open-BLxSL7Dk.js → book-open-DvWqOode.js} +1 -1
- package/dist/assets/chat-session-display-D4bYa0b8.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-Bp0t5xoO.js → chunk-JZWAC4HX-CxfKRD7X.js} +1 -1
- package/dist/assets/{config-C96FWufn.js → config-BeGwf2Ao.js} +1 -1
- package/dist/assets/{createLucideIcon-B_U7Nq4F.js → createLucideIcon-C7MmdIX3.js} +1 -1
- package/dist/assets/{dist-D9pHzW9z.js → dist-B6VMuIQN.js} +1 -1
- package/dist/assets/{dist-BFY-GyT4.js → dist-RWNFhxvR.js} +1 -1
- package/dist/assets/{external-link-BydIQTIH.js → external-link-U86Acd1t.js} +1 -1
- package/dist/assets/{hash-Djdf0x1C.js → hash-D-OVfV3Z.js} +1 -1
- package/dist/assets/i18n-hM3v-3YG.js +1 -0
- package/dist/assets/{index-DqSv8Azv.js → index-8XNPYwJu.js} +3 -3
- package/dist/assets/index-CpxuJa9o.css +1 -0
- package/dist/assets/{label-Bvv4Mrea.js → label-CHJ1ATds.js} +1 -1
- package/dist/assets/loader-circle-C8cpaL0w.js +1 -0
- package/dist/assets/{logos-CGJJRI5_.js → logos-U1_qDA3U.js} +1 -1
- package/dist/assets/{page-layout-6Nm4Cnvr.js → page-layout-Z1klaUFW.js} +1 -1
- package/dist/assets/plus-CrkO1kob.js +1 -0
- package/dist/assets/{popover-b9rSYI6X.js → popover-xWbqMnIN.js} +1 -1
- package/dist/assets/{react-CDZz_StC.js → react-3YE87-lE.js} +1 -1
- package/dist/assets/{refresh-ccw-BvSSnnCw.js → refresh-ccw-JQh1lwq-.js} +1 -1
- package/dist/assets/{save-CAf0_-b9.js → save-4VRlzkii.js} +1 -1
- package/dist/assets/search-EX-Papzl.js +1 -0
- package/dist/assets/{security-config-DF66-l25.js → security-config-CGazBahs.js} +1 -1
- package/dist/assets/{select-CEIMqc0H.js → select-DF-AUoie.js} +1 -1
- package/dist/assets/skeleton-B0mmt1vo.js +1 -0
- package/dist/assets/{status-dot-CmQI5Qq2.js → status-dot-Bq_8Ojvv.js} +1 -1
- package/dist/assets/{switch-B7SxDXyR.js → switch-D7JF_RZ-.js} +1 -1
- package/dist/assets/{tabs-custom-Dxt6EJJW.js → tabs-custom-CLksZ2bO.js} +1 -1
- package/dist/assets/{trash-2-BnQ1PDTw.js → trash-2-VV8jvziy.js} +1 -1
- package/dist/assets/{useConfirmDialog-B-vMOmhG.js → useConfirmDialog-D6HxybcM.js} +1 -1
- package/dist/assets/{useMutation-Bi39Z9_J.js → useMutation-DBTWPbTg.js} +1 -1
- package/dist/assets/x-B4sxJkGY.js +1 -0
- package/dist/index.html +18 -18
- package/package.json +5 -5
- package/src/api/agents.ts +9 -1
- package/src/api/types.ts +25 -1
- package/src/components/agents/AgentDialogs.tsx +400 -0
- package/src/components/agents/AgentsPage.test.tsx +112 -1
- package/src/components/agents/AgentsPage.tsx +104 -112
- package/src/components/chat/ncp/NcpChatPage.tsx +1 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +6 -1
- package/src/components/chat/useChatSessionTypeState.ts +1 -1
- package/src/components/common/ProviderScopedModelInput.tsx +149 -0
- package/src/components/config/ChannelForm.test.tsx +60 -0
- package/src/components/config/ChannelForm.tsx +52 -12
- package/src/components/config/ModelConfig.test.tsx +61 -0
- package/src/components/config/ModelConfig.tsx +15 -90
- package/src/components/config/RuntimeConfig.tsx +2 -2
- package/src/components/config/SearchConfig.test.tsx +150 -0
- package/src/components/config/SearchConfig.tsx +257 -71
- package/src/components/config/runtime-config-agent.utils.ts +5 -4
- package/src/hooks/agents/useAgents.ts +18 -1
- package/src/lib/i18n.agents.ts +19 -0
- package/src/lib/i18n.search.ts +37 -0
- package/src/lib/i18n.ts +6 -26
- package/dist/assets/ChannelsList-DekMP4a3.js +0 -8
- package/dist/assets/ChatPage-Dgw4vlDt.js +0 -43
- package/dist/assets/DocBrowser-CExjX5is.js +0 -1
- package/dist/assets/MarketplacePage-DGfzg1LG.js +0 -1
- package/dist/assets/ModelConfig-C_49_a9v.js +0 -1
- package/dist/assets/RuntimeConfig-DBWzwoY-.js +0 -1
- package/dist/assets/SearchConfig-jSdwlH4b.js +0 -1
- package/dist/assets/chat-session-display-8yW6-mtm.js +0 -1
- package/dist/assets/i18n-DAekxt_G.js +0 -1
- package/dist/assets/index-CHEgQIiO.css +0 -1
- package/dist/assets/loader-circle-CGXXikVG.js +0 -1
- package/dist/assets/plus-CrW9BJDy.js +0 -1
- package/dist/assets/provider-models-IJDA940D.js +0 -1
- package/dist/assets/search-DgoXxocn.js +0 -1
- package/dist/assets/skeleton-BiPUQkOD.js +0 -1
- package/dist/assets/x-PBSiWt3l.js +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { ModelConfig } from '@/components/config/ModelConfig';
|
|
4
|
+
import { setLanguage } from '@/lib/i18n';
|
|
4
5
|
|
|
5
6
|
const mocks = vi.hoisted(() => ({
|
|
6
7
|
mutate: vi.fn(),
|
|
@@ -56,6 +57,34 @@ vi.mock('@/hooks/useConfig', () => ({
|
|
|
56
57
|
describe('ModelConfig', () => {
|
|
57
58
|
beforeEach(() => {
|
|
58
59
|
mocks.mutate.mockReset();
|
|
60
|
+
setLanguage('en');
|
|
61
|
+
mocks.configQuery.data = {
|
|
62
|
+
agents: {
|
|
63
|
+
defaults: {
|
|
64
|
+
model: 'openai/gpt-5.2',
|
|
65
|
+
workspace: '~/old-workspace'
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
providers: {
|
|
69
|
+
openai: {
|
|
70
|
+
enabled: true,
|
|
71
|
+
apiKeySet: true,
|
|
72
|
+
models: ['gpt-5.2']
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
mocks.metaQuery.data = {
|
|
77
|
+
providers: [
|
|
78
|
+
{
|
|
79
|
+
name: 'openai',
|
|
80
|
+
displayName: 'OpenAI',
|
|
81
|
+
modelPrefix: 'openai',
|
|
82
|
+
defaultModels: ['openai/gpt-5.2'],
|
|
83
|
+
keywords: [],
|
|
84
|
+
envKey: 'OPENAI_API_KEY'
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
};
|
|
59
88
|
});
|
|
60
89
|
|
|
61
90
|
it('submits the workspace together with the selected model', async () => {
|
|
@@ -75,4 +104,36 @@ describe('ModelConfig', () => {
|
|
|
75
104
|
});
|
|
76
105
|
});
|
|
77
106
|
});
|
|
107
|
+
|
|
108
|
+
it('shows a clear empty state and still allows manual model input when no providers are configured', async () => {
|
|
109
|
+
const user = userEvent.setup();
|
|
110
|
+
mocks.configQuery.data = {
|
|
111
|
+
agents: {
|
|
112
|
+
defaults: {
|
|
113
|
+
model: '',
|
|
114
|
+
workspace: '~/workspace'
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
providers: {}
|
|
118
|
+
} as typeof mocks.configQuery.data;
|
|
119
|
+
mocks.metaQuery.data = {
|
|
120
|
+
providers: []
|
|
121
|
+
} as typeof mocks.metaQuery.data;
|
|
122
|
+
|
|
123
|
+
render(<ModelConfig />);
|
|
124
|
+
|
|
125
|
+
expect(await screen.findByText('No providers configured')).toBeTruthy();
|
|
126
|
+
expect(screen.getByText('Add an AI provider to start using the platform.')).toBeTruthy();
|
|
127
|
+
|
|
128
|
+
const modelInput = screen.getByPlaceholderText('provider/model');
|
|
129
|
+
await user.type(modelInput, 'openai/gpt-5.1');
|
|
130
|
+
await user.click(screen.getByRole('button', { name: /save/i }));
|
|
131
|
+
|
|
132
|
+
await waitFor(() => {
|
|
133
|
+
expect(mocks.mutate).toHaveBeenCalledWith({
|
|
134
|
+
model: 'openai/gpt-5.1',
|
|
135
|
+
workspace: '~/workspace'
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
78
139
|
});
|
|
@@ -3,30 +3,25 @@ import { Card } from '@/components/ui/card';
|
|
|
3
3
|
import { Input } from '@/components/ui/input';
|
|
4
4
|
import { Label } from '@/components/ui/label';
|
|
5
5
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
6
|
-
import {
|
|
7
|
-
import { SearchableModelInput } from '@/components/common/SearchableModelInput';
|
|
6
|
+
import { ProviderScopedModelInput } from '@/components/common/ProviderScopedModelInput';
|
|
8
7
|
import { useConfig, useConfigMeta, useConfigSchema, useUpdateModel } from '@/hooks/useConfig';
|
|
9
8
|
import { hintForPath } from '@/lib/config-hints';
|
|
10
9
|
import { t } from '@/lib/i18n';
|
|
11
|
-
import {
|
|
12
|
-
buildProviderModelCatalog,
|
|
13
|
-
composeProviderModel,
|
|
14
|
-
findProviderByModel,
|
|
15
|
-
toProviderLocalModel
|
|
16
|
-
} from '@/lib/provider-models';
|
|
10
|
+
import { buildProviderModelCatalog } from '@/lib/provider-models';
|
|
17
11
|
import { PageLayout, PageHeader } from '@/components/layout/page-layout';
|
|
18
12
|
import { DOCS_DEFAULT_BASE_URL } from '@/components/doc-browser/DocBrowserContext';
|
|
19
13
|
import { BookOpen, Folder, Loader2, Sparkles } from 'lucide-react';
|
|
20
14
|
import { useEffect, useMemo, useState } from 'react';
|
|
21
15
|
|
|
16
|
+
const DEFAULT_MODEL_INPUT_PLACEHOLDER = 'provider/model';
|
|
17
|
+
|
|
22
18
|
export function ModelConfig() {
|
|
23
19
|
const { data: config, isLoading } = useConfig();
|
|
24
20
|
const { data: meta } = useConfigMeta();
|
|
25
21
|
const { data: schema } = useConfigSchema();
|
|
26
22
|
const updateModel = useUpdateModel();
|
|
27
23
|
|
|
28
|
-
const [
|
|
29
|
-
const [modelId, setModelId] = useState('');
|
|
24
|
+
const [model, setModel] = useState('');
|
|
30
25
|
const [workspace, setWorkspace] = useState('');
|
|
31
26
|
const uiHints = schema?.uiHints;
|
|
32
27
|
const modelHint = hintForPath('agents.defaults.model', uiHints);
|
|
@@ -37,66 +32,20 @@ export function ModelConfig() {
|
|
|
37
32
|
[config, meta]
|
|
38
33
|
);
|
|
39
34
|
|
|
40
|
-
const providerMap = useMemo(() => new Map(providerCatalog.map((provider) => [provider.name, provider])), [providerCatalog]);
|
|
41
|
-
const selectedProvider = providerMap.get(providerName);
|
|
42
|
-
const selectedProviderName = selectedProvider?.name ?? '';
|
|
43
|
-
const selectedProviderAliases = useMemo(() => selectedProvider?.aliases ?? [], [selectedProvider]);
|
|
44
|
-
const selectedProviderModels = useMemo(() => selectedProvider?.models ?? [], [selectedProvider]);
|
|
45
|
-
|
|
46
35
|
useEffect(() => {
|
|
47
36
|
if (!config?.agents?.defaults) {
|
|
48
37
|
return;
|
|
49
38
|
}
|
|
50
|
-
|
|
51
|
-
const matchedProvider = findProviderByModel(currentModel, providerCatalog);
|
|
52
|
-
const effectiveProvider = matchedProvider ?? '';
|
|
53
|
-
const aliases = providerMap.get(effectiveProvider)?.aliases ?? [];
|
|
54
|
-
setProviderName(effectiveProvider);
|
|
55
|
-
setModelId(effectiveProvider ? toProviderLocalModel(currentModel, aliases) : '');
|
|
39
|
+
setModel((config.agents.defaults.model || '').trim());
|
|
56
40
|
setWorkspace(config.agents.defaults.workspace || '');
|
|
57
|
-
}, [config
|
|
58
|
-
|
|
59
|
-
const modelOptions = useMemo(() => {
|
|
60
|
-
const deduped = new Set<string>();
|
|
61
|
-
for (const modelName of selectedProviderModels) {
|
|
62
|
-
const trimmed = modelName.trim();
|
|
63
|
-
if (trimmed) {
|
|
64
|
-
deduped.add(trimmed);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return [...deduped];
|
|
68
|
-
}, [selectedProviderModels]);
|
|
69
|
-
|
|
70
|
-
const composedModel = useMemo(() => {
|
|
71
|
-
if (!selectedProvider) {
|
|
72
|
-
return '';
|
|
73
|
-
}
|
|
74
|
-
const normalizedModelId = toProviderLocalModel(modelId, selectedProviderAliases);
|
|
75
|
-
if (!normalizedModelId) {
|
|
76
|
-
return '';
|
|
77
|
-
}
|
|
78
|
-
return composeProviderModel(selectedProvider.prefix, normalizedModelId);
|
|
79
|
-
}, [modelId, selectedProvider, selectedProviderAliases]);
|
|
41
|
+
}, [config]);
|
|
80
42
|
|
|
81
43
|
const modelHelpText = t('modelIdentifierHelp') || modelHint?.help || '';
|
|
82
44
|
|
|
83
|
-
const handleProviderChange = (nextProvider: string) => {
|
|
84
|
-
setProviderName(nextProvider);
|
|
85
|
-
setModelId('');
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const handleModelChange = (nextModelId: string) => {
|
|
89
|
-
if (!selectedProvider) {
|
|
90
|
-
setModelId('');
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
setModelId(toProviderLocalModel(nextModelId, selectedProviderAliases));
|
|
94
|
-
};
|
|
95
|
-
|
|
96
45
|
const handleSubmit = (e: React.FormEvent) => {
|
|
97
46
|
e.preventDefault();
|
|
98
47
|
updateModel.mutate({
|
|
99
|
-
model
|
|
48
|
+
model,
|
|
100
49
|
workspace
|
|
101
50
|
});
|
|
102
51
|
};
|
|
@@ -146,38 +95,14 @@ export function ModelConfig() {
|
|
|
146
95
|
<Label htmlFor="model" className="text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
|
147
96
|
{modelHint?.label ?? 'Model Name'}
|
|
148
97
|
</Label>
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
{providerCatalog.map((provider) => (
|
|
157
|
-
<SelectItem key={provider.name} value={provider.name}>
|
|
158
|
-
{provider.displayName}
|
|
159
|
-
</SelectItem>
|
|
160
|
-
))}
|
|
161
|
-
</SelectContent>
|
|
162
|
-
</Select>
|
|
163
|
-
</div>
|
|
164
|
-
<span className="hidden h-10 items-center justify-center leading-none text-lg font-semibold text-gray-300 sm:inline-flex">/</span>
|
|
165
|
-
<SearchableModelInput
|
|
166
|
-
key={selectedProviderName}
|
|
167
|
-
id="model"
|
|
168
|
-
value={modelId}
|
|
169
|
-
onChange={handleModelChange}
|
|
170
|
-
options={modelOptions}
|
|
171
|
-
disabled={!selectedProviderName}
|
|
172
|
-
placeholder={modelHint?.placeholder ?? 'gpt-5.1'}
|
|
173
|
-
className="sm:flex-1"
|
|
174
|
-
inputClassName="h-10 rounded-xl"
|
|
175
|
-
emptyText={t('modelPickerNoOptions')}
|
|
176
|
-
createText={t('modelPickerUseCustom')}
|
|
177
|
-
/>
|
|
178
|
-
</div>
|
|
98
|
+
<ProviderScopedModelInput
|
|
99
|
+
id="model"
|
|
100
|
+
value={model}
|
|
101
|
+
onChange={setModel}
|
|
102
|
+
providerCatalog={providerCatalog}
|
|
103
|
+
modelPlaceholder={modelHint?.placeholder ?? DEFAULT_MODEL_INPUT_PLACEHOLDER}
|
|
104
|
+
/>
|
|
179
105
|
<p className="text-xs text-gray-400">{modelHelpText}</p>
|
|
180
|
-
<p className="text-xs text-gray-500">{t('modelInputCustomHint')}</p>
|
|
181
106
|
<a
|
|
182
107
|
href={`${DOCS_DEFAULT_BASE_URL}/guide/model-selection`}
|
|
183
108
|
className="inline-flex items-center gap-1.5 text-xs text-primary hover:text-primary-hover transition-colors"
|
|
@@ -245,8 +245,8 @@ export function RuntimeConfig() {
|
|
|
245
245
|
placeholder={t('modelOverridePlaceholder')}
|
|
246
246
|
/>
|
|
247
247
|
<Input
|
|
248
|
-
value={agent.engine ?? ''}
|
|
249
|
-
onChange={(event) => updateAgent(index, {
|
|
248
|
+
value={agent.runtime ?? agent.engine ?? ''}
|
|
249
|
+
onChange={(event) => updateAgent(index, { runtime: event.target.value })}
|
|
250
250
|
placeholder={agentEngineHint?.label ?? t('engineOverridePlaceholder')}
|
|
251
251
|
/>
|
|
252
252
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { SearchConfig } from '@/components/config/SearchConfig';
|
|
5
|
+
import { setLanguage } from '@/lib/i18n';
|
|
6
|
+
|
|
7
|
+
const mocks = vi.hoisted(() => ({
|
|
8
|
+
mutate: vi.fn(),
|
|
9
|
+
useConfigData: {
|
|
10
|
+
data: {
|
|
11
|
+
search: {
|
|
12
|
+
provider: 'tavily',
|
|
13
|
+
enabledProviders: ['bocha', 'tavily'],
|
|
14
|
+
defaults: {
|
|
15
|
+
maxResults: 8
|
|
16
|
+
},
|
|
17
|
+
providers: {
|
|
18
|
+
bocha: {
|
|
19
|
+
enabled: true,
|
|
20
|
+
apiKeySet: false,
|
|
21
|
+
baseUrl: 'https://api.bocha.cn/v1/web-search',
|
|
22
|
+
docsUrl: 'https://open.bocha.cn',
|
|
23
|
+
summary: true,
|
|
24
|
+
freshness: 'noLimit'
|
|
25
|
+
},
|
|
26
|
+
tavily: {
|
|
27
|
+
enabled: true,
|
|
28
|
+
apiKeySet: true,
|
|
29
|
+
apiKeyMasked: 'tv****1234',
|
|
30
|
+
baseUrl: 'https://api.tavily.com/search',
|
|
31
|
+
searchDepth: 'advanced',
|
|
32
|
+
includeAnswer: true
|
|
33
|
+
},
|
|
34
|
+
brave: {
|
|
35
|
+
enabled: false,
|
|
36
|
+
apiKeySet: false,
|
|
37
|
+
baseUrl: 'https://api.search.brave.com/res/v1/web/search'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
useConfigMetaData: {
|
|
44
|
+
data: {
|
|
45
|
+
search: [
|
|
46
|
+
{
|
|
47
|
+
name: 'bocha',
|
|
48
|
+
displayName: 'Bocha Search',
|
|
49
|
+
description: 'China-friendly web search with AI-ready summaries.',
|
|
50
|
+
docsUrl: 'https://open.bocha.cn',
|
|
51
|
+
isDefault: true,
|
|
52
|
+
supportsSummary: true
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'tavily',
|
|
56
|
+
displayName: 'Tavily Search',
|
|
57
|
+
description: 'Research-focused web search with optional synthesized answers.',
|
|
58
|
+
docsUrl: 'https://docs.tavily.com/documentation/api-reference/endpoint/search',
|
|
59
|
+
supportsSummary: true
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'brave',
|
|
63
|
+
displayName: 'Brave Search',
|
|
64
|
+
description: 'Brave web search API kept as an optional provider.',
|
|
65
|
+
supportsSummary: false
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
vi.mock('@/hooks/useConfig', () => ({
|
|
73
|
+
useConfig: () => mocks.useConfigData,
|
|
74
|
+
useConfigMeta: () => mocks.useConfigMetaData,
|
|
75
|
+
useUpdateSearch: () => ({
|
|
76
|
+
mutate: mocks.mutate,
|
|
77
|
+
isPending: false
|
|
78
|
+
})
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
describe('SearchConfig', () => {
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
setLanguage('zh');
|
|
84
|
+
mocks.mutate.mockReset();
|
|
85
|
+
if (!HTMLElement.prototype.hasPointerCapture) {
|
|
86
|
+
HTMLElement.prototype.hasPointerCapture = () => false;
|
|
87
|
+
}
|
|
88
|
+
if (!HTMLElement.prototype.setPointerCapture) {
|
|
89
|
+
HTMLElement.prototype.setPointerCapture = () => {};
|
|
90
|
+
}
|
|
91
|
+
if (!HTMLElement.prototype.releasePointerCapture) {
|
|
92
|
+
HTMLElement.prototype.releasePointerCapture = () => {};
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('renders Tavily-specific controls and submits Tavily config through updateSearch', async () => {
|
|
97
|
+
const user = userEvent.setup();
|
|
98
|
+
|
|
99
|
+
render(<SearchConfig />);
|
|
100
|
+
|
|
101
|
+
expect(screen.getByRole('heading', { name: 'Tavily Search' })).toBeTruthy();
|
|
102
|
+
expect(screen.getByText('搜索深度')).toBeTruthy();
|
|
103
|
+
expect(screen.getByText('包含回答')).toBeTruthy();
|
|
104
|
+
expect(screen.getByDisplayValue('https://api.tavily.com/search')).toBeTruthy();
|
|
105
|
+
expect(screen.queryByText('结果摘要')).toBeNull();
|
|
106
|
+
|
|
107
|
+
const searchDepthSection = screen.getByText('搜索深度').parentElement;
|
|
108
|
+
const includeAnswerSection = screen.getByText('包含回答').parentElement;
|
|
109
|
+
const searchDepthTrigger = searchDepthSection?.querySelector('[role="combobox"]');
|
|
110
|
+
const includeAnswerTrigger = includeAnswerSection?.querySelector('[role="combobox"]');
|
|
111
|
+
|
|
112
|
+
expect(searchDepthTrigger).toBeTruthy();
|
|
113
|
+
expect(includeAnswerTrigger).toBeTruthy();
|
|
114
|
+
|
|
115
|
+
await user.click(searchDepthTrigger as HTMLElement);
|
|
116
|
+
await user.click(screen.getByRole('option', { name: '高级' }));
|
|
117
|
+
await user.click(includeAnswerTrigger as HTMLElement);
|
|
118
|
+
await user.click(screen.getByRole('option', { name: '启用' }));
|
|
119
|
+
|
|
120
|
+
await user.click(screen.getByRole('button', { name: '保存变更' }));
|
|
121
|
+
|
|
122
|
+
expect(mocks.mutate).toHaveBeenCalledWith({
|
|
123
|
+
data: {
|
|
124
|
+
provider: 'tavily',
|
|
125
|
+
enabledProviders: ['bocha', 'tavily'],
|
|
126
|
+
defaults: {
|
|
127
|
+
maxResults: 8
|
|
128
|
+
},
|
|
129
|
+
providers: {
|
|
130
|
+
bocha: {
|
|
131
|
+
apiKey: undefined,
|
|
132
|
+
baseUrl: 'https://api.bocha.cn/v1/web-search',
|
|
133
|
+
summary: true,
|
|
134
|
+
freshness: 'noLimit'
|
|
135
|
+
},
|
|
136
|
+
tavily: {
|
|
137
|
+
apiKey: undefined,
|
|
138
|
+
baseUrl: 'https://api.tavily.com/search',
|
|
139
|
+
searchDepth: 'advanced',
|
|
140
|
+
includeAnswer: true
|
|
141
|
+
},
|
|
142
|
+
brave: {
|
|
143
|
+
apiKey: undefined,
|
|
144
|
+
baseUrl: 'https://api.search.brave.com/res/v1/web/search'
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|