@lobehub/chat 1.141.5 → 1.141.7
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/.github/PULL_REQUEST_TEMPLATE.md +26 -0
- package/.github/workflows/e2e.yml +6 -6
- package/.github/workflows/test.yml +6 -8
- package/CHANGELOG.md +42 -0
- package/changelog/v1.json +14 -0
- package/e2e/README.md +143 -0
- package/e2e/cucumber.config.js +20 -0
- package/e2e/package.json +24 -0
- package/e2e/src/features/discover/smoke.feature +11 -0
- package/e2e/src/features/routes/core-routes.feature +43 -0
- package/e2e/src/steps/common/navigation.steps.ts +36 -0
- package/e2e/src/steps/discover/smoke.steps.ts +34 -0
- package/e2e/src/steps/hooks.ts +69 -0
- package/e2e/src/steps/routes/routes.steps.ts +41 -0
- package/e2e/src/support/webServer.ts +96 -0
- package/e2e/src/support/world.ts +76 -0
- package/e2e/tsconfig.json +19 -0
- package/package.json +6 -3
- package/packages/const/src/layoutTokens.ts +1 -1
- package/packages/database/src/models/__tests__/session.test.ts +108 -0
- package/packages/database/src/models/session.ts +41 -1
- package/packages/model-bank/src/aiModels/groq.ts +0 -17
- package/packages/model-bank/src/aiModels/novita.ts +2 -60
- package/packages/model-bank/src/aiModels/siliconcloud.ts +116 -17
- package/pnpm-workspace.yaml +1 -0
- package/src/app/[variants]/(main)/discover/(list)/assistant/features/List/Item.tsx +1 -0
- package/src/app/[variants]/(main)/discover/DiscoverRouter.tsx +12 -10
- package/src/app/[variants]/(main)/discover/[[...path]]/page.tsx +7 -6
- package/src/app/[variants]/(main)/discover/features/Search.tsx +1 -0
- package/src/components/Loading/index.ts +1 -0
- package/src/features/AgentSetting/AgentModal/index.tsx +262 -35
- package/src/features/ChatInput/ActionBar/Params/Controls.tsx +261 -50
- package/src/features/ModelParamsControl/FrequencyPenalty.tsx +8 -3
- package/src/features/ModelParamsControl/PresencePenalty.tsx +8 -3
- package/src/features/ModelParamsControl/Temperature.tsx +8 -5
- package/src/features/ModelParamsControl/TopP.tsx +8 -3
- package/src/layout/GlobalProvider/Query.tsx +1 -2
- package/src/services/chat/index.ts +6 -0
- package/e2e/routes.spec.ts +0 -73
- package/playwright.config.ts +0 -35
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { Form, type FormItemProps, Tag } from '@lobehub/ui';
|
|
2
|
+
import { Form as AntdForm, Checkbox } from 'antd';
|
|
3
|
+
import { createStyles } from 'antd-style';
|
|
2
4
|
import isEqual from 'fast-deep-equal';
|
|
3
5
|
import { debounce } from 'lodash-es';
|
|
4
|
-
import { memo } from 'react';
|
|
6
|
+
import { memo, useCallback, useEffect, useRef } from 'react';
|
|
7
|
+
import type { ComponentType } from 'react';
|
|
5
8
|
import { useTranslation } from 'react-i18next';
|
|
6
9
|
import { Flexbox } from 'react-layout-kit';
|
|
7
10
|
|
|
@@ -20,75 +23,283 @@ interface ControlsProps {
|
|
|
20
23
|
setUpdating: (updating: boolean) => void;
|
|
21
24
|
updating: boolean;
|
|
22
25
|
}
|
|
26
|
+
|
|
27
|
+
type ParamKey = 'temperature' | 'top_p' | 'presence_penalty' | 'frequency_penalty';
|
|
28
|
+
|
|
29
|
+
type ParamLabelKey =
|
|
30
|
+
| 'settingModel.temperature.title'
|
|
31
|
+
| 'settingModel.topP.title'
|
|
32
|
+
| 'settingModel.presencePenalty.title'
|
|
33
|
+
| 'settingModel.frequencyPenalty.title';
|
|
34
|
+
|
|
35
|
+
type ParamDescKey =
|
|
36
|
+
| 'settingModel.temperature.desc'
|
|
37
|
+
| 'settingModel.topP.desc'
|
|
38
|
+
| 'settingModel.presencePenalty.desc'
|
|
39
|
+
| 'settingModel.frequencyPenalty.desc';
|
|
40
|
+
|
|
41
|
+
const useStyles = createStyles(({ css, token }) => ({
|
|
42
|
+
checkbox: css`
|
|
43
|
+
.ant-checkbox-inner {
|
|
44
|
+
border-radius: 4px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
&:hover .ant-checkbox-inner {
|
|
48
|
+
border-color: ${token.colorPrimary};
|
|
49
|
+
}
|
|
50
|
+
`,
|
|
51
|
+
label: css`
|
|
52
|
+
user-select: none;
|
|
53
|
+
`,
|
|
54
|
+
sliderWrapper: css`
|
|
55
|
+
display: flex;
|
|
56
|
+
gap: 16px;
|
|
57
|
+
align-items: center;
|
|
58
|
+
width: 100%;
|
|
59
|
+
`,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
// Wrapper component to handle checkbox + slider
|
|
63
|
+
interface ParamControlWrapperProps {
|
|
64
|
+
Component: ComponentType<any>;
|
|
65
|
+
checked: boolean;
|
|
66
|
+
disabled: boolean;
|
|
67
|
+
onChange?: (value: number) => void;
|
|
68
|
+
onToggle: (checked: boolean) => void;
|
|
69
|
+
styles: any;
|
|
70
|
+
value?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const ParamControlWrapper = memo<ParamControlWrapperProps>(
|
|
74
|
+
({ Component, value, onChange, disabled, checked, onToggle, styles }) => {
|
|
75
|
+
return (
|
|
76
|
+
<div className={styles.sliderWrapper}>
|
|
77
|
+
<Checkbox
|
|
78
|
+
checked={checked}
|
|
79
|
+
className={styles.checkbox}
|
|
80
|
+
onChange={(e) => {
|
|
81
|
+
e.stopPropagation();
|
|
82
|
+
onToggle(e.target.checked);
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
<div style={{ flex: 1 }}>
|
|
86
|
+
<Component disabled={disabled} onChange={onChange} value={value} />
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const PARAM_NAME_MAP: Record<ParamKey, (string | number)[]> = {
|
|
94
|
+
frequency_penalty: ['params', 'frequency_penalty'],
|
|
95
|
+
presence_penalty: ['params', 'presence_penalty'],
|
|
96
|
+
temperature: ['params', 'temperature'],
|
|
97
|
+
top_p: ['params', 'top_p'],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const PARAM_DEFAULTS: Record<ParamKey, number> = {
|
|
101
|
+
frequency_penalty: 0,
|
|
102
|
+
presence_penalty: 0,
|
|
103
|
+
temperature: 0.7,
|
|
104
|
+
top_p: 1,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const PARAM_CONFIG = {
|
|
108
|
+
frequency_penalty: {
|
|
109
|
+
Component: FrequencyPenalty,
|
|
110
|
+
descKey: 'settingModel.frequencyPenalty.desc',
|
|
111
|
+
labelKey: 'settingModel.frequencyPenalty.title',
|
|
112
|
+
tag: 'frequency_penalty',
|
|
113
|
+
},
|
|
114
|
+
presence_penalty: {
|
|
115
|
+
Component: PresencePenalty,
|
|
116
|
+
descKey: 'settingModel.presencePenalty.desc',
|
|
117
|
+
labelKey: 'settingModel.presencePenalty.title',
|
|
118
|
+
tag: 'presence_penalty',
|
|
119
|
+
},
|
|
120
|
+
temperature: {
|
|
121
|
+
Component: Temperature,
|
|
122
|
+
descKey: 'settingModel.temperature.desc',
|
|
123
|
+
labelKey: 'settingModel.temperature.title',
|
|
124
|
+
tag: 'temperature',
|
|
125
|
+
},
|
|
126
|
+
top_p: {
|
|
127
|
+
Component: TopP,
|
|
128
|
+
descKey: 'settingModel.topP.desc',
|
|
129
|
+
labelKey: 'settingModel.topP.title',
|
|
130
|
+
tag: 'top_p',
|
|
131
|
+
},
|
|
132
|
+
} satisfies Record<
|
|
133
|
+
ParamKey,
|
|
134
|
+
{
|
|
135
|
+
Component: ComponentType<any>;
|
|
136
|
+
descKey: ParamDescKey;
|
|
137
|
+
labelKey: ParamLabelKey;
|
|
138
|
+
tag: string;
|
|
139
|
+
}
|
|
140
|
+
>;
|
|
141
|
+
|
|
23
142
|
const Controls = memo<ControlsProps>(({ setUpdating }) => {
|
|
24
143
|
const { t } = useTranslation('setting');
|
|
25
144
|
const mobile = useServerConfigStore((s) => s.isMobile);
|
|
26
145
|
const updateAgentConfig = useAgentStore((s) => s.updateAgentConfig);
|
|
146
|
+
const { styles } = useStyles();
|
|
27
147
|
|
|
28
148
|
const config = useAgentStore(agentSelectors.currentAgentConfig, isEqual);
|
|
149
|
+
const [form] = Form.useForm();
|
|
29
150
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
151
|
+
const { frequency_penalty, presence_penalty, temperature, top_p } = config.params ?? {};
|
|
152
|
+
|
|
153
|
+
const lastValuesRef = useRef<Record<ParamKey, number | undefined>>({
|
|
154
|
+
frequency_penalty,
|
|
155
|
+
presence_penalty,
|
|
156
|
+
temperature,
|
|
157
|
+
top_p,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
form.setFieldsValue(config);
|
|
162
|
+
|
|
163
|
+
if (typeof temperature === 'number') lastValuesRef.current.temperature = temperature;
|
|
164
|
+
if (typeof top_p === 'number') lastValuesRef.current.top_p = top_p;
|
|
165
|
+
if (typeof presence_penalty === 'number') {
|
|
166
|
+
lastValuesRef.current.presence_penalty = presence_penalty;
|
|
167
|
+
}
|
|
168
|
+
if (typeof frequency_penalty === 'number') {
|
|
169
|
+
lastValuesRef.current.frequency_penalty = frequency_penalty;
|
|
170
|
+
}
|
|
171
|
+
}, [config, form, frequency_penalty, presence_penalty, temperature, top_p]);
|
|
172
|
+
|
|
173
|
+
const temperatureValue = AntdForm.useWatch(PARAM_NAME_MAP.temperature, form);
|
|
174
|
+
const topPValue = AntdForm.useWatch(PARAM_NAME_MAP.top_p, form);
|
|
175
|
+
const presencePenaltyValue = AntdForm.useWatch(PARAM_NAME_MAP.presence_penalty, form);
|
|
176
|
+
const frequencyPenaltyValue = AntdForm.useWatch(PARAM_NAME_MAP.frequency_penalty, form);
|
|
177
|
+
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (typeof temperatureValue === 'number') lastValuesRef.current.temperature = temperatureValue;
|
|
180
|
+
}, [temperatureValue]);
|
|
181
|
+
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (typeof topPValue === 'number') lastValuesRef.current.top_p = topPValue;
|
|
184
|
+
}, [topPValue]);
|
|
185
|
+
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
if (typeof presencePenaltyValue === 'number') {
|
|
188
|
+
lastValuesRef.current.presence_penalty = presencePenaltyValue;
|
|
189
|
+
}
|
|
190
|
+
}, [presencePenaltyValue]);
|
|
191
|
+
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
if (typeof frequencyPenaltyValue === 'number') {
|
|
194
|
+
lastValuesRef.current.frequency_penalty = frequencyPenaltyValue;
|
|
195
|
+
}
|
|
196
|
+
}, [frequencyPenaltyValue]);
|
|
197
|
+
|
|
198
|
+
const enabledMap: Record<ParamKey, boolean> = {
|
|
199
|
+
frequency_penalty: typeof frequencyPenaltyValue === 'number',
|
|
200
|
+
presence_penalty: typeof presencePenaltyValue === 'number',
|
|
201
|
+
temperature: typeof temperatureValue === 'number',
|
|
202
|
+
top_p: typeof topPValue === 'number',
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const handleToggle = useCallback(
|
|
206
|
+
async (key: ParamKey, enabled: boolean) => {
|
|
207
|
+
const namePath = PARAM_NAME_MAP[key];
|
|
208
|
+
let newValue: number | undefined;
|
|
209
|
+
|
|
210
|
+
if (!enabled) {
|
|
211
|
+
const currentValue = form.getFieldValue(namePath);
|
|
212
|
+
if (typeof currentValue === 'number') {
|
|
213
|
+
lastValuesRef.current[key] = currentValue;
|
|
214
|
+
}
|
|
215
|
+
newValue = undefined;
|
|
216
|
+
form.setFieldValue(namePath, undefined);
|
|
217
|
+
} else {
|
|
218
|
+
const fallback = lastValuesRef.current[key];
|
|
219
|
+
const nextValue = typeof fallback === 'number' ? fallback : PARAM_DEFAULTS[key];
|
|
220
|
+
lastValuesRef.current[key] = nextValue;
|
|
221
|
+
newValue = nextValue;
|
|
222
|
+
form.setFieldValue(namePath, nextValue);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 立即保存变更 - 手动构造配置对象确保使用最新值
|
|
226
|
+
setUpdating(true);
|
|
227
|
+
const currentValues = form.getFieldsValue();
|
|
228
|
+
const prevParams = (currentValues.params ?? {}) as Record<ParamKey, number | undefined>;
|
|
229
|
+
const currentParams: Record<ParamKey, number | undefined> = { ...prevParams };
|
|
230
|
+
|
|
231
|
+
if (newValue === undefined) {
|
|
232
|
+
// 显式删除该属性,而不是设置为 undefined
|
|
233
|
+
// 这样可以确保 Form 表单状态同步
|
|
234
|
+
delete currentParams[key];
|
|
235
|
+
// 使用 null 作为禁用标记(数据库会保留 null,前端据此判断复选框状态)
|
|
236
|
+
currentParams[key] = null as any;
|
|
237
|
+
} else {
|
|
238
|
+
currentParams[key] = newValue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const updatedConfig = {
|
|
242
|
+
...currentValues,
|
|
243
|
+
params: currentParams,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
await updateAgentConfig(updatedConfig);
|
|
247
|
+
setUpdating(false);
|
|
52
248
|
},
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
249
|
+
[form, setUpdating, updateAgentConfig],
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// 使用 useMemo 确保防抖函数只创建一次
|
|
253
|
+
const handleValuesChange = useCallback(
|
|
254
|
+
debounce(async (values) => {
|
|
255
|
+
setUpdating(true);
|
|
256
|
+
await updateAgentConfig(values);
|
|
257
|
+
setUpdating(false);
|
|
258
|
+
}, 500),
|
|
259
|
+
[updateAgentConfig, setUpdating],
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const baseItems: FormItemProps[] = (Object.keys(PARAM_CONFIG) as ParamKey[]).map((key) => {
|
|
263
|
+
const meta = PARAM_CONFIG[key];
|
|
264
|
+
const Component = meta.Component;
|
|
265
|
+
const enabled = enabledMap[key];
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
children: (
|
|
269
|
+
<ParamControlWrapper
|
|
270
|
+
Component={Component}
|
|
271
|
+
checked={enabled}
|
|
272
|
+
disabled={!enabled}
|
|
273
|
+
onToggle={(checked) => handleToggle(key, checked)}
|
|
274
|
+
styles={styles}
|
|
275
|
+
/>
|
|
60
276
|
),
|
|
61
|
-
name: ['params', 'presence_penalty'],
|
|
62
|
-
tag: 'presence_penalty',
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
children: <FrequencyPenalty />,
|
|
66
277
|
label: (
|
|
67
|
-
<Flexbox gap={8} horizontal>
|
|
68
|
-
{t(
|
|
69
|
-
<InfoTooltip title={t(
|
|
278
|
+
<Flexbox align={'center'} className={styles.label} gap={8} horizontal>
|
|
279
|
+
{t(meta.labelKey)}
|
|
280
|
+
<InfoTooltip title={t(meta.descKey)} />
|
|
70
281
|
</Flexbox>
|
|
71
282
|
),
|
|
72
|
-
name: [
|
|
73
|
-
tag:
|
|
74
|
-
}
|
|
75
|
-
|
|
283
|
+
name: PARAM_NAME_MAP[key],
|
|
284
|
+
tag: meta.tag,
|
|
285
|
+
} satisfies FormItemProps;
|
|
286
|
+
});
|
|
76
287
|
|
|
77
288
|
return (
|
|
78
289
|
<Form
|
|
290
|
+
form={form}
|
|
79
291
|
initialValues={config}
|
|
80
|
-
itemMinWidth={
|
|
292
|
+
itemMinWidth={220}
|
|
81
293
|
items={
|
|
82
294
|
mobile
|
|
83
|
-
?
|
|
84
|
-
:
|
|
295
|
+
? baseItems
|
|
296
|
+
: baseItems.map(({ tag, ...item }) => ({
|
|
297
|
+
...item,
|
|
298
|
+
desc: <Tag size={'small'}>{tag}</Tag>,
|
|
299
|
+
}))
|
|
85
300
|
}
|
|
86
301
|
itemsType={'flat'}
|
|
87
|
-
onValuesChange={
|
|
88
|
-
setUpdating(true);
|
|
89
|
-
await updateAgentConfig(values);
|
|
90
|
-
setUpdating(false);
|
|
91
|
-
}, 500)}
|
|
302
|
+
onValuesChange={handleValuesChange}
|
|
92
303
|
styles={{
|
|
93
304
|
group: {
|
|
94
305
|
background: 'transparent',
|
|
@@ -5,16 +5,20 @@ import { memo } from 'react';
|
|
|
5
5
|
import { Flexbox } from 'react-layout-kit';
|
|
6
6
|
|
|
7
7
|
interface FrequencyPenaltyProps {
|
|
8
|
+
disabled?: boolean;
|
|
8
9
|
onChange?: (value: number) => void;
|
|
9
10
|
value?: number;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
const FrequencyPenalty = memo<FrequencyPenaltyProps>(({ value, onChange }) => {
|
|
13
|
+
const FrequencyPenalty = memo<FrequencyPenaltyProps>(({ value, onChange, disabled }) => {
|
|
13
14
|
const theme = useTheme();
|
|
14
15
|
|
|
15
16
|
return (
|
|
16
|
-
<Flexbox style={{
|
|
17
|
+
<Flexbox style={{ width: '100%' }}>
|
|
17
18
|
<SliderWithInput
|
|
19
|
+
changeOnWheel
|
|
20
|
+
controls={false}
|
|
21
|
+
disabled={disabled}
|
|
18
22
|
marks={{
|
|
19
23
|
'-2': (
|
|
20
24
|
<Icon icon={FileIcon} size={'small'} style={{ color: theme.colorTextQuaternary }} />
|
|
@@ -29,9 +33,10 @@ const FrequencyPenalty = memo<FrequencyPenaltyProps>(({ value, onChange }) => {
|
|
|
29
33
|
onChange={onChange}
|
|
30
34
|
size={'small'}
|
|
31
35
|
step={0.1}
|
|
36
|
+
style={{ height: 42 }}
|
|
32
37
|
styles={{
|
|
33
38
|
input: {
|
|
34
|
-
maxWidth:
|
|
39
|
+
maxWidth: 43,
|
|
35
40
|
},
|
|
36
41
|
}}
|
|
37
42
|
value={value}
|
|
@@ -5,16 +5,20 @@ import { memo } from 'react';
|
|
|
5
5
|
import { Flexbox } from 'react-layout-kit';
|
|
6
6
|
|
|
7
7
|
interface PresencePenaltyProps {
|
|
8
|
+
disabled?: boolean;
|
|
8
9
|
onChange?: (value: number) => void;
|
|
9
10
|
value?: number;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
const PresencePenalty = memo<PresencePenaltyProps>(({ value, onChange }) => {
|
|
13
|
+
const PresencePenalty = memo<PresencePenaltyProps>(({ value, onChange, disabled }) => {
|
|
13
14
|
const theme = useTheme();
|
|
14
15
|
|
|
15
16
|
return (
|
|
16
|
-
<Flexbox style={{
|
|
17
|
+
<Flexbox style={{ width: '100%' }}>
|
|
17
18
|
<SliderWithInput
|
|
19
|
+
changeOnWheel
|
|
20
|
+
controls={false}
|
|
21
|
+
disabled={disabled}
|
|
18
22
|
marks={{
|
|
19
23
|
'-2': (
|
|
20
24
|
<Icon icon={RepeatIcon} size={'small'} style={{ color: theme.colorTextQuaternary }} />
|
|
@@ -27,9 +31,10 @@ const PresencePenalty = memo<PresencePenaltyProps>(({ value, onChange }) => {
|
|
|
27
31
|
onChange={onChange}
|
|
28
32
|
size={'small'}
|
|
29
33
|
step={0.1}
|
|
34
|
+
style={{ height: 42 }}
|
|
30
35
|
styles={{
|
|
31
36
|
input: {
|
|
32
|
-
maxWidth:
|
|
37
|
+
maxWidth: 43,
|
|
33
38
|
},
|
|
34
39
|
}}
|
|
35
40
|
value={value}
|
|
@@ -41,16 +41,19 @@ const Warning = memo(() => {
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
interface TemperatureProps {
|
|
44
|
+
disabled?: boolean;
|
|
44
45
|
onChange?: (value: number) => void;
|
|
45
46
|
value?: number;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
const Temperature = memo<TemperatureProps>(({ value, onChange }) => {
|
|
49
|
+
const Temperature = memo<TemperatureProps>(({ value, onChange, disabled }) => {
|
|
49
50
|
const theme = useTheme();
|
|
50
51
|
return (
|
|
51
|
-
<Flexbox gap={4} style={{
|
|
52
|
+
<Flexbox gap={4} style={{ width: '100%' }}>
|
|
52
53
|
<SliderWithInput
|
|
54
|
+
changeOnWheel
|
|
53
55
|
controls={false}
|
|
56
|
+
disabled={disabled}
|
|
54
57
|
marks={{
|
|
55
58
|
0: <Icon icon={Sparkle} size={'small'} style={{ color: theme.colorTextQuaternary }} />,
|
|
56
59
|
1: <div />,
|
|
@@ -60,15 +63,15 @@ const Temperature = memo<TemperatureProps>(({ value, onChange }) => {
|
|
|
60
63
|
onChange={onChange}
|
|
61
64
|
size={'small'}
|
|
62
65
|
step={0.1}
|
|
63
|
-
style={{ height:
|
|
66
|
+
style={{ height: 42 }}
|
|
64
67
|
styles={{
|
|
65
68
|
input: {
|
|
66
|
-
maxWidth:
|
|
69
|
+
maxWidth: 43,
|
|
67
70
|
},
|
|
68
71
|
}}
|
|
69
72
|
value={value}
|
|
70
73
|
/>
|
|
71
|
-
<Warning />
|
|
74
|
+
{!disabled && <Warning />}
|
|
72
75
|
</Flexbox>
|
|
73
76
|
);
|
|
74
77
|
});
|
|
@@ -5,16 +5,20 @@ import { memo } from 'react';
|
|
|
5
5
|
import { Flexbox } from 'react-layout-kit';
|
|
6
6
|
|
|
7
7
|
interface TopPProps {
|
|
8
|
+
disabled?: boolean;
|
|
8
9
|
onChange?: (value: number) => void;
|
|
9
10
|
value?: number;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
const TopP = memo<TopPProps>(({ value, onChange }) => {
|
|
13
|
+
const TopP = memo<TopPProps>(({ value, onChange, disabled }) => {
|
|
13
14
|
const theme = useTheme();
|
|
14
15
|
|
|
15
16
|
return (
|
|
16
|
-
<Flexbox style={{
|
|
17
|
+
<Flexbox style={{ width: '100%' }}>
|
|
17
18
|
<SliderWithInput
|
|
19
|
+
changeOnWheel
|
|
20
|
+
controls={false}
|
|
21
|
+
disabled={disabled}
|
|
18
22
|
marks={{
|
|
19
23
|
0: (
|
|
20
24
|
<Icon
|
|
@@ -31,9 +35,10 @@ const TopP = memo<TopPProps>(({ value, onChange }) => {
|
|
|
31
35
|
onChange={onChange}
|
|
32
36
|
size={'small'}
|
|
33
37
|
step={0.1}
|
|
38
|
+
style={{ height: 42 }}
|
|
34
39
|
styles={{
|
|
35
40
|
input: {
|
|
36
|
-
maxWidth:
|
|
41
|
+
maxWidth: 43,
|
|
37
42
|
},
|
|
38
43
|
}}
|
|
39
44
|
value={value}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { QueryClient } from '@tanstack/query
|
|
4
|
-
import { QueryClientProvider } from '@tanstack/react-query';
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
5
4
|
import React, { PropsWithChildren, useState } from 'react';
|
|
6
5
|
|
|
7
6
|
import { lambdaQuery, lambdaQueryClient } from '@/libs/trpc/client';
|
|
@@ -274,6 +274,12 @@ class ChatService {
|
|
|
274
274
|
{ ...res, apiMode, model },
|
|
275
275
|
);
|
|
276
276
|
|
|
277
|
+
// Convert null to undefined for model params to prevent sending null values to API
|
|
278
|
+
if (payload.temperature === null) payload.temperature = undefined;
|
|
279
|
+
if (payload.top_p === null) payload.top_p = undefined;
|
|
280
|
+
if (payload.presence_penalty === null) payload.presence_penalty = undefined;
|
|
281
|
+
if (payload.frequency_penalty === null) payload.frequency_penalty = undefined;
|
|
282
|
+
|
|
277
283
|
const sdkType = resolveRuntimeProvider(provider);
|
|
278
284
|
|
|
279
285
|
/**
|
package/e2e/routes.spec.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { expect, test } from '@playwright/test';
|
|
2
|
-
|
|
3
|
-
// 覆盖核心可访问路径(含重定向来源)
|
|
4
|
-
const baseRoutes: string[] = [
|
|
5
|
-
'/',
|
|
6
|
-
'/chat',
|
|
7
|
-
'/discover',
|
|
8
|
-
'/image',
|
|
9
|
-
'/files',
|
|
10
|
-
'/repos', // next.config.ts -> /files
|
|
11
|
-
'/changelog',
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
// settings 路由改为通过 query 参数控制 active tab
|
|
15
|
-
// 参考 SettingsTabs: about, agent, common, hotkey, llm, provider, proxy, storage, system-agent, tts
|
|
16
|
-
const settingsTabs = [
|
|
17
|
-
'common',
|
|
18
|
-
'llm',
|
|
19
|
-
'provider',
|
|
20
|
-
'about',
|
|
21
|
-
'hotkey',
|
|
22
|
-
'proxy',
|
|
23
|
-
'storage',
|
|
24
|
-
'tts',
|
|
25
|
-
'system-agent',
|
|
26
|
-
'agent',
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
const routes: string[] = [...baseRoutes, ...settingsTabs.map((key) => `/settings?active=${key}`)];
|
|
30
|
-
|
|
31
|
-
// CI 环境下跳过容易不稳定或受特性开关影响的路由
|
|
32
|
-
const ciSkipPaths = new Set<string>([
|
|
33
|
-
'/image',
|
|
34
|
-
'/changelog',
|
|
35
|
-
'/settings?active=common',
|
|
36
|
-
'/settings?active=llm',
|
|
37
|
-
]);
|
|
38
|
-
|
|
39
|
-
// @ts-ignore
|
|
40
|
-
async function assertNoPageErrors(page: Parameters<typeof test>[0]['page']) {
|
|
41
|
-
const pageErrors: Error[] = [];
|
|
42
|
-
const consoleErrors: string[] = [];
|
|
43
|
-
|
|
44
|
-
page.on('pageerror', (err: Error) => pageErrors.push(err));
|
|
45
|
-
page.on('console', (msg: any) => {
|
|
46
|
-
if (msg.type() === 'error') consoleErrors.push(msg.text());
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// 仅校验页面级错误,忽略控制台 error 以提升稳定性
|
|
50
|
-
expect
|
|
51
|
-
.soft(pageErrors, `page errors: ${pageErrors.map((e) => e.message).join('\n')}`)
|
|
52
|
-
.toHaveLength(0);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
test.describe('Smoke: core routes', () => {
|
|
56
|
-
for (const path of routes) {
|
|
57
|
-
test(`should open ${path} without error`, async ({ page }) => {
|
|
58
|
-
if (process.env.CI && ciSkipPaths.has(path)) test.skip(true, 'skip flaky route on CI');
|
|
59
|
-
const response = await page.goto(path, { waitUntil: 'commit' });
|
|
60
|
-
// 2xx 或 3xx 视为可接受(允许中间件/重定向)
|
|
61
|
-
const status = response?.status() ?? 0;
|
|
62
|
-
expect(status, `unexpected status for ${path}: ${status}`).toBeLessThan(400);
|
|
63
|
-
|
|
64
|
-
// 一般错误标题防御
|
|
65
|
-
await expect(page).not.toHaveTitle(/not found|error/i);
|
|
66
|
-
|
|
67
|
-
// body 可见
|
|
68
|
-
await expect(page.locator('body')).toBeVisible();
|
|
69
|
-
|
|
70
|
-
await assertNoPageErrors(page);
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
});
|
package/playwright.config.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
-
|
|
3
|
-
const PORT = process.env.PORT ? Number(process.env.PORT) : 3010;
|
|
4
|
-
|
|
5
|
-
export default defineConfig({
|
|
6
|
-
expect: { timeout: 10_000 },
|
|
7
|
-
fullyParallel: true,
|
|
8
|
-
projects: [
|
|
9
|
-
{
|
|
10
|
-
name: 'chromium',
|
|
11
|
-
use: { ...devices['Desktop Chrome'] },
|
|
12
|
-
},
|
|
13
|
-
],
|
|
14
|
-
reporter: 'list',
|
|
15
|
-
retries: 0,
|
|
16
|
-
testDir: './e2e',
|
|
17
|
-
timeout: 1_200_000,
|
|
18
|
-
use: {
|
|
19
|
-
baseURL: `http://localhost:${PORT}`,
|
|
20
|
-
trace: 'on-first-retry',
|
|
21
|
-
},
|
|
22
|
-
webServer: {
|
|
23
|
-
command: 'npm run dev',
|
|
24
|
-
env: {
|
|
25
|
-
ENABLE_AUTH_PROTECTION: '0',
|
|
26
|
-
ENABLE_OIDC: '0',
|
|
27
|
-
NEXT_PUBLIC_ENABLE_CLERK_AUTH: '0',
|
|
28
|
-
NEXT_PUBLIC_ENABLE_NEXT_AUTH: '0',
|
|
29
|
-
NODE_OPTIONS: '--max-old-space-size=6144',
|
|
30
|
-
},
|
|
31
|
-
reuseExistingServer: true,
|
|
32
|
-
timeout: 120_000,
|
|
33
|
-
url: `http://localhost:${PORT}/chat`,
|
|
34
|
-
},
|
|
35
|
-
});
|