@nextclaw/ui 0.12.0 → 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 +57 -2
- package/dist/assets/ChannelsList-uKmkpD25.js +8 -0
- package/dist/assets/ChatPage-CslhBPfT.js +43 -0
- package/dist/assets/{DocBrowser-DxdSujSc.js → DocBrowser-C7-1sXqo.js} +1 -1
- package/dist/assets/DocBrowser-DQjtSsY3.js +1 -0
- package/dist/assets/{DocBrowserContext-CQ-8jMha.js → DocBrowserContext-DN5tjUoS.js} +1 -1
- package/dist/assets/{LogoBadge-D-KQIN4U.js → LogoBadge-DDS1sU_U.js} +1 -1
- package/dist/assets/MarketplacePage-BZQW70ti.js +1 -0
- package/dist/assets/{MarketplacePage-CRNvxtvx.js → MarketplacePage-DE0QjYVv.js} +1 -1
- package/dist/assets/{McpMarketplacePage-Cu7GmCcc.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-BWbUb7-2.js → ProvidersList-1rKi3aQT.js} +1 -1
- package/dist/assets/{RemoteAccessPage-NsawrZb0.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-CgDZOd3w.js → SecretsConfig-cnAXvREZ.js} +1 -1
- package/dist/assets/{SessionsConfig-Dd-KM7F7.js → SessionsConfig-BIXiDaK2.js} +1 -1
- package/dist/assets/{book-open-FnK2xCQd.js → book-open-DvWqOode.js} +1 -1
- package/dist/assets/chat-session-display-D4bYa0b8.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-B5l0hr_u.js → chunk-JZWAC4HX-CxfKRD7X.js} +1 -1
- package/dist/assets/{config-JKmXfZ3q.js → config-BeGwf2Ao.js} +1 -1
- package/dist/assets/{createLucideIcon-o1WWhwhd.js → createLucideIcon-C7MmdIX3.js} +1 -1
- package/dist/assets/{dist-C_moWYv7.js → dist-B6VMuIQN.js} +1 -1
- package/dist/assets/{dist-DazA6Wd_.js → dist-RWNFhxvR.js} +1 -1
- package/dist/assets/{external-link-BKje3SiD.js → external-link-U86Acd1t.js} +1 -1
- package/dist/assets/{hash-DfW4DT8O.js → hash-D-OVfV3Z.js} +1 -1
- package/dist/assets/i18n-hM3v-3YG.js +1 -0
- package/dist/assets/{index-BZaB1TqM.js → index-8XNPYwJu.js} +3 -3
- package/dist/assets/index-CpxuJa9o.css +1 -0
- package/dist/assets/{label-BzDWmdOe.js → label-CHJ1ATds.js} +1 -1
- package/dist/assets/loader-circle-C8cpaL0w.js +1 -0
- package/dist/assets/{logos-CTLlde_T.js → logos-U1_qDA3U.js} +1 -1
- package/dist/assets/{page-layout-BagR3t59.js → page-layout-Z1klaUFW.js} +1 -1
- package/dist/assets/plus-CrkO1kob.js +1 -0
- package/dist/assets/{popover-5DWhNfd4.js → popover-xWbqMnIN.js} +1 -1
- package/dist/assets/{react-C3yu5yge.js → react-3YE87-lE.js} +1 -1
- package/dist/assets/{refresh-ccw-BAJf-h7w.js → refresh-ccw-JQh1lwq-.js} +1 -1
- package/dist/assets/{save-aa6z4GJL.js → save-4VRlzkii.js} +1 -1
- package/dist/assets/search-EX-Papzl.js +1 -0
- package/dist/assets/{security-config-DRDxrApx.js → security-config-CGazBahs.js} +1 -1
- package/dist/assets/{select-BHJPiJWt.js → select-DF-AUoie.js} +1 -1
- package/dist/assets/skeleton-B0mmt1vo.js +1 -0
- package/dist/assets/{status-dot-DUwsTIdv.js → status-dot-Bq_8Ojvv.js} +1 -1
- package/dist/assets/{switch-B6nCfcOB.js → switch-D7JF_RZ-.js} +1 -1
- package/dist/assets/{tabs-custom-B57SMElx.js → tabs-custom-CLksZ2bO.js} +1 -1
- package/dist/assets/{trash-2-CrjYH5ok.js → trash-2-VV8jvziy.js} +1 -1
- package/dist/assets/{useConfirmDialog-DsxnXB1B.js → useConfirmDialog-D6HxybcM.js} +1 -1
- package/dist/assets/{useMutation-oTTWXgLG.js → useMutation-DBTWPbTg.js} +1 -1
- package/dist/assets/x-B4sxJkGY.js +1 -0
- package/dist/index.html +18 -18
- package/package.json +6 -6
- package/src/api/agents.ts +9 -1
- package/src/api/types.ts +25 -4
- 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/ChatConversationPanel.test.tsx +31 -0
- package/src/components/chat/ChatConversationPanel.tsx +7 -6
- package/src/components/chat/ChatSidebar.test.tsx +41 -1
- package/src/components/chat/ChatWelcome.test.tsx +7 -2
- package/src/components/chat/ChatWelcome.tsx +38 -35
- package/src/components/chat/chat-page-runtime.test.ts +30 -0
- package/src/components/chat/chat-sidebar-session-item.tsx +6 -2
- package/src/components/chat/ncp/NcpChatPage.tsx +23 -1
- 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 +3 -24
- 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 +21 -2
- package/src/lib/i18n.search.ts +37 -0
- package/src/lib/i18n.ts +6 -28
- package/dist/assets/ChannelsList-NKNKsf1J.js +0 -8
- package/dist/assets/ChatPage-p23OnnEI.js +0 -43
- package/dist/assets/DocBrowser-C8b2uPgL.js +0 -1
- package/dist/assets/MarketplacePage-GGkEXowp.js +0 -1
- package/dist/assets/ModelConfig-CEpx9fro.js +0 -1
- package/dist/assets/RuntimeConfig-BJHBsVTd.js +0 -1
- package/dist/assets/SearchConfig-BsaX_WYy.js +0 -1
- package/dist/assets/chat-session-display-BD_AN71I.js +0 -1
- package/dist/assets/i18n-BK1w-oBy.js +0 -1
- package/dist/assets/index-DaR9igPC.css +0 -1
- package/dist/assets/loader-circle-DdZPxBUz.js +0 -1
- package/dist/assets/plus-DP2PSCPO.js +0 -1
- package/dist/assets/provider-models-DJ29qHuA.js +0 -1
- package/dist/assets/search-pD6ZwQYF.js +0 -1
- package/dist/assets/skeleton-D6kCk9Y6.js +0 -1
- package/dist/assets/x-CTIQHUuD.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,12 +73,13 @@ 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' },
|
|
61
80
|
agentsCardBuiltInTag: { zh: '系统主', en: 'Main Agent' },
|
|
62
81
|
agentsCardCustomTag: { zh: '专职', en: 'Specialist' },
|
|
63
|
-
chatDraftAgentTitle: { zh: '
|
|
64
|
-
chatDraftAgentDescription: { zh: '
|
|
82
|
+
chatDraftAgentTitle: { zh: '本次会话 Agent', en: 'Draft agent' },
|
|
83
|
+
chatDraftAgentDescription: { zh: '创建后不可切换', en: 'Locked after creation' },
|
|
65
84
|
chatDraftAgentCurrent: { zh: '当前 Agent', en: 'Current Agent' }
|
|
66
85
|
};
|