@nextclaw/ui 0.12.1 → 0.12.3
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 +44 -0
- package/dist/assets/ChannelsList-DZWam3Ob.js +8 -0
- package/dist/assets/ChatPage-YBL7iJ1X.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-BlIeNn3x.js → MarketplacePage-2tWWgwAb.js} +1 -1
- package/dist/assets/MarketplacePage-BorWJftJ.js +1 -0
- package/dist/assets/{McpMarketplacePage-mz2_IX1O.js → McpMarketplacePage-N-fB4HID.js} +1 -1
- package/dist/assets/ModelConfig-DvsBTUiE.js +1 -0
- package/dist/assets/ProviderScopedModelInput-D9woCARc.js +1 -0
- package/dist/assets/{ProvidersList-B0RCb_Vg.js → ProvidersList-D-qPGgC4.js} +1 -1
- package/dist/assets/{RemoteAccessPage-CcfQjLtx.js → RemoteAccessPage-COnjm8_x.js} +1 -1
- package/dist/assets/RuntimeConfig-BHpqcaHm.js +1 -0
- package/dist/assets/SearchConfig-DIT6M65Q.js +1 -0
- package/dist/assets/{SecretsConfig-DbiS3txa.js → SecretsConfig-Cefg1LFJ.js} +1 -1
- package/dist/assets/{SessionsConfig-CbIOcAp8.js → SessionsConfig-BZnmVTIu.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-CpxuJa9o.css +1 -0
- package/dist/assets/{index-DqSv8Azv.js → index-DHmCjcxq.js} +3 -3
- 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-DEgOD4VX.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-CuQqiPx7.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 +4 -4
- 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 +148 -1
- package/src/components/agents/AgentsPage.tsx +114 -115
- package/src/components/chat/ChatConversationPanel.test.tsx +69 -3
- package/src/components/chat/ChatConversationPanel.tsx +24 -3
- package/src/components/chat/managers/chat-session-list.manager.test.ts +34 -3
- package/src/components/chat/managers/chat-session-list.manager.ts +17 -4
- package/src/components/chat/ncp/NcpChatPage.tsx +1 -0
- package/src/components/chat/ncp/ncp-chat.presenter.ts +5 -1
- package/src/components/chat/ncp/ncp-session-adapter.ts +6 -1
- package/src/components/chat/stores/chat-session-list.store.ts +6 -1
- package/src/components/chat/useChatSessionTypeState.ts +10 -2
- 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
|
@@ -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
|
+
});
|
|
@@ -9,7 +9,7 @@ import { useConfig, useConfigMeta, useUpdateSearch } from '@/hooks/useConfig';
|
|
|
9
9
|
import { t } from '@/lib/i18n';
|
|
10
10
|
import { cn } from '@/lib/utils';
|
|
11
11
|
import { CONFIG_DETAIL_CARD_CLASS, CONFIG_SIDEBAR_CARD_CLASS, CONFIG_SPLIT_GRID_CLASS } from './config-layout';
|
|
12
|
-
import type { SearchConfigUpdate, SearchProviderName } from '@/api/types';
|
|
12
|
+
import type { SearchConfigUpdate, SearchProviderName, TavilySearchDepthValue } from '@/api/types';
|
|
13
13
|
|
|
14
14
|
const FRESHNESS_OPTIONS = [
|
|
15
15
|
{ value: 'noLimit', label: 'searchFreshnessNoLimit' },
|
|
@@ -19,11 +19,219 @@ const FRESHNESS_OPTIONS = [
|
|
|
19
19
|
{ value: 'oneYear', label: 'searchFreshnessOneYear' }
|
|
20
20
|
] as const;
|
|
21
21
|
|
|
22
|
+
const SEARCH_DEPTH_OPTIONS = [
|
|
23
|
+
{ value: 'basic', label: 'searchDepthBasic' },
|
|
24
|
+
{ value: 'advanced', label: 'searchDepthAdvanced' }
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
const SEARCH_PROVIDER_DESCRIPTION_KEYS: Record<SearchProviderName, string> = {
|
|
28
|
+
bocha: 'searchProviderBochaDescription',
|
|
29
|
+
tavily: 'searchProviderTavilyDescription',
|
|
30
|
+
brave: 'searchProviderBraveDescription'
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type BochaProviderFieldsProps = {
|
|
34
|
+
apiKey: string;
|
|
35
|
+
apiKeyMasked?: string;
|
|
36
|
+
baseUrl: string;
|
|
37
|
+
summary: boolean;
|
|
38
|
+
freshness: string;
|
|
39
|
+
docsUrl?: string;
|
|
40
|
+
onApiKeyChange: (value: string) => void;
|
|
41
|
+
onBaseUrlChange: (value: string) => void;
|
|
42
|
+
onSummaryChange: (value: boolean) => void;
|
|
43
|
+
onFreshnessChange: (value: string) => void;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function BochaProviderFields({
|
|
47
|
+
apiKey,
|
|
48
|
+
apiKeyMasked,
|
|
49
|
+
baseUrl,
|
|
50
|
+
summary,
|
|
51
|
+
freshness,
|
|
52
|
+
docsUrl,
|
|
53
|
+
onApiKeyChange,
|
|
54
|
+
onBaseUrlChange,
|
|
55
|
+
onSummaryChange,
|
|
56
|
+
onFreshnessChange
|
|
57
|
+
}: BochaProviderFieldsProps) {
|
|
58
|
+
return (
|
|
59
|
+
<div className="space-y-4">
|
|
60
|
+
<div className="space-y-2">
|
|
61
|
+
<Label>{t('apiKey')}</Label>
|
|
62
|
+
<Input
|
|
63
|
+
type="password"
|
|
64
|
+
value={apiKey}
|
|
65
|
+
onChange={(event) => onApiKeyChange(event.target.value)}
|
|
66
|
+
placeholder={apiKeyMasked || t('enterApiKey')}
|
|
67
|
+
className="rounded-xl"
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
<div className="space-y-2">
|
|
71
|
+
<Label>{t('searchProviderBaseUrl')}</Label>
|
|
72
|
+
<Input value={baseUrl} onChange={(event) => onBaseUrlChange(event.target.value)} className="rounded-xl" />
|
|
73
|
+
</div>
|
|
74
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
75
|
+
<div className="space-y-2">
|
|
76
|
+
<Label>{t('searchProviderSummary')}</Label>
|
|
77
|
+
<Select value={summary ? 'true' : 'false'} onValueChange={(value) => onSummaryChange(value === 'true')}>
|
|
78
|
+
<SelectTrigger className="rounded-xl">
|
|
79
|
+
<SelectValue />
|
|
80
|
+
</SelectTrigger>
|
|
81
|
+
<SelectContent>
|
|
82
|
+
<SelectItem value="true">{t('enabled')}</SelectItem>
|
|
83
|
+
<SelectItem value="false">{t('disabled')}</SelectItem>
|
|
84
|
+
</SelectContent>
|
|
85
|
+
</Select>
|
|
86
|
+
</div>
|
|
87
|
+
<div className="space-y-2">
|
|
88
|
+
<Label>{t('searchProviderFreshness')}</Label>
|
|
89
|
+
<Select value={freshness} onValueChange={onFreshnessChange}>
|
|
90
|
+
<SelectTrigger className="rounded-xl">
|
|
91
|
+
<SelectValue />
|
|
92
|
+
</SelectTrigger>
|
|
93
|
+
<SelectContent>
|
|
94
|
+
{FRESHNESS_OPTIONS.map((option) => (
|
|
95
|
+
<SelectItem key={option.value} value={option.value}>{t(option.label)}</SelectItem>
|
|
96
|
+
))}
|
|
97
|
+
</SelectContent>
|
|
98
|
+
</Select>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="space-y-2">
|
|
102
|
+
<a href={docsUrl ?? 'https://open.bocha.cn'} target="_blank" rel="noreferrer">
|
|
103
|
+
<Button type="button" variant="outline" className="rounded-xl">
|
|
104
|
+
<ExternalLink className="mr-2 h-4 w-4" />
|
|
105
|
+
{t('searchProviderOpenDocs')}
|
|
106
|
+
</Button>
|
|
107
|
+
</a>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type TavilyProviderFieldsProps = {
|
|
114
|
+
apiKey: string;
|
|
115
|
+
apiKeyMasked?: string;
|
|
116
|
+
baseUrl: string;
|
|
117
|
+
searchDepth: TavilySearchDepthValue;
|
|
118
|
+
includeAnswer: boolean;
|
|
119
|
+
docsUrl?: string;
|
|
120
|
+
onApiKeyChange: (value: string) => void;
|
|
121
|
+
onBaseUrlChange: (value: string) => void;
|
|
122
|
+
onSearchDepthChange: (value: TavilySearchDepthValue) => void;
|
|
123
|
+
onIncludeAnswerChange: (value: boolean) => void;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
function TavilyProviderFields({
|
|
127
|
+
apiKey,
|
|
128
|
+
apiKeyMasked,
|
|
129
|
+
baseUrl,
|
|
130
|
+
searchDepth,
|
|
131
|
+
includeAnswer,
|
|
132
|
+
docsUrl,
|
|
133
|
+
onApiKeyChange,
|
|
134
|
+
onBaseUrlChange,
|
|
135
|
+
onSearchDepthChange,
|
|
136
|
+
onIncludeAnswerChange
|
|
137
|
+
}: TavilyProviderFieldsProps) {
|
|
138
|
+
return (
|
|
139
|
+
<div className="space-y-4">
|
|
140
|
+
<div className="space-y-2">
|
|
141
|
+
<Label>{t('apiKey')}</Label>
|
|
142
|
+
<Input
|
|
143
|
+
type="password"
|
|
144
|
+
value={apiKey}
|
|
145
|
+
onChange={(event) => onApiKeyChange(event.target.value)}
|
|
146
|
+
placeholder={apiKeyMasked || t('enterApiKey')}
|
|
147
|
+
className="rounded-xl"
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="space-y-2">
|
|
151
|
+
<Label>{t('searchProviderBaseUrl')}</Label>
|
|
152
|
+
<Input value={baseUrl} onChange={(event) => onBaseUrlChange(event.target.value)} className="rounded-xl" />
|
|
153
|
+
</div>
|
|
154
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
155
|
+
<div className="space-y-2">
|
|
156
|
+
<Label>{t('searchProviderSearchDepth')}</Label>
|
|
157
|
+
<Select value={searchDepth} onValueChange={(value) => onSearchDepthChange(value as TavilySearchDepthValue)}>
|
|
158
|
+
<SelectTrigger className="rounded-xl">
|
|
159
|
+
<SelectValue />
|
|
160
|
+
</SelectTrigger>
|
|
161
|
+
<SelectContent>
|
|
162
|
+
{SEARCH_DEPTH_OPTIONS.map((option) => (
|
|
163
|
+
<SelectItem key={option.value} value={option.value}>{t(option.label)}</SelectItem>
|
|
164
|
+
))}
|
|
165
|
+
</SelectContent>
|
|
166
|
+
</Select>
|
|
167
|
+
</div>
|
|
168
|
+
<div className="space-y-2">
|
|
169
|
+
<Label>{t('searchProviderIncludeAnswer')}</Label>
|
|
170
|
+
<Select value={includeAnswer ? 'true' : 'false'} onValueChange={(value) => onIncludeAnswerChange(value === 'true')}>
|
|
171
|
+
<SelectTrigger className="rounded-xl">
|
|
172
|
+
<SelectValue />
|
|
173
|
+
</SelectTrigger>
|
|
174
|
+
<SelectContent>
|
|
175
|
+
<SelectItem value="true">{t('enabled')}</SelectItem>
|
|
176
|
+
<SelectItem value="false">{t('disabled')}</SelectItem>
|
|
177
|
+
</SelectContent>
|
|
178
|
+
</Select>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
{docsUrl ? (
|
|
182
|
+
<div className="space-y-2">
|
|
183
|
+
<a href={docsUrl} target="_blank" rel="noreferrer">
|
|
184
|
+
<Button type="button" variant="outline" className="rounded-xl">
|
|
185
|
+
<ExternalLink className="mr-2 h-4 w-4" />
|
|
186
|
+
{t('searchProviderOpenDocs')}
|
|
187
|
+
</Button>
|
|
188
|
+
</a>
|
|
189
|
+
</div>
|
|
190
|
+
) : null}
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
type BraveProviderFieldsProps = {
|
|
196
|
+
apiKey: string;
|
|
197
|
+
apiKeyMasked?: string;
|
|
198
|
+
baseUrl: string;
|
|
199
|
+
onApiKeyChange: (value: string) => void;
|
|
200
|
+
onBaseUrlChange: (value: string) => void;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
function BraveProviderFields({
|
|
204
|
+
apiKey,
|
|
205
|
+
apiKeyMasked,
|
|
206
|
+
baseUrl,
|
|
207
|
+
onApiKeyChange,
|
|
208
|
+
onBaseUrlChange
|
|
209
|
+
}: BraveProviderFieldsProps) {
|
|
210
|
+
return (
|
|
211
|
+
<div className="space-y-4">
|
|
212
|
+
<div className="space-y-2">
|
|
213
|
+
<Label>{t('apiKey')}</Label>
|
|
214
|
+
<Input
|
|
215
|
+
type="password"
|
|
216
|
+
value={apiKey}
|
|
217
|
+
onChange={(event) => onApiKeyChange(event.target.value)}
|
|
218
|
+
placeholder={apiKeyMasked || t('enterApiKey')}
|
|
219
|
+
className="rounded-xl"
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
<div className="space-y-2">
|
|
223
|
+
<Label>{t('searchProviderBaseUrl')}</Label>
|
|
224
|
+
<Input value={baseUrl} onChange={(event) => onBaseUrlChange(event.target.value)} className="rounded-xl" />
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
22
230
|
export function SearchConfig() {
|
|
23
231
|
const { data: config } = useConfig();
|
|
24
232
|
const { data: meta } = useConfigMeta();
|
|
25
233
|
const updateSearch = useUpdateSearch();
|
|
26
|
-
const providers = meta?.search ?? [];
|
|
234
|
+
const providers = useMemo(() => meta?.search ?? [], [meta]);
|
|
27
235
|
const search = config?.search;
|
|
28
236
|
|
|
29
237
|
const [selectedProvider, setSelectedProvider] = useState<SearchProviderName>('bocha');
|
|
@@ -34,6 +242,10 @@ export function SearchConfig() {
|
|
|
34
242
|
const [bochaBaseUrl, setBochaBaseUrl] = useState('https://api.bocha.cn/v1/web-search');
|
|
35
243
|
const [bochaSummary, setBochaSummary] = useState(true);
|
|
36
244
|
const [bochaFreshness, setBochaFreshness] = useState('noLimit');
|
|
245
|
+
const [tavilyApiKey, setTavilyApiKey] = useState('');
|
|
246
|
+
const [tavilyBaseUrl, setTavilyBaseUrl] = useState('https://api.tavily.com/search');
|
|
247
|
+
const [tavilySearchDepth, setTavilySearchDepth] = useState<TavilySearchDepthValue>('basic');
|
|
248
|
+
const [tavilyIncludeAnswer, setTavilyIncludeAnswer] = useState(false);
|
|
37
249
|
const [braveApiKey, setBraveApiKey] = useState('');
|
|
38
250
|
const [braveBaseUrl, setBraveBaseUrl] = useState('https://api.search.brave.com/res/v1/web/search');
|
|
39
251
|
|
|
@@ -48,6 +260,9 @@ export function SearchConfig() {
|
|
|
48
260
|
setBochaBaseUrl(search.providers.bocha.baseUrl);
|
|
49
261
|
setBochaSummary(Boolean(search.providers.bocha.summary));
|
|
50
262
|
setBochaFreshness(search.providers.bocha.freshness ?? 'noLimit');
|
|
263
|
+
setTavilyBaseUrl(search.providers.tavily.baseUrl);
|
|
264
|
+
setTavilySearchDepth(search.providers.tavily.searchDepth ?? 'basic');
|
|
265
|
+
setTavilyIncludeAnswer(Boolean(search.providers.tavily.includeAnswer));
|
|
51
266
|
setBraveBaseUrl(search.providers.brave.baseUrl);
|
|
52
267
|
}, [search]);
|
|
53
268
|
|
|
@@ -57,7 +272,7 @@ export function SearchConfig() {
|
|
|
57
272
|
);
|
|
58
273
|
const selectedView = search?.providers[selectedProvider];
|
|
59
274
|
const selectedEnabled = enabledProviders.includes(selectedProvider);
|
|
60
|
-
const
|
|
275
|
+
const selectedDocsUrl = selectedView?.docsUrl ?? selectedMeta?.docsUrl;
|
|
61
276
|
const activationButtonLabel = selectedEnabled
|
|
62
277
|
? t('searchProviderDeactivate')
|
|
63
278
|
: t('searchProviderActivate');
|
|
@@ -78,6 +293,12 @@ export function SearchConfig() {
|
|
|
78
293
|
summary: bochaSummary,
|
|
79
294
|
freshness: bochaFreshness
|
|
80
295
|
},
|
|
296
|
+
tavily: {
|
|
297
|
+
apiKey: tavilyApiKey || undefined,
|
|
298
|
+
baseUrl: tavilyBaseUrl,
|
|
299
|
+
searchDepth: tavilySearchDepth,
|
|
300
|
+
includeAnswer: tavilyIncludeAnswer
|
|
301
|
+
},
|
|
81
302
|
brave: {
|
|
82
303
|
apiKey: braveApiKey || undefined,
|
|
83
304
|
baseUrl: braveBaseUrl
|
|
@@ -140,7 +361,7 @@ export function SearchConfig() {
|
|
|
140
361
|
<div className="min-w-0">
|
|
141
362
|
<p className="truncate text-sm font-semibold text-gray-900">{provider.displayName}</p>
|
|
142
363
|
<p className="line-clamp-2 text-[11px] text-gray-500">
|
|
143
|
-
{provider.name
|
|
364
|
+
{t(SEARCH_PROVIDER_DESCRIPTION_KEYS[provider.name])}
|
|
144
365
|
</p>
|
|
145
366
|
</div>
|
|
146
367
|
<div className="flex flex-col items-end gap-1">
|
|
@@ -212,74 +433,39 @@ export function SearchConfig() {
|
|
|
212
433
|
</div>
|
|
213
434
|
|
|
214
435
|
{selectedProvider === 'bocha' ? (
|
|
215
|
-
<
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
</SelectContent>
|
|
241
|
-
</Select>
|
|
242
|
-
</div>
|
|
243
|
-
<div className="space-y-2">
|
|
244
|
-
<Label>{t('searchProviderFreshness')}</Label>
|
|
245
|
-
<Select value={bochaFreshness} onValueChange={setBochaFreshness}>
|
|
246
|
-
<SelectTrigger className="rounded-xl">
|
|
247
|
-
<SelectValue />
|
|
248
|
-
</SelectTrigger>
|
|
249
|
-
<SelectContent>
|
|
250
|
-
{FRESHNESS_OPTIONS.map((option) => (
|
|
251
|
-
<SelectItem key={option.value} value={option.value}>{t(option.label)}</SelectItem>
|
|
252
|
-
))}
|
|
253
|
-
</SelectContent>
|
|
254
|
-
</Select>
|
|
255
|
-
</div>
|
|
256
|
-
</div>
|
|
257
|
-
<div className="space-y-2">
|
|
258
|
-
<a href={bochaDocsUrl} target="_blank" rel="noreferrer">
|
|
259
|
-
<Button type="button" variant="outline" className="rounded-xl">
|
|
260
|
-
<ExternalLink className="mr-2 h-4 w-4" />
|
|
261
|
-
{t('searchProviderOpenDocs')}
|
|
262
|
-
</Button>
|
|
263
|
-
</a>
|
|
264
|
-
</div>
|
|
265
|
-
</div>
|
|
436
|
+
<BochaProviderFields
|
|
437
|
+
apiKey={bochaApiKey}
|
|
438
|
+
apiKeyMasked={search.providers.bocha.apiKeyMasked}
|
|
439
|
+
baseUrl={bochaBaseUrl}
|
|
440
|
+
summary={bochaSummary}
|
|
441
|
+
freshness={bochaFreshness}
|
|
442
|
+
docsUrl={selectedDocsUrl}
|
|
443
|
+
onApiKeyChange={setBochaApiKey}
|
|
444
|
+
onBaseUrlChange={setBochaBaseUrl}
|
|
445
|
+
onSummaryChange={setBochaSummary}
|
|
446
|
+
onFreshnessChange={setBochaFreshness}
|
|
447
|
+
/>
|
|
448
|
+
) : selectedProvider === 'tavily' ? (
|
|
449
|
+
<TavilyProviderFields
|
|
450
|
+
apiKey={tavilyApiKey}
|
|
451
|
+
apiKeyMasked={search.providers.tavily.apiKeyMasked}
|
|
452
|
+
baseUrl={tavilyBaseUrl}
|
|
453
|
+
searchDepth={tavilySearchDepth}
|
|
454
|
+
includeAnswer={tavilyIncludeAnswer}
|
|
455
|
+
docsUrl={selectedDocsUrl}
|
|
456
|
+
onApiKeyChange={setTavilyApiKey}
|
|
457
|
+
onBaseUrlChange={setTavilyBaseUrl}
|
|
458
|
+
onSearchDepthChange={setTavilySearchDepth}
|
|
459
|
+
onIncludeAnswerChange={setTavilyIncludeAnswer}
|
|
460
|
+
/>
|
|
266
461
|
) : (
|
|
267
|
-
<
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
placeholder={search.providers.brave.apiKeyMasked || t('enterApiKey')}
|
|
275
|
-
className="rounded-xl"
|
|
276
|
-
/>
|
|
277
|
-
</div>
|
|
278
|
-
<div className="space-y-2">
|
|
279
|
-
<Label>{t('searchProviderBaseUrl')}</Label>
|
|
280
|
-
<Input value={braveBaseUrl} onChange={(event) => setBraveBaseUrl(event.target.value)} className="rounded-xl" />
|
|
281
|
-
</div>
|
|
282
|
-
</div>
|
|
462
|
+
<BraveProviderFields
|
|
463
|
+
apiKey={braveApiKey}
|
|
464
|
+
apiKeyMasked={search.providers.brave.apiKeyMasked}
|
|
465
|
+
baseUrl={braveBaseUrl}
|
|
466
|
+
onApiKeyChange={setBraveApiKey}
|
|
467
|
+
onBaseUrlChange={setBraveBaseUrl}
|
|
468
|
+
/>
|
|
283
469
|
)}
|
|
284
470
|
|
|
285
471
|
<div className="flex justify-end">
|
|
@@ -6,7 +6,7 @@ export function createEmptyRuntimeAgent(): AgentProfileView {
|
|
|
6
6
|
default: false,
|
|
7
7
|
workspace: '',
|
|
8
8
|
model: '',
|
|
9
|
-
|
|
9
|
+
runtime: '',
|
|
10
10
|
contextTokens: undefined,
|
|
11
11
|
maxToolIterations: undefined
|
|
12
12
|
};
|
|
@@ -31,7 +31,7 @@ export function hydrateRuntimeAgent(agent: AgentProfileView): AgentProfileView {
|
|
|
31
31
|
avatar: agent.avatar ?? '',
|
|
32
32
|
workspace: agent.workspace ?? '',
|
|
33
33
|
model: agent.model ?? '',
|
|
34
|
-
|
|
34
|
+
runtime: agent.runtime ?? agent.engine ?? '',
|
|
35
35
|
contextTokens: agent.contextTokens,
|
|
36
36
|
maxToolIterations: agent.maxToolIterations
|
|
37
37
|
};
|
|
@@ -82,8 +82,9 @@ export function toPersistedRuntimeAgent(agent: AgentProfileView): AgentProfileVi
|
|
|
82
82
|
if (agent.model?.trim()) {
|
|
83
83
|
normalized.model = agent.model.trim();
|
|
84
84
|
}
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
const runtime = agent.runtime?.trim() ?? agent.engine?.trim();
|
|
86
|
+
if (runtime) {
|
|
87
|
+
normalized.engine = runtime;
|
|
87
88
|
}
|
|
88
89
|
if (typeof agent.contextTokens === 'number') {
|
|
89
90
|
normalized.contextTokens = Math.max(1000, agent.contextTokens);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
2
|
-
import { createAgent, deleteAgent, fetchAgents } from '@/api/agents';
|
|
2
|
+
import { createAgent, deleteAgent, fetchAgents, updateAgent } from '@/api/agents';
|
|
3
3
|
import { toast } from 'sonner';
|
|
4
4
|
import { t } from '@/lib/i18n';
|
|
5
5
|
|
|
@@ -27,6 +27,23 @@ export function useCreateAgent() {
|
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export function useUpdateAgent() {
|
|
31
|
+
const queryClient = useQueryClient();
|
|
32
|
+
|
|
33
|
+
return useMutation({
|
|
34
|
+
mutationFn: ({ agentId, data }: { agentId: string; data: Parameters<typeof updateAgent>[1] }) =>
|
|
35
|
+
updateAgent(agentId, data),
|
|
36
|
+
onSuccess: () => {
|
|
37
|
+
queryClient.invalidateQueries({ queryKey: ['agents'] });
|
|
38
|
+
queryClient.invalidateQueries({ queryKey: ['config'] });
|
|
39
|
+
toast.success(t('configSavedApplied'));
|
|
40
|
+
},
|
|
41
|
+
onError: (error: Error) => {
|
|
42
|
+
toast.error(t('configSaveFailed') + ': ' + error.message);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
export function useDeleteAgent() {
|
|
31
48
|
const queryClient = useQueryClient();
|
|
32
49
|
|
package/src/lib/i18n.agents.ts
CHANGED
|
@@ -33,7 +33,25 @@ export const AGENT_LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
33
33
|
agentsFormDescriptionPlaceholder: { zh: '角色描述,可选', en: 'Role description, optional' },
|
|
34
34
|
agentsFormAvatarPlaceholder: { zh: '头像 URL 或本地路径,可选', en: 'Avatar URL or local path, optional' },
|
|
35
35
|
agentsFormHomePlaceholder: { zh: '主目录,可选', en: 'Home Directory, optional' },
|
|
36
|
+
agentsFormRuntimePlaceholder: { zh: 'Runtime(如 native 或 codex,可选)', en: 'Runtime (e.g. native or codex, optional)' },
|
|
37
|
+
agentsRuntimeSelectPlaceholder: { zh: '选择 Runtime', en: 'Select runtime' },
|
|
38
|
+
agentsRuntimeUnavailableHelp: {
|
|
39
|
+
zh: '当前 Runtime 已不在可用列表中;建议改选一个已安装且可用的 Runtime。',
|
|
40
|
+
en: 'This runtime is no longer available in the current list. Switch to an installed and available runtime.'
|
|
41
|
+
},
|
|
36
42
|
agentsCreateAction: { zh: '创建 Agent', en: 'Create Agent' },
|
|
43
|
+
agentsEditAction: { zh: '编辑', en: 'Edit' },
|
|
44
|
+
agentsEditDialogTitle: { zh: '编辑 Agent 身份', en: 'Edit agent identity' },
|
|
45
|
+
agentsEditDialogDescription: {
|
|
46
|
+
zh: '更新名称、角色描述与头像。Agent ID 与主目录保持稳定,避免影响既有记忆、技能与会话。',
|
|
47
|
+
en: 'Update the name, role description, and avatar. The agent ID and home directory stay stable to protect existing memory, skills, and sessions.'
|
|
48
|
+
},
|
|
49
|
+
agentsEditHomeReadonly: { zh: '主目录保持不变', en: 'Home directory stays unchanged' },
|
|
50
|
+
agentsEditHomeReadonlyHint: {
|
|
51
|
+
zh: '如需迁移主目录,请走独立配置迁移流程,避免意外丢失 Agent 上下文。',
|
|
52
|
+
en: 'Use a dedicated config migration flow to move the home directory and avoid losing agent context unexpectedly.'
|
|
53
|
+
},
|
|
54
|
+
agentsEditSaveAction: { zh: '保存编辑', en: 'Save edits' },
|
|
37
55
|
agentsRemoveAction: { zh: '移除', en: 'Remove' },
|
|
38
56
|
agentsNewChat: { zh: '新建会话', en: 'New Chat' },
|
|
39
57
|
agentsLoading: { zh: '正在加载 Agent...', en: 'Loading agents...' },
|
|
@@ -55,6 +73,7 @@ export const AGENT_LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
55
73
|
zh: '专属 Agent 身份,可沉淀自己的记忆、技能与角色风格。',
|
|
56
74
|
en: 'A dedicated identity with its own memory, skills, and role style.'
|
|
57
75
|
},
|
|
76
|
+
agentsCardRuntimeLabel: { zh: 'Runtime', en: 'Runtime' },
|
|
58
77
|
agentsCardHomeLabel: { zh: '主目录', en: 'Home Directory' },
|
|
59
78
|
agentsCardAvatarLabel: { zh: 'Avatar', en: 'Avatar' },
|
|
60
79
|
agentsCardStartChat: { zh: '开始对话', en: 'Start Chat' },
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const SEARCH_LABELS: Record<string, { zh: string; en: string }> = {
|
|
2
|
+
searchPageTitle: { zh: '搜索渠道', en: 'Search Channels' },
|
|
3
|
+
searchPageDescription: { zh: '配置网页搜索提供商', en: 'Configure web search providers.' },
|
|
4
|
+
searchActiveProvider: { zh: '当前搜索提供商', en: 'Active Search Provider' },
|
|
5
|
+
searchDefaultMaxResults: { zh: '默认返回条数', en: 'Default Result Count' },
|
|
6
|
+
searchProviderSummary: { zh: '结果摘要', en: 'Result Summary' },
|
|
7
|
+
searchProviderFreshness: { zh: '时间范围', en: 'Freshness' },
|
|
8
|
+
searchProviderSearchDepth: { zh: '搜索深度', en: 'Search Depth' },
|
|
9
|
+
searchProviderIncludeAnswer: { zh: '包含回答', en: 'Include Answer' },
|
|
10
|
+
searchProviderBaseUrl: { zh: '接口地址', en: 'API Base URL' },
|
|
11
|
+
searchProviderOpenDocs: { zh: '查看文档', en: 'Open Docs' },
|
|
12
|
+
searchProviderActivate: { zh: '激活', en: 'Activate' },
|
|
13
|
+
searchProviderActivated: { zh: '已激活', en: 'Activated' },
|
|
14
|
+
searchProviderDeactivate: { zh: '取消激活', en: 'Deactivate' },
|
|
15
|
+
searchProviderBochaDescription: {
|
|
16
|
+
zh: '更适合中国大陆用户的 AI 搜索。',
|
|
17
|
+
en: 'AI-ready search that works better for mainland China users.'
|
|
18
|
+
},
|
|
19
|
+
searchProviderTavilyDescription: {
|
|
20
|
+
zh: '更适合调研和网页信息整合的搜索 provider。',
|
|
21
|
+
en: 'Research-focused search with configurable answer synthesis.'
|
|
22
|
+
},
|
|
23
|
+
searchProviderBraveDescription: {
|
|
24
|
+
zh: '保留 Brave 作为可选 provider。',
|
|
25
|
+
en: 'Keep Brave as an optional provider.'
|
|
26
|
+
},
|
|
27
|
+
searchStatusConfigured: { zh: '已配置', en: 'Configured' },
|
|
28
|
+
searchStatusNeedsSetup: { zh: '待配置', en: 'Needs Setup' },
|
|
29
|
+
searchFreshnessNoLimit: { zh: '不限', en: 'No Limit' },
|
|
30
|
+
searchFreshnessOneDay: { zh: '一天内', en: 'One Day' },
|
|
31
|
+
searchFreshnessOneWeek: { zh: '一周内', en: 'One Week' },
|
|
32
|
+
searchFreshnessOneMonth: { zh: '一个月内', en: 'One Month' },
|
|
33
|
+
searchFreshnessOneYear: { zh: '一年内', en: 'One Year' },
|
|
34
|
+
searchDepthBasic: { zh: '基础', en: 'Basic' },
|
|
35
|
+
searchDepthAdvanced: { zh: '高级', en: 'Advanced' },
|
|
36
|
+
searchNoProviderSelected: { zh: '请选择左侧搜索 provider', en: 'Select a search provider from the left.' }
|
|
37
|
+
};
|