feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
const { defineConfig } = require('@coze-arch/stylelint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
extends: [],
|
||||
});
|
||||
16
frontend/packages/agent-ide/model-manager/README.md
Normal file
16
frontend/packages/agent-ide/model-manager/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# @coze-agent-ide/model-manager
|
||||
|
||||
Bot 编辑页面下模型管理编辑等功能。
|
||||
|
||||
## Features
|
||||
|
||||
- [x] eslint & ts
|
||||
- [x] esm bundle
|
||||
- [x] umd bundle
|
||||
- [x] storybook
|
||||
|
||||
## Commands
|
||||
|
||||
- init: `rush update`
|
||||
- dev: `npm run dev`
|
||||
- build: `npm run build`
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { convertModelParamsToSchema } from '../src/utils/model/convert-model-params-to-schema';
|
||||
import {
|
||||
ModelFormComponent,
|
||||
ModelFormVoidFieldComponent,
|
||||
} from '../src/constant/model-form-component';
|
||||
import modelParams from './model-params.json';
|
||||
|
||||
vi.mock('@coze-studio/bot-detail-store/multi-agent', () => ({}));
|
||||
vi.mock('@coze-studio/bot-detail-store/model', () => ({}));
|
||||
vi.mock('@coze-studio/bot-detail-store/bot-info', () => ({}));
|
||||
|
||||
describe('convert-model-params-to-schema', () => {
|
||||
it('should convert correctly', () => {
|
||||
const res = convertModelParamsToSchema({ model_params: modelParams });
|
||||
expect(res).toStrictEqual({
|
||||
type: 'object',
|
||||
properties: {
|
||||
1: {
|
||||
type: 'void',
|
||||
'x-decorator':
|
||||
ModelFormVoidFieldComponent.ModelFormGenerationDiversityGroupItem,
|
||||
'x-decorator-props': {
|
||||
title: '生成多样性',
|
||||
},
|
||||
'x-index': 1,
|
||||
properties: {
|
||||
temperature: {
|
||||
type: 'number',
|
||||
'x-component': ModelFormComponent.SliderInputNumber,
|
||||
'x-component-props': {
|
||||
max: 2,
|
||||
min: 0,
|
||||
step: 0.01,
|
||||
decimalPlaces: 2,
|
||||
},
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-decorator-props': {
|
||||
label: '回复随机性',
|
||||
popoverContent:
|
||||
'即 Temperature,较高的 Temperature 会让模型生成更多样和创新的文本,反之生成内容会更加保守且类似于训练数据。',
|
||||
},
|
||||
'x-index': 1,
|
||||
},
|
||||
top_p: {
|
||||
type: 'number',
|
||||
'x-component': ModelFormComponent.SliderInputNumber,
|
||||
'x-component-props': {
|
||||
max: 1,
|
||||
min: 0,
|
||||
step: 0.01,
|
||||
decimalPlaces: 2,
|
||||
},
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-decorator-props': {
|
||||
label: 'Top P',
|
||||
popoverContent:
|
||||
'设定Top p概率阈值,模型在生成文本时只从概率超过阈值的词汇中选择,从而控制文本的多样性',
|
||||
},
|
||||
'x-index': 2,
|
||||
},
|
||||
frequency_penalty: {
|
||||
type: 'number',
|
||||
'x-component': ModelFormComponent.SliderInputNumber,
|
||||
'x-component-props': {
|
||||
max: 2,
|
||||
min: -2,
|
||||
step: 0.01,
|
||||
decimalPlaces: 2,
|
||||
},
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-decorator-props': {
|
||||
label: '重复词汇惩罚',
|
||||
popoverContent:
|
||||
'当该值为正时,它会降低已出现词汇的重复率,进而提高模型输出词汇的多样性',
|
||||
},
|
||||
'x-index': 3,
|
||||
},
|
||||
presence_penalty: {
|
||||
type: 'number',
|
||||
'x-component': ModelFormComponent.SliderInputNumber,
|
||||
'x-component-props': {
|
||||
max: 2,
|
||||
min: -2,
|
||||
step: 0.01,
|
||||
decimalPlaces: 2,
|
||||
},
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-decorator-props': {
|
||||
label: '存在惩罚',
|
||||
popoverContent:
|
||||
'减少已提及内容的重复,增加新主题和概念的引入,促进内容的多元化。',
|
||||
},
|
||||
'x-index': 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
2: {
|
||||
type: 'void',
|
||||
'x-decorator': ModelFormVoidFieldComponent.ModelFormGroupItem,
|
||||
'x-decorator-props': {
|
||||
title: '输入及输出长度',
|
||||
},
|
||||
'x-index': 2,
|
||||
properties: {
|
||||
max_tokens: {
|
||||
type: 'number',
|
||||
'x-component': ModelFormComponent.SliderInputNumber,
|
||||
'x-component-props': {
|
||||
max: 16384,
|
||||
min: 1,
|
||||
step: 1,
|
||||
decimalPlaces: 0,
|
||||
},
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-decorator-props': {
|
||||
label: '最大回复长度',
|
||||
popoverContent:
|
||||
'可控制模型回复的最多 Token 数量,以满足不同场景和需求。通常 100 Tokens 约等于 60 个中文汉字。',
|
||||
},
|
||||
'x-index': 1,
|
||||
},
|
||||
response_format: {
|
||||
type: 'number',
|
||||
'x-component': ModelFormComponent.RadioButton,
|
||||
'x-component-props': {
|
||||
type: 'button',
|
||||
options: [
|
||||
{
|
||||
label: '文本',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: 'Markdown',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-decorator-props': {
|
||||
label: '输出格式',
|
||||
popoverContent:
|
||||
'文本: 使用普通文本格式回复Markdown: 将强制模型使用Markdown格式输出回复\nJSON: 将强制模型使用 JSON 格式输出回复',
|
||||
},
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,323 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getFixedSingleAgentSchema } from '../src/utils/model/get-fixed-single-agent-schema';
|
||||
import {
|
||||
ModelFormComponent,
|
||||
ModelFormVoidFieldComponent,
|
||||
} from '../src/constant/model-form-component';
|
||||
|
||||
vi.mock('@coze-studio/bot-detail-store', () => ({}));
|
||||
|
||||
vi.mock('@coze-arch/i18n', () => ({
|
||||
I18n: {
|
||||
t: () => 'mockedI18n',
|
||||
},
|
||||
}));
|
||||
|
||||
describe('get-fixed-single-agent-schema', () => {
|
||||
it('should fixed correctly', () => {
|
||||
const prevSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
1: {
|
||||
type: 'void',
|
||||
'x-decorator':
|
||||
ModelFormVoidFieldComponent.ModelFormGenerationDiversityGroupItem,
|
||||
'x-decorator-props': {
|
||||
title: '生成多样性',
|
||||
},
|
||||
'x-index': 1,
|
||||
properties: {
|
||||
temperature: {
|
||||
type: 'number',
|
||||
'x-component': ModelFormComponent.SliderInputNumber,
|
||||
'x-component-props': {
|
||||
max: 2,
|
||||
min: 0,
|
||||
step: 0.01,
|
||||
decimalPlaces: 2,
|
||||
},
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-decorator-props': {
|
||||
label: '回复随机性',
|
||||
popoverContent:
|
||||
'即 Temperature,较高的 Temperature 会让模型生成更多样和创新的文本,反之生成内容会更加保守且类似于训练数据。',
|
||||
},
|
||||
'x-index': 1,
|
||||
},
|
||||
top_p: {
|
||||
type: 'number',
|
||||
'x-component': ModelFormComponent.SliderInputNumber,
|
||||
'x-component-props': {
|
||||
max: 1,
|
||||
min: 0,
|
||||
step: 0.01,
|
||||
decimalPlaces: 2,
|
||||
},
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-decorator-props': {
|
||||
label: 'Top P',
|
||||
popoverContent:
|
||||
'设定Top p概率阈值,模型在生成文本时只从概率超过阈值的词汇中选择,从而控制文本的多样性',
|
||||
},
|
||||
'x-index': 2,
|
||||
},
|
||||
frequency_penalty: {
|
||||
type: 'number',
|
||||
'x-component': ModelFormComponent.SliderInputNumber,
|
||||
'x-component-props': {
|
||||
max: 2,
|
||||
min: -2,
|
||||
step: 0.01,
|
||||
decimalPlaces: 2,
|
||||
},
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-decorator-props': {
|
||||
label: '重复词汇惩罚',
|
||||
popoverContent:
|
||||
'当该值为正时,它会降低已出现词汇的重复率,进而提高模型输出词汇的多样性',
|
||||
},
|
||||
'x-index': 3,
|
||||
},
|
||||
presence_penalty: {
|
||||
type: 'number',
|
||||
'x-component': ModelFormComponent.SliderInputNumber,
|
||||
'x-component-props': {
|
||||
max: 2,
|
||||
min: -2,
|
||||
step: 0.01,
|
||||
decimalPlaces: 2,
|
||||
},
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-decorator-props': {
|
||||
label: '存在惩罚',
|
||||
popoverContent:
|
||||
'减少已提及内容的重复,增加新主题和概念的引入,促进内容的多元化。',
|
||||
},
|
||||
'x-index': 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
2: {
|
||||
type: 'void',
|
||||
'x-decorator': ModelFormVoidFieldComponent.ModelFormGroupItem,
|
||||
'x-decorator-props': {
|
||||
title: '输入及输出长度',
|
||||
},
|
||||
'x-index': 2,
|
||||
properties: {
|
||||
max_tokens: {
|
||||
type: 'number',
|
||||
'x-component': ModelFormComponent.SliderInputNumber,
|
||||
'x-component-props': {
|
||||
max: 16384,
|
||||
min: 1,
|
||||
step: 1,
|
||||
decimalPlaces: 0,
|
||||
},
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-decorator-props': {
|
||||
label: '最大回复长度',
|
||||
popoverContent:
|
||||
'可控制模型回复的最多 Token 数量,以满足不同场景和需求。通常 100 Tokens 约等于 60 个中文汉字。',
|
||||
},
|
||||
'x-index': 1,
|
||||
},
|
||||
response_format: {
|
||||
type: 'number',
|
||||
'x-component': ModelFormComponent.RadioButton,
|
||||
'x-component-props': {
|
||||
type: 'button',
|
||||
options: [
|
||||
{
|
||||
label: '文本',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: 'Markdown',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-decorator-props': {
|
||||
label: '输出格式',
|
||||
popoverContent:
|
||||
'文本: 使用普通文本格式回复Markdown: 将强制模型使用Markdown格式输出回复\nJSON: 将强制模型使用 JSON 格式输出回复',
|
||||
},
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const res = getFixedSingleAgentSchema(prevSchema);
|
||||
|
||||
expect(res).toStrictEqual({
|
||||
type: 'object',
|
||||
properties: {
|
||||
1: {
|
||||
type: 'void',
|
||||
'x-decorator':
|
||||
ModelFormVoidFieldComponent.ModelFormGenerationDiversityGroupItem,
|
||||
'x-decorator-props': {
|
||||
title: '生成多样性',
|
||||
},
|
||||
'x-index': 1,
|
||||
properties: {
|
||||
temperature: {
|
||||
type: 'number',
|
||||
'x-component': ModelFormComponent.SliderInputNumber,
|
||||
'x-component-props': {
|
||||
max: 2,
|
||||
min: 0,
|
||||
step: 0.01,
|
||||
decimalPlaces: 2,
|
||||
},
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-decorator-props': {
|
||||
label: '回复随机性',
|
||||
popoverContent:
|
||||
'即 Temperature,较高的 Temperature 会让模型生成更多样和创新的文本,反之生成内容会更加保守且类似于训练数据。',
|
||||
},
|
||||
'x-index': 1,
|
||||
},
|
||||
top_p: {
|
||||
type: 'number',
|
||||
'x-component': ModelFormComponent.SliderInputNumber,
|
||||
'x-component-props': {
|
||||
max: 1,
|
||||
min: 0,
|
||||
step: 0.01,
|
||||
decimalPlaces: 2,
|
||||
},
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-decorator-props': {
|
||||
label: 'Top P',
|
||||
popoverContent:
|
||||
'设定Top p概率阈值,模型在生成文本时只从概率超过阈值的词汇中选择,从而控制文本的多样性',
|
||||
},
|
||||
'x-index': 2,
|
||||
},
|
||||
frequency_penalty: {
|
||||
type: 'number',
|
||||
'x-component': ModelFormComponent.SliderInputNumber,
|
||||
'x-component-props': {
|
||||
max: 2,
|
||||
min: -2,
|
||||
step: 0.01,
|
||||
decimalPlaces: 2,
|
||||
},
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-decorator-props': {
|
||||
label: '重复词汇惩罚',
|
||||
popoverContent:
|
||||
'当该值为正时,它会降低已出现词汇的重复率,进而提高模型输出词汇的多样性',
|
||||
},
|
||||
'x-index': 3,
|
||||
},
|
||||
presence_penalty: {
|
||||
type: 'number',
|
||||
'x-component': ModelFormComponent.SliderInputNumber,
|
||||
'x-component-props': {
|
||||
max: 2,
|
||||
min: -2,
|
||||
step: 0.01,
|
||||
decimalPlaces: 2,
|
||||
},
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-decorator-props': {
|
||||
label: '存在惩罚',
|
||||
popoverContent:
|
||||
'减少已提及内容的重复,增加新主题和概念的引入,促进内容的多元化。',
|
||||
},
|
||||
'x-index': 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
2: {
|
||||
type: 'void',
|
||||
'x-decorator': ModelFormVoidFieldComponent.ModelFormGroupItem,
|
||||
'x-decorator-props': {
|
||||
title: '输入及输出长度',
|
||||
},
|
||||
'x-index': 2,
|
||||
properties: {
|
||||
HistoryRound: {
|
||||
type: 'number',
|
||||
'x-component': ModelFormComponent.SliderInputNumber,
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-component-props': {
|
||||
step: 1,
|
||||
max: 100,
|
||||
min: 0,
|
||||
decimalPlaces: 0,
|
||||
},
|
||||
'x-decorator-props': {
|
||||
label: 'mockedI18n',
|
||||
popoverContent: 'mockedI18n',
|
||||
},
|
||||
// 放到最前面
|
||||
'x-index': 0,
|
||||
},
|
||||
max_tokens: {
|
||||
type: 'number',
|
||||
'x-component': ModelFormComponent.SliderInputNumber,
|
||||
'x-component-props': {
|
||||
max: 16384,
|
||||
min: 1,
|
||||
step: 1,
|
||||
decimalPlaces: 0,
|
||||
},
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-decorator-props': {
|
||||
label: '最大回复长度',
|
||||
popoverContent:
|
||||
'可控制模型回复的最多 Token 数量,以满足不同场景和需求。通常 100 Tokens 约等于 60 个中文汉字。',
|
||||
},
|
||||
'x-index': 1,
|
||||
},
|
||||
response_format: {
|
||||
type: 'number',
|
||||
'x-component': ModelFormComponent.RadioButton,
|
||||
'x-component-props': {
|
||||
type: 'button',
|
||||
options: [
|
||||
{
|
||||
label: '文本',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: 'Markdown',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-decorator-props': {
|
||||
label: '输出格式',
|
||||
popoverContent:
|
||||
'文本: 使用普通文本格式回复Markdown: 将强制模型使用Markdown格式输出回复\nJSON: 将强制模型使用 JSON 格式输出回复',
|
||||
},
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getModelClassSortList } from '../src/utils/model/get-model-class-sort-list';
|
||||
|
||||
vi.mock('@coze-studio/bot-detail-store', () => ({}));
|
||||
|
||||
describe('get-model-class-sort-list', () => {
|
||||
it('should sort correctly', () => {
|
||||
const res = getModelClassSortList([
|
||||
'1',
|
||||
'1',
|
||||
'3',
|
||||
'11',
|
||||
'3',
|
||||
'3',
|
||||
'1',
|
||||
'1',
|
||||
'1',
|
||||
'1',
|
||||
'5',
|
||||
'1',
|
||||
'11',
|
||||
'3',
|
||||
'5',
|
||||
'1',
|
||||
]);
|
||||
expect(res).toStrictEqual(['1', '3', '11', '5']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
[
|
||||
{
|
||||
"name": "temperature",
|
||||
"label": "回复随机性",
|
||||
"desc": "即 Temperature,较高的 Temperature 会让模型生成更多样和创新的文本,反之生成内容会更加保守且类似于训练数据。",
|
||||
"type": 1,
|
||||
"min": "0",
|
||||
"max": "2",
|
||||
"precision": 2,
|
||||
"default_val": {
|
||||
"default_val": "1",
|
||||
"creative": "0.8",
|
||||
"balance": "0.5",
|
||||
"precise": "0.1"
|
||||
},
|
||||
"options": [],
|
||||
"param_class": {
|
||||
"class_id": 1,
|
||||
"label": "生成多样性"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "top_p",
|
||||
"label": "Top P",
|
||||
"desc": "设定Top p概率阈值,模型在生成文本时只从概率超过阈值的词汇中选择,从而控制文本的多样性",
|
||||
"type": 1,
|
||||
"min": "0",
|
||||
"max": "1",
|
||||
"precision": 2,
|
||||
"default_val": {
|
||||
"default_val": "1",
|
||||
"creative": "1",
|
||||
"balance": "1",
|
||||
"precise": "1"
|
||||
},
|
||||
"options": [],
|
||||
"param_class": {
|
||||
"class_id": 1,
|
||||
"label": "生成多样性"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "frequency_penalty",
|
||||
"label": "重复词汇惩罚",
|
||||
"desc": "当该值为正时,它会降低已出现词汇的重复率,进而提高模型输出词汇的多样性",
|
||||
"type": 1,
|
||||
"min": "-2",
|
||||
"max": "2",
|
||||
"precision": 2,
|
||||
"default_val": {
|
||||
"default_val": "0",
|
||||
"creative": "0",
|
||||
"balance": "0",
|
||||
"precise": "0"
|
||||
},
|
||||
"options": [],
|
||||
"param_class": {
|
||||
"class_id": 1,
|
||||
"label": "生成多样性"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "presence_penalty",
|
||||
"label": "存在惩罚",
|
||||
"desc": "减少已提及内容的重复,增加新主题和概念的引入,促进内容的多元化。",
|
||||
"type": 1,
|
||||
"min": "-2",
|
||||
"max": "2",
|
||||
"precision": 2,
|
||||
"default_val": {
|
||||
"default_val": "0",
|
||||
"creative": "0",
|
||||
"balance": "0",
|
||||
"precise": "0"
|
||||
},
|
||||
"options": [],
|
||||
"param_class": {
|
||||
"class_id": 1,
|
||||
"label": "生成多样性"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "max_tokens",
|
||||
"label": "最大回复长度",
|
||||
"desc": "可控制模型回复的最多 Token 数量,以满足不同场景和需求。通常 100 Tokens 约等于 60 个中文汉字。",
|
||||
"type": 2,
|
||||
"min": "1",
|
||||
"max": "16384",
|
||||
"precision": 0,
|
||||
"default_val": {
|
||||
"default_val": "2048"
|
||||
},
|
||||
"options": [],
|
||||
"param_class": {
|
||||
"class_id": 2,
|
||||
"label": "输入及输出长度"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "response_format",
|
||||
"label": "输出格式",
|
||||
"desc": "文本: 使用普通文本格式回复Markdown: 将强制模型使用Markdown格式输出回复\nJSON: 将强制模型使用 JSON 格式输出回复",
|
||||
"type": 2,
|
||||
"min": "",
|
||||
"max": "",
|
||||
"precision": 0,
|
||||
"default_val": {
|
||||
"default_val": "0"
|
||||
},
|
||||
"options": [
|
||||
{
|
||||
"label": "文本",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"label": "Markdown",
|
||||
"value": "1"
|
||||
}
|
||||
],
|
||||
"param_class": {
|
||||
"class_id": 2,
|
||||
"label": "输入及输出长度"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["./dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {},
|
||||
});
|
||||
105
frontend/packages/agent-ide/model-manager/package.json
Normal file
105
frontend/packages/agent-ide/model-manager/package.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"name": "@coze-agent-ide/model-manager",
|
||||
"version": "0.0.1",
|
||||
"description": "Bot 编辑页面下模型管理编辑等功能。",
|
||||
"license": "Apache-2.0",
|
||||
"author": "gaoyuanhan.duty@bytedance.com",
|
||||
"maintainers": [],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./model-select-v2": "./src/components/model-select-v2/index.ts"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"model-select-v2": [
|
||||
"./src/components/model-select-v2/index.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "npm run test -- --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blueprintjs/core": "^5.1.5",
|
||||
"@coze-arch/bot-error": "workspace:*",
|
||||
"@formily/core": "^2.3.0",
|
||||
"@formily/react": "^2.3.0",
|
||||
"classnames": "^2.3.2",
|
||||
"immer": "^10.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@coze-agent-ide/bot-creator-context": "workspace:*",
|
||||
"@coze-agent-ide/bot-editor-context-store": "workspace:*",
|
||||
"@coze-agent-ide/tool": "workspace:*",
|
||||
"@coze-agent-ide/tool-config": "workspace:*",
|
||||
"@coze-arch/bot-api": "workspace:*",
|
||||
"@coze-arch/bot-env": "workspace:*",
|
||||
"@coze-arch/bot-flags": "workspace:*",
|
||||
"@coze-arch/bot-hooks": "workspace:*",
|
||||
"@coze-arch/bot-icons": "workspace:*",
|
||||
"@coze-arch/bot-md-box-adapter": "workspace:*",
|
||||
"@coze-arch/bot-semi": "workspace:*",
|
||||
"@coze-arch/bot-space-api": "workspace:*",
|
||||
"@coze-arch/bot-typings": "workspace:*",
|
||||
"@coze-arch/bot-utils": "workspace:*",
|
||||
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
|
||||
"@coze-arch/eslint-config": "workspace:*",
|
||||
"@coze-arch/i18n": "workspace:*",
|
||||
"@coze-arch/stylelint-config": "workspace:*",
|
||||
"@coze-arch/ts-config": "workspace:*",
|
||||
"@coze-arch/vitest-config": "workspace:*",
|
||||
"@coze-studio/bot-detail-store": "workspace:*",
|
||||
"@coze-studio/components": "workspace:*",
|
||||
"@coze-studio/premium-components-adapter": "workspace:*",
|
||||
"@coze-studio/premium-store-adapter": "workspace:*",
|
||||
"@modern-js/plugin-router-v5": "^2.38.0",
|
||||
"@modern-js/runtime": "^2.38.0",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@types/lodash-es": "^4.17.10",
|
||||
"@types/react": "18.2.37",
|
||||
"@types/react-dom": "18.2.15",
|
||||
"@vitest/coverage-v8": "~3.0.5",
|
||||
"ahooks": "^3.7.8",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nanoid": "^4.0.2",
|
||||
"react": "~18.2.0",
|
||||
"react-dom": "~18.2.0",
|
||||
"react-is": ">=16.8.0",
|
||||
"require-from-string": "^2.0.2",
|
||||
"stylelint": "^15.11.0",
|
||||
"typescript": "~5.8.2",
|
||||
"use-event-callback": "~0.1.0",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-svgr": "~3.3.0",
|
||||
"vitest": "~3.0.5",
|
||||
"webpack": "~5.91.0",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@coze-agent-ide/tool": "workspace:*",
|
||||
"@coze-agent-ide/tool-config": "workspace:*",
|
||||
"@coze-arch/bot-env": "workspace:*",
|
||||
"@coze-arch/bot-flags": "workspace:*",
|
||||
"@coze-arch/bot-icons": "workspace:*",
|
||||
"@coze-arch/bot-md-box-adapter": "workspace:*",
|
||||
"@coze-arch/bot-semi": "workspace:*",
|
||||
"@coze-arch/bot-typings": "workspace:*",
|
||||
"@coze-arch/i18n": "workspace:*",
|
||||
"@coze-studio/bot-detail-store": "workspace:*",
|
||||
"@douyinfe/semi-webpack-plugin": "^2.38.1",
|
||||
"ahooks": "^3.7.8",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nanoid": "^4.0.2",
|
||||
"react": ">=18.2.0",
|
||||
"react-dom": ">=18.2.0",
|
||||
"zustand": "^4.4.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState, type FC } from 'react';
|
||||
|
||||
import { groupBy } from 'lodash-es';
|
||||
import { ToolGroupKey, ToolKey } from '@coze-agent-ide/tool-config';
|
||||
import {
|
||||
type useRegisteredToolKeyConfigList,
|
||||
abilityKey2ModelFunctionConfigType,
|
||||
} from '@coze-agent-ide/tool';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
type Model,
|
||||
ModelFuncConfigStatus,
|
||||
ModelFuncConfigType,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
import { useBotSkillStore } from '@coze-studio/bot-detail-store/bot-skill';
|
||||
import { mergeModelFuncConfigStatus } from '@coze-agent-ide/bot-editor-context-store';
|
||||
import { IconCozCross } from '@coze-arch/coze-design/icons';
|
||||
import { Button, Checkbox, Modal, IconButton, Space } from '@coze-arch/coze-design';
|
||||
|
||||
type IRegisteredToolKeyConfig = ReturnType<
|
||||
typeof useRegisteredToolKeyConfigList
|
||||
>[number];
|
||||
|
||||
const getToolGroupText = (key: ToolGroupKey): string =>
|
||||
({
|
||||
[ToolGroupKey.SKILL]: I18n.t('bot_edit_type_skills'),
|
||||
[ToolGroupKey.KNOWLEDGE]: I18n.t('bot_edit_type_knowledge'),
|
||||
[ToolGroupKey.MEMORY]: I18n.t('bot_edit_type_memory'),
|
||||
[ToolGroupKey.DIALOG]: I18n.t('bot_edit_type_dialog'),
|
||||
[ToolGroupKey.CHARACTER]: I18n.t('bot_edit_type_character'),
|
||||
[ToolGroupKey.HOOKS]: 'Hooks',
|
||||
})[key];
|
||||
|
||||
const getToolText = (toolKey: ToolKey) =>
|
||||
({
|
||||
[ToolKey.PLUGIN]: I18n.t('Plugins'),
|
||||
[ToolKey.WORKFLOW]: I18n.t('Workflows'),
|
||||
[ToolKey.IMAGEFLOW]: I18n.t('imageflow_title'),
|
||||
// 已废弃
|
||||
[ToolKey.KNOWLEDGE]: '',
|
||||
[ToolKey.VARIABLE]: I18n.t('user_profile'),
|
||||
[ToolKey.DATABASE]: I18n.t('bot_database'),
|
||||
[ToolKey.LONG_TERM_MEMORY]: I18n.t('timecapsule_1228_001'),
|
||||
[ToolKey.FILE_BOX]: I18n.t('Starling_filebox_name'),
|
||||
[ToolKey.TRIGGER]: I18n.t('platfrom_triggers_title'),
|
||||
[ToolKey.ONBOARDING]: I18n.t('bot_preview_opening_remarks'),
|
||||
[ToolKey.SUGGEST]: I18n.t('bot_edit_suggestion'),
|
||||
[ToolKey.VOICE]: I18n.t('bot_edit_voices_title'),
|
||||
[ToolKey.BACKGROUND]: I18n.t('bgi_title'),
|
||||
[ToolKey.DOCUMENT]: I18n.t('dataset_detail_type_text'),
|
||||
[ToolKey.TABLE]: I18n.t('dataset_detail_type_table'),
|
||||
[ToolKey.PHOTO]: I18n.t('knowledge_photo_025'),
|
||||
[ToolKey.SHORTCUT]: I18n.t('bot_ide_shortcut'),
|
||||
[ToolKey.DEV_HOOKS]: 'Hooks',
|
||||
[ToolKey.USER_INPUT]: I18n.t('chat_setting_user_input_default_mode'),
|
||||
})[toolKey];
|
||||
|
||||
const AlertGroups: FC<{ items: AlertItem[] }> = ({ items }) => {
|
||||
const grouped = groupBy(items, 'groupTitle');
|
||||
return (
|
||||
<div>
|
||||
{Object.entries(grouped).map(([groupTitle, groupItems]) => (
|
||||
<div
|
||||
key={groupTitle}
|
||||
className="py-[12px] flex items-center coz-stroke-primary border-0 border-solid border-b gap-5 pr-2"
|
||||
>
|
||||
<div className="min-w-[60px]">{groupTitle}</div>
|
||||
<div className="flex-1 overflow-hidden whitespace-pre-line">
|
||||
{groupItems.map(item => item.title).join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface AlertItem {
|
||||
title: string;
|
||||
groupTitle: string;
|
||||
}
|
||||
|
||||
export const ModelCapabilityAlertModelContent: FC<{
|
||||
notSupported: AlertItem[];
|
||||
poorSupported: AlertItem[];
|
||||
modelName: string;
|
||||
onOk: () => void;
|
||||
onCancel: () => void;
|
||||
}> = ({ notSupported, poorSupported, modelName, onOk, onCancel }) => {
|
||||
const [dontShowAgain, setDontShowAgain] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="coz-fg-plus text-[20px]">
|
||||
{I18n.t('confirm_switch_model')}
|
||||
</div>
|
||||
<IconButton
|
||||
icon={<IconCozCross />}
|
||||
onClick={onCancel}
|
||||
theme="borderless"
|
||||
/>
|
||||
</div>
|
||||
<div className="coz-fg-primary text-[14px]">
|
||||
{I18n.t('model_support_poor_warning', {
|
||||
modelName,
|
||||
})}
|
||||
</div>
|
||||
{notSupported.length ? (
|
||||
<div className="mt-[24px]">
|
||||
<div className="coz-fg-secondary text-[12px]">
|
||||
{I18n.t('model_not_supported')}
|
||||
</div>
|
||||
<AlertGroups items={notSupported} />
|
||||
</div>
|
||||
) : null}
|
||||
{poorSupported.length ? (
|
||||
<div className="mt-[24px]">
|
||||
<div className="coz-fg-secondary text-[12px]">
|
||||
{I18n.t('model_support_poor')}
|
||||
</div>
|
||||
<AlertGroups items={poorSupported} />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-[24px] flex items-center justify-between">
|
||||
<Checkbox onChange={e => setDontShowAgain(!!e.target.checked)}>
|
||||
{I18n.t('do_not_remind_again')}
|
||||
</Checkbox>
|
||||
<Space>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
onCancel();
|
||||
}}
|
||||
>
|
||||
{I18n.t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
color="brand"
|
||||
onClick={() => {
|
||||
if (dontShowAgain) {
|
||||
localStorage.setItem(DONT_SHOW_TIPS_LOCAL_CACHE_KEY, 'true');
|
||||
}
|
||||
onOk();
|
||||
}}
|
||||
>
|
||||
{I18n.t('Confirm')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// TODO 统一封装 localStorage 服务,管理本地缓存的生命周期
|
||||
export const DONT_SHOW_TIPS_LOCAL_CACHE_KEY =
|
||||
'model_capability_check_do_not_show_again';
|
||||
|
||||
export const checkModelAbility = (
|
||||
toolKeyConfigList: IRegisteredToolKeyConfig[],
|
||||
config: NonNullable<Model['func_config']>,
|
||||
): [AlertItem[], AlertItem[]] =>
|
||||
toolKeyConfigList.reduce<[AlertItem[], AlertItem[]]>(
|
||||
([notSupportedRes, poorSupportedRes], item) => {
|
||||
const { hasValidData, toolKey, toolGroupKey } = item;
|
||||
// 只在当前 tool 存在配置时才需要检查
|
||||
if (hasValidData) {
|
||||
const modelFunctionConfigType =
|
||||
abilityKey2ModelFunctionConfigType(toolKey);
|
||||
if (!modelFunctionConfigType) {
|
||||
return [notSupportedRes, poorSupportedRes];
|
||||
}
|
||||
let modelFunctionConfigStatus = config[modelFunctionConfigType];
|
||||
if (toolGroupKey === ToolGroupKey.KNOWLEDGE) {
|
||||
const { auto } = useBotSkillStore.getState().knowledge.dataSetInfo;
|
||||
if (toolGroupKey === ToolGroupKey.KNOWLEDGE) {
|
||||
const autoConfigStatus =
|
||||
config[
|
||||
auto
|
||||
? ModelFuncConfigType.KnowledgeAutoCall
|
||||
: ModelFuncConfigType.KnowledgeOnDemandCall
|
||||
];
|
||||
modelFunctionConfigStatus = mergeModelFuncConfigStatus(
|
||||
autoConfigStatus,
|
||||
modelFunctionConfigStatus,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const alertItem: AlertItem = {
|
||||
groupTitle: getToolGroupText(item.toolGroupKey),
|
||||
title: item.toolTitle ?? getToolText(item.toolKey),
|
||||
};
|
||||
if (modelFunctionConfigStatus === ModelFuncConfigStatus.NotSupport) {
|
||||
notSupportedRes.push(alertItem);
|
||||
}
|
||||
if (modelFunctionConfigStatus === ModelFuncConfigStatus.PoorSupport) {
|
||||
poorSupportedRes.push(alertItem);
|
||||
}
|
||||
}
|
||||
return [notSupportedRes, poorSupportedRes];
|
||||
},
|
||||
[[], []],
|
||||
);
|
||||
|
||||
export const confirm = ({
|
||||
notSupported,
|
||||
poorSupported,
|
||||
modelName,
|
||||
}: {
|
||||
notSupported: AlertItem[];
|
||||
poorSupported: AlertItem[];
|
||||
modelName: string;
|
||||
}): Promise<boolean> => {
|
||||
if (notSupported.length > 0 || poorSupported.length > 0) {
|
||||
return new Promise(resolve => {
|
||||
const modal = Modal.confirm({
|
||||
header: null,
|
||||
// 需要比模型配置的popover默认 z-index 1030 更高,这里进行内卷
|
||||
zIndex: 1031,
|
||||
mask: false,
|
||||
width: 480,
|
||||
onCancel: () => {
|
||||
resolve(false);
|
||||
modal.destroy();
|
||||
},
|
||||
content: (
|
||||
<ModelCapabilityAlertModelContent
|
||||
notSupported={notSupported}
|
||||
poorSupported={poorSupported}
|
||||
modelName={modelName}
|
||||
onOk={() => {
|
||||
resolve(true);
|
||||
modal.destroy();
|
||||
}}
|
||||
onCancel={() => {
|
||||
resolve(false);
|
||||
modal.destroy();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
footer: false,
|
||||
showCancelButton: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
const getGroupTittleByConfigType = (type: ModelFuncConfigType): string =>
|
||||
({
|
||||
[ModelFuncConfigType.Plugin]: I18n.t('bot_edit_type_skills'),
|
||||
[ModelFuncConfigType.Workflow]: I18n.t('bot_edit_type_skills'),
|
||||
[ModelFuncConfigType.ImageFlow]: I18n.t('bot_edit_type_skills'),
|
||||
[ModelFuncConfigType.Trigger]: I18n.t('bot_edit_type_skills'),
|
||||
[ModelFuncConfigType.KnowledgeText]: I18n.t('bot_edit_type_knowledge'),
|
||||
[ModelFuncConfigType.KnowledgeTable]: I18n.t('bot_edit_type_knowledge'),
|
||||
[ModelFuncConfigType.KnowledgePhoto]: I18n.t('bot_edit_type_knowledge'),
|
||||
[ModelFuncConfigType.KnowledgeAutoCall]: I18n.t('bot_edit_type_knowledge'),
|
||||
[ModelFuncConfigType.KnowledgeOnDemandCall]: I18n.t(
|
||||
'bot_edit_type_knowledge',
|
||||
),
|
||||
[ModelFuncConfigType.Variable]: I18n.t('bot_edit_type_memory'),
|
||||
[ModelFuncConfigType.Database]: I18n.t('bot_edit_type_memory'),
|
||||
[ModelFuncConfigType.LongTermMemory]: I18n.t('bot_edit_type_memory'),
|
||||
[ModelFuncConfigType.FileBox]: I18n.t('bot_edit_type_memory'),
|
||||
[ModelFuncConfigType.Onboarding]: I18n.t('bot_edit_type_dialog'),
|
||||
[ModelFuncConfigType.Suggestion]: I18n.t('bot_edit_type_dialog'),
|
||||
[ModelFuncConfigType.ShortcutCommand]: I18n.t('bot_edit_type_dialog'),
|
||||
[ModelFuncConfigType.BackGroundImage]: I18n.t('bot_edit_type_dialog'),
|
||||
[ModelFuncConfigType.TTS]: I18n.t('bot_edit_type_character'),
|
||||
[ModelFuncConfigType.MultiAgentRecognize]: I18n.t(
|
||||
'agentflow_transfer_ conversation_settings_title',
|
||||
),
|
||||
[ModelFuncConfigType.HookInfo]: 'Hooks',
|
||||
})[type];
|
||||
|
||||
const getTitleByConfigType = (type: ModelFuncConfigType): string =>
|
||||
({
|
||||
[ModelFuncConfigType.Plugin]: I18n.t('Plugins'),
|
||||
[ModelFuncConfigType.Workflow]: I18n.t('Workflows'),
|
||||
[ModelFuncConfigType.ImageFlow]: I18n.t('imageflow_title'),
|
||||
[ModelFuncConfigType.Trigger]: I18n.t('platfrom_triggers_title'),
|
||||
[ModelFuncConfigType.KnowledgeText]: I18n.t('dataset_detail_type_text'),
|
||||
[ModelFuncConfigType.KnowledgeTable]: I18n.t('dataset_detail_type_table'),
|
||||
[ModelFuncConfigType.KnowledgePhoto]: I18n.t('knowledge_photo_025'),
|
||||
[ModelFuncConfigType.KnowledgeAutoCall]: I18n.t('dataset_automatic_call'),
|
||||
[ModelFuncConfigType.KnowledgeOnDemandCall]: I18n.t(
|
||||
'dataset_on_demand_call',
|
||||
),
|
||||
[ModelFuncConfigType.Variable]: I18n.t('user_profile'),
|
||||
[ModelFuncConfigType.Database]: I18n.t('bot_database'),
|
||||
[ModelFuncConfigType.LongTermMemory]: I18n.t('timecapsule_1228_001'),
|
||||
[ModelFuncConfigType.FileBox]: I18n.t('Starling_filebox_name'),
|
||||
[ModelFuncConfigType.Onboarding]: I18n.t('bot_preview_opening_remarks'),
|
||||
[ModelFuncConfigType.Suggestion]: I18n.t('bot_edit_suggestion'),
|
||||
[ModelFuncConfigType.ShortcutCommand]: I18n.t('bot_ide_shortcut'),
|
||||
[ModelFuncConfigType.BackGroundImage]: I18n.t('bgi_title'),
|
||||
[ModelFuncConfigType.TTS]: I18n.t('bot_edit_voices_title'),
|
||||
[ModelFuncConfigType.MultiAgentRecognize]: I18n.t(
|
||||
'agentflow_transfer_ conversation_settings_mode_node_title',
|
||||
),
|
||||
[ModelFuncConfigType.HookInfo]: 'Hooks',
|
||||
})[type];
|
||||
|
||||
export const mapConfigTypeToAlertItem = (
|
||||
type: ModelFuncConfigType,
|
||||
): AlertItem => ({
|
||||
groupTitle: getGroupTittleByConfigType(type),
|
||||
title: getTitleByConfigType(type),
|
||||
});
|
||||
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode, useState } from 'react';
|
||||
|
||||
import { useRegisteredToolKeyConfigList } from '@coze-agent-ide/tool';
|
||||
import { type Agent } from '@coze-studio/bot-detail-store';
|
||||
import { useBotEditor } from '@coze-agent-ide/bot-editor-context-store';
|
||||
import { Modal } from '@coze-arch/coze-design';
|
||||
|
||||
import { agentModelFuncConfigCheck } from '../../utils/model-func-config-check/agent-check';
|
||||
import {
|
||||
checkModelAbility,
|
||||
DONT_SHOW_TIPS_LOCAL_CACHE_KEY,
|
||||
confirm,
|
||||
ModelCapabilityAlertModelContent,
|
||||
mapConfigTypeToAlertItem,
|
||||
type AlertItem,
|
||||
} from './base';
|
||||
|
||||
export const useModelCapabilityCheckAndConfirm = () => {
|
||||
const toolKeyConfigList = useRegisteredToolKeyConfigList();
|
||||
const {
|
||||
storeSet: { useModelStore },
|
||||
} = useBotEditor();
|
||||
|
||||
return async (modelId: string): Promise<boolean> => {
|
||||
if (localStorage.getItem(DONT_SHOW_TIPS_LOCAL_CACHE_KEY)) {
|
||||
return true;
|
||||
}
|
||||
const model = useModelStore.getState().getModelById(modelId);
|
||||
const functionConfig = model?.func_config;
|
||||
if (!functionConfig) {
|
||||
return true;
|
||||
}
|
||||
const [notSupported, poorSupported] = checkModelAbility(
|
||||
toolKeyConfigList,
|
||||
functionConfig,
|
||||
);
|
||||
return confirm({
|
||||
notSupported,
|
||||
poorSupported,
|
||||
modelName: model.name ?? '',
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function useModelCapabilityCheckModal({
|
||||
onOk,
|
||||
}: {
|
||||
/** 当新模型不满足配置时会自动弹窗确认,该参数是确认弹窗的回调 */
|
||||
onOk: (modelId: string) => void;
|
||||
}): {
|
||||
modalNode: ReactNode;
|
||||
/**
|
||||
* 检查新模型能力是否满足配置,不满足的话会自动弹出确认 modal
|
||||
* @returns true 代表满足
|
||||
*/
|
||||
checkAndOpenModal: (modelId: string) => boolean;
|
||||
} {
|
||||
const toolKeyConfigList = useRegisteredToolKeyConfigList();
|
||||
const {
|
||||
storeSet: { useModelStore },
|
||||
} = useBotEditor();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
// Q:单纯通过 modalState 是不是就能判断 modal 是否需要展示,modalVisible 有点多余?
|
||||
// A:这里将 visible 和 state 拆分是为了避免弹窗在关闭动画期间 state 数据变更导致弹窗内容跳变
|
||||
const [modalState, setModalState] = useState<
|
||||
| {
|
||||
notSupported: AlertItem[];
|
||||
poorSupported: AlertItem[];
|
||||
modelName: string;
|
||||
modelId: string;
|
||||
}
|
||||
| undefined
|
||||
>();
|
||||
|
||||
return {
|
||||
modalNode: modalState ? (
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
// 需要比模型配置的popover默认 z-index 1030 更高,这里进行内卷
|
||||
zIndex={1031}
|
||||
width={480}
|
||||
header={null}
|
||||
footer={null}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
>
|
||||
<ModelCapabilityAlertModelContent
|
||||
notSupported={modalState.notSupported}
|
||||
poorSupported={modalState.poorSupported}
|
||||
modelName={modalState.modelName}
|
||||
onOk={() => {
|
||||
onOk(modalState.modelId);
|
||||
setModalVisible(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setModalVisible(false);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
) : null,
|
||||
checkAndOpenModal: modelId => {
|
||||
if (localStorage.getItem(DONT_SHOW_TIPS_LOCAL_CACHE_KEY)) {
|
||||
return true;
|
||||
}
|
||||
const model = useModelStore.getState().getModelById(modelId);
|
||||
const functionConfig = model?.func_config;
|
||||
if (!functionConfig) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const [notSupported, poorSupported] = checkModelAbility(
|
||||
toolKeyConfigList,
|
||||
functionConfig,
|
||||
);
|
||||
if (notSupported.length === 0 && poorSupported.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
setModalVisible(true);
|
||||
setModalState({
|
||||
notSupported,
|
||||
poorSupported,
|
||||
modelId,
|
||||
modelName: model?.name ?? '',
|
||||
});
|
||||
return false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const useAgentModelCapabilityCheckAndAlert = () => {
|
||||
const toolKeyConfigList = useRegisteredToolKeyConfigList();
|
||||
const {
|
||||
storeSet: { useModelStore, useDraftBotDataSetStore },
|
||||
} = useBotEditor();
|
||||
return async (modelId: string, agent: Agent) => {
|
||||
if (localStorage.getItem(DONT_SHOW_TIPS_LOCAL_CACHE_KEY)) {
|
||||
return true;
|
||||
}
|
||||
const model = useModelStore.getState().getModelById(modelId);
|
||||
const config = model?.func_config;
|
||||
if (!config) {
|
||||
return true;
|
||||
}
|
||||
const [commonNotSupported, commonPoorSupported] = checkModelAbility(
|
||||
toolKeyConfigList,
|
||||
config,
|
||||
);
|
||||
|
||||
const { notSupported, poorSupported } = agentModelFuncConfigCheck({
|
||||
config,
|
||||
agent,
|
||||
context: {
|
||||
getDatasetById: id =>
|
||||
useDraftBotDataSetStore.getState().datasetsMap[id],
|
||||
config,
|
||||
},
|
||||
});
|
||||
return confirm({
|
||||
notSupported: [
|
||||
...commonNotSupported,
|
||||
...notSupported.map(mapConfigTypeToAlertItem),
|
||||
],
|
||||
poorSupported: [
|
||||
...commonPoorSupported,
|
||||
...poorSupported.map(mapConfigTypeToAlertItem),
|
||||
],
|
||||
modelName: model.name ?? '',
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function useAgentModelCapabilityCheckModal({
|
||||
onOk,
|
||||
}: {
|
||||
/** 当新模型不满足配置时会自动弹窗确认,该参数是确认弹窗的回调 */
|
||||
onOk: (modelId: string) => void;
|
||||
}): {
|
||||
modalNode: ReactNode;
|
||||
/**
|
||||
* 检查新模型能力是否满足配置,不满足的话会自动弹出确认 modal
|
||||
* @returns true 代表满足
|
||||
*/
|
||||
checkAndOpenModal: (modelId: string, agent: Agent) => boolean;
|
||||
} {
|
||||
const toolKeyConfigList = useRegisteredToolKeyConfigList();
|
||||
const {
|
||||
storeSet: { useModelStore, useDraftBotDataSetStore },
|
||||
} = useBotEditor();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
// Q:单纯通过 modalState 是不是就能判断 modal 是否需要展示,modalVisible 有点多余?
|
||||
// A:这里将 visible 和 state 拆分是为了避免弹窗在关闭动画期间 state 数据变更导致弹窗内容跳变
|
||||
const [modalState, setModalState] = useState<
|
||||
| {
|
||||
notSupported: AlertItem[];
|
||||
poorSupported: AlertItem[];
|
||||
modelName: string;
|
||||
modelId: string;
|
||||
}
|
||||
| undefined
|
||||
>();
|
||||
|
||||
return {
|
||||
modalNode: modalState ? (
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
// 需要比模型配置的popover默认 z-index 1030 更高,这里进行内卷
|
||||
zIndex={1031}
|
||||
width={480}
|
||||
header={null}
|
||||
footer={null}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
>
|
||||
<ModelCapabilityAlertModelContent
|
||||
notSupported={modalState.notSupported}
|
||||
poorSupported={modalState.poorSupported}
|
||||
modelName={modalState.modelName}
|
||||
onOk={() => {
|
||||
onOk(modalState.modelId);
|
||||
setModalVisible(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setModalVisible(false);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
) : null,
|
||||
checkAndOpenModal: (modelId, agent) => {
|
||||
if (localStorage.getItem(DONT_SHOW_TIPS_LOCAL_CACHE_KEY)) {
|
||||
return true;
|
||||
}
|
||||
const model = useModelStore.getState().getModelById(modelId);
|
||||
const config = model?.func_config;
|
||||
if (!config) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const [commonNotSupported, commonPoorSupported] = checkModelAbility(
|
||||
toolKeyConfigList,
|
||||
config,
|
||||
);
|
||||
|
||||
const { notSupported, poorSupported } = agentModelFuncConfigCheck({
|
||||
config,
|
||||
agent,
|
||||
context: {
|
||||
getDatasetById: id =>
|
||||
useDraftBotDataSetStore.getState().datasetsMap[id],
|
||||
config,
|
||||
},
|
||||
});
|
||||
|
||||
if (notSupported.length === 0 && poorSupported.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
setModalVisible(true);
|
||||
setModalState({
|
||||
notSupported: [
|
||||
...commonNotSupported,
|
||||
...notSupported.map(mapConfigTypeToAlertItem),
|
||||
],
|
||||
poorSupported: [
|
||||
...commonPoorSupported,
|
||||
...poorSupported.map(mapConfigTypeToAlertItem),
|
||||
],
|
||||
modelId,
|
||||
modelName: model?.name ?? '',
|
||||
});
|
||||
return false;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
.form-item {
|
||||
margin-bottom: 18px;
|
||||
|
||||
.field-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.label-content {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
column-gap: 4px;
|
||||
align-items: center;
|
||||
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
/* stylelint-disable-next-line custom-property-pattern */
|
||||
color: var(--Fg-Primary-COZ_fg_secondary, rgba(6, 7, 9, 50%));
|
||||
|
||||
.label {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.field-main {
|
||||
width: 280px;
|
||||
|
||||
:global(.semi-select) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: #6B6D75;
|
||||
}
|
||||
|
||||
.field-feedback {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
|
||||
@apply coz-fg-secondary;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type PropsWithChildren, type ReactNode } from 'react';
|
||||
|
||||
import { useField } from '@formily/react';
|
||||
import { type Field } from '@formily/core';
|
||||
import { Popover } from '@coze-arch/bot-semi';
|
||||
import { MdBoxLazy } from '@coze-arch/bot-md-box-adapter/lazy';
|
||||
import { IconInfo } from '@coze-arch/bot-icons';
|
||||
|
||||
import commonStyles from '../index.module.less';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface ModelFormItemProps {
|
||||
label: ReactNode | undefined;
|
||||
popoverContent: string | undefined;
|
||||
}
|
||||
|
||||
export const ModelFormItem: React.FC<PropsWithChildren<ModelFormItemProps>> = ({
|
||||
label,
|
||||
popoverContent,
|
||||
children,
|
||||
}) => {
|
||||
const field = useField<Field>();
|
||||
|
||||
return (
|
||||
<div className={styles['form-item']}>
|
||||
<div className={styles['field-content']}>
|
||||
<label className={styles['label-content']}>
|
||||
<span className={styles.label}>{label}</span>
|
||||
{popoverContent ? (
|
||||
<Popover
|
||||
className={commonStyles.popover}
|
||||
showArrow
|
||||
arrowPointAtCenter
|
||||
content={
|
||||
<MdBoxLazy
|
||||
markDown={popoverContent}
|
||||
autoFixSyntax={{ autoFixEnding: false }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<IconInfo className={styles.icon} />
|
||||
</Popover>
|
||||
) : null}
|
||||
</label>
|
||||
<div className={styles['field-main']}>{children}</div>
|
||||
</div>
|
||||
{field?.feedbacks?.map((feedback, index) => (
|
||||
<p key={index} className={styles['field-feedback']}>
|
||||
{feedback.messages}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
.content {
|
||||
width: 100%;
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
.group {
|
||||
width: 100%;
|
||||
padding-top: 12px;
|
||||
|
||||
&:last-child {
|
||||
.content {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group-with-collapse {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.generation-diversity-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.group-label {
|
||||
margin-bottom: 4px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
/* stylelint-disable-next-line custom-property-pattern */
|
||||
color: var(--Fg-Primary-COZ_fg_plus, rgba(6, 7, 9, 96%));
|
||||
}
|
||||
|
||||
.diversity-label {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
column-gap: 8px;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
|
||||
.icon {
|
||||
font-size: 14px;
|
||||
// 转换自 color: #060709" opacity="0.5"
|
||||
color: #828384;
|
||||
}
|
||||
}
|
||||
|
||||
.rotate {
|
||||
svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.advance {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
align-self: center;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
type PropsWithChildren,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import classNames from 'classnames';
|
||||
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozArrowDown } from '@coze-arch/coze-design/icons';
|
||||
import { Button, Collapsible } from '@coze-arch/coze-design';
|
||||
import { Popover } from '@coze-arch/bot-semi';
|
||||
import { MdBoxLazy } from '@coze-arch/bot-md-box-adapter/lazy';
|
||||
import { IconInfo } from '@coze-arch/bot-icons';
|
||||
import { ModelStyle } from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
import { PresetRadioGroup } from '../preset-radio-group';
|
||||
import commonStyles from '../index.module.less';
|
||||
import { useModelForm } from '../../../context/model-form-context';
|
||||
import {
|
||||
type FormilyCoreType,
|
||||
type FormilyReactType,
|
||||
} from '../../../context/formily-context/type';
|
||||
import { useFormily } from '../../../context/formily-context';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface ModelFormGroupItemProps {
|
||||
title: ReactNode | undefined;
|
||||
}
|
||||
|
||||
export const ModelFormGroupItem: React.FC<
|
||||
PropsWithChildren<ModelFormGroupItemProps>
|
||||
> = ({ title, children }) => (
|
||||
<div className={styles.group}>
|
||||
<div className={classNames(styles['group-label'], styles.title)}>
|
||||
{title}
|
||||
</div>
|
||||
<div className={styles.content}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
type DiversityGroupItemImplProps = ModelFormGroupItemProps & {
|
||||
disabled: boolean;
|
||||
formilyReact: FormilyReactType;
|
||||
formilyCore: FormilyCoreType;
|
||||
};
|
||||
|
||||
const DiversityGroupItemImpl: React.FC<
|
||||
PropsWithChildren<DiversityGroupItemImplProps>
|
||||
> = ({ formilyCore, formilyReact, title, children, disabled }) => {
|
||||
const form = formilyReact.useForm();
|
||||
const {
|
||||
isGenerationDiversityOpen,
|
||||
setGenerationDiversityOpen,
|
||||
hideDiversityCollapseButton,
|
||||
} = useModelForm();
|
||||
|
||||
const [modelStyle, setModelStyle] = useState<ModelStyle>(
|
||||
form.values.model_style,
|
||||
);
|
||||
|
||||
const toggleOpen = () => {
|
||||
setGenerationDiversityOpen(!isGenerationDiversityOpen);
|
||||
};
|
||||
|
||||
const handleValuesChange = (changedValue: ModelStyle) => {
|
||||
form.setValues({ model_style: changedValue });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const effectId = nanoid();
|
||||
|
||||
form.addEffects(effectId, () => {
|
||||
formilyCore.onFormValuesChange(localeForm => {
|
||||
setModelStyle(localeForm.values.model_style);
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
form.removeEffects(effectId);
|
||||
};
|
||||
}, [form]);
|
||||
|
||||
/**
|
||||
* 这里区分一下初始化和后续操作
|
||||
* 只读状态时 customize 也默认收起否则默认展开
|
||||
* 当然只读状态无法修改 model_style
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (modelStyle !== ModelStyle.Custom) {
|
||||
return;
|
||||
}
|
||||
setGenerationDiversityOpen(true);
|
||||
}, [modelStyle]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.group,
|
||||
!isGenerationDiversityOpen && styles['group-with-collapse'],
|
||||
)}
|
||||
>
|
||||
<div className={styles['generation-diversity-group']}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles['group-label'],
|
||||
styles['diversity-label'],
|
||||
)}
|
||||
>
|
||||
<div>{title}</div>
|
||||
<Popover
|
||||
className={commonStyles.popover}
|
||||
showArrow
|
||||
arrowPointAtCenter
|
||||
content={
|
||||
<MdBoxLazy
|
||||
markDown={I18n.t('model_config_generate_explain')}
|
||||
autoFixSyntax={{ autoFixEnding: false }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<IconInfo className={styles.icon} />
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<PresetRadioGroup
|
||||
className={styles['radio-group']}
|
||||
disabled={disabled}
|
||||
value={modelStyle}
|
||||
onChange={handleValuesChange}
|
||||
/>
|
||||
{hideDiversityCollapseButton ? null : (
|
||||
<Button
|
||||
onClick={toggleOpen}
|
||||
iconPosition="right"
|
||||
icon={<IconCozArrowDown />}
|
||||
className={classNames(
|
||||
isGenerationDiversityOpen && styles.rotate,
|
||||
styles.advance,
|
||||
)}
|
||||
color="secondary"
|
||||
>
|
||||
{I18n.t('model_config_generate_advance')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Collapsible
|
||||
isOpen={isGenerationDiversityOpen}
|
||||
className={styles.collapse}
|
||||
>
|
||||
<div className={styles.content}>{children}</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModelFormGenerationDiversityGroupItem: React.FC<
|
||||
PropsWithChildren<ModelFormGroupItemProps>
|
||||
> = props => {
|
||||
const {
|
||||
formilyModule: { formilyReact, formilyCore },
|
||||
} = useFormily();
|
||||
|
||||
const isReadonly = useBotDetailIsReadonly();
|
||||
|
||||
if (!formilyReact || !formilyCore) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DiversityGroupItemImpl
|
||||
formilyCore={formilyCore}
|
||||
formilyReact={formilyReact}
|
||||
disabled={isReadonly}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
.model-select-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 4px;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
|
||||
margin-bottom: 18px;
|
||||
|
||||
>span {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
line-height: 24px;
|
||||
/* stylelint-disable-next-line custom-property-pattern */
|
||||
color: var(--Fg-Primary-COZ_fg_plus);
|
||||
}
|
||||
}
|
||||
|
||||
.popover {
|
||||
max-width: 224px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.error-state {
|
||||
display: flex;
|
||||
column-gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
height: 100%;
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
|
||||
.fail-page-icon {
|
||||
color: #FF2710;
|
||||
}
|
||||
|
||||
.fail-page-text {
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.fail-page-retry {
|
||||
cursor: pointer;
|
||||
color: #4D53E8;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useCreation } from 'ahooks';
|
||||
import { type ISchema } from '@formily/react';
|
||||
import { type Form } from '@formily/core';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozWarningCircleFill } from '@coze-arch/coze-design/icons';
|
||||
import { Loading } from '@coze-arch/coze-design';
|
||||
|
||||
import { primitiveExhaustiveCheck } from '../../utils/exhaustive-check';
|
||||
import {
|
||||
type FormilyReactType,
|
||||
type FormilyCoreType,
|
||||
} from '../../context/formily-context/type';
|
||||
import { useFormily } from '../../context/formily-context';
|
||||
import { modelFormComponentMap } from './type';
|
||||
import { ModelSelect } from './model-select';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface ModelFormProps {
|
||||
currentModelId: string | undefined;
|
||||
onModelChange: (value: string) => void;
|
||||
onFormInit: (form: Form, formilyCore: FormilyCoreType) => void;
|
||||
onFormUnmount: () => void;
|
||||
schema: ISchema | undefined;
|
||||
}
|
||||
|
||||
interface ModelFormImplProps extends ModelFormProps {
|
||||
formilyCore: FormilyCoreType;
|
||||
formilyReact: FormilyReactType;
|
||||
}
|
||||
|
||||
const ModelFormImpl: React.FC<ModelFormImplProps> = ({
|
||||
formilyCore,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- 此规则不适用这个 case
|
||||
formilyReact: { createSchemaField, FormProvider },
|
||||
currentModelId,
|
||||
onModelChange,
|
||||
onFormInit,
|
||||
onFormUnmount,
|
||||
schema,
|
||||
}) => {
|
||||
const { createForm } = formilyCore;
|
||||
|
||||
const form = useCreation(() => createForm(), [currentModelId]);
|
||||
|
||||
const SchemaField = useCreation(
|
||||
() => createSchemaField({ components: modelFormComponentMap }),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// 在 promise executor 中执行回调,其中的错误会异步产生 promise rejection ,而不是导致页面白屏
|
||||
new Promise(() => onFormInit(form, formilyCore));
|
||||
|
||||
return onFormUnmount;
|
||||
}, [form]);
|
||||
|
||||
return (
|
||||
<FormProvider form={form}>
|
||||
<div>
|
||||
<div className={styles['model-select-wrapper']}>
|
||||
<span>{I18n.t('model_config_model')}</span>
|
||||
<ModelSelect value={currentModelId} onChange={onModelChange} />
|
||||
</div>
|
||||
<SchemaField schema={schema} />
|
||||
</div>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModelForm: React.FC<ModelFormProps> = ({
|
||||
currentModelId,
|
||||
onModelChange,
|
||||
onFormInit,
|
||||
schema,
|
||||
onFormUnmount,
|
||||
}) => {
|
||||
const { formilyModule, retryImportFormily } = useFormily();
|
||||
|
||||
if (formilyModule.status === 'loading' || formilyModule.status === 'unInit') {
|
||||
return <Loading loading />;
|
||||
}
|
||||
|
||||
if (formilyModule.status === 'error') {
|
||||
return (
|
||||
<div className={styles['error-state']}>
|
||||
<IconCozWarningCircleFill className={styles['fail-page-icon']} />
|
||||
<div className={styles['fail-page-text']}>
|
||||
<span>{I18n.t('model_form_fail_text')}</span>
|
||||
<span
|
||||
className={styles['fail-page-retry']}
|
||||
onClick={retryImportFormily}
|
||||
>
|
||||
{I18n.t('model_form_fail_retry')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (formilyModule.status === 'ready') {
|
||||
const { formilyCore, formilyReact } = formilyModule;
|
||||
return (
|
||||
<ModelFormImpl
|
||||
schema={schema}
|
||||
formilyCore={formilyCore}
|
||||
formilyReact={formilyReact}
|
||||
currentModelId={currentModelId}
|
||||
onModelChange={onModelChange}
|
||||
onFormInit={onFormInit}
|
||||
onFormUnmount={onFormUnmount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
primitiveExhaustiveCheck(formilyModule.status);
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
.label-content {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
padding-left: 8px;
|
||||
|
||||
}
|
||||
|
||||
.model-content {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
column-gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
|
||||
|
||||
.model-content-icon {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.select {
|
||||
width: 100%;
|
||||
|
||||
:global(.semi-select-content-wrapper) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.model-token {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.model-name {
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { type CSSProperties } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { type Model } from '@coze-arch/bot-api/developer_api';
|
||||
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
|
||||
import { useBotEditor } from '@coze-agent-ide/bot-editor-context-store';
|
||||
|
||||
import { UIModelSelect } from './ui-model-select';
|
||||
|
||||
const getModelOptionList = ({
|
||||
onlineModelList,
|
||||
offlineModelMap,
|
||||
currentModelId,
|
||||
}: {
|
||||
onlineModelList: Model[];
|
||||
offlineModelMap: Record<string, Model>;
|
||||
currentModelId: string | undefined;
|
||||
}) => {
|
||||
if (!currentModelId) {
|
||||
return onlineModelList;
|
||||
}
|
||||
const specialModel = offlineModelMap[currentModelId];
|
||||
if (!specialModel) {
|
||||
return onlineModelList;
|
||||
}
|
||||
return onlineModelList.concat([specialModel]);
|
||||
};
|
||||
|
||||
export interface ModelSelectProps {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
value: string | undefined;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export const ModelSelect: React.FC<ModelSelectProps> = ({
|
||||
value,
|
||||
...restProps
|
||||
}) => {
|
||||
const {
|
||||
storeSet: { useModelStore },
|
||||
} = useBotEditor();
|
||||
const { onlineModelList, offlineModelMap } = useModelStore(
|
||||
useShallow(state => ({
|
||||
onlineModelList: state.onlineModelList,
|
||||
offlineModelMap: state.offlineModelMap,
|
||||
})),
|
||||
);
|
||||
|
||||
const isReadonly = useBotDetailIsReadonly();
|
||||
|
||||
// 用户从特殊模型切换到正常模型后, 可选项列表将发生变化,于是用户再也切换不回去了
|
||||
const modelList = getModelOptionList({
|
||||
onlineModelList,
|
||||
offlineModelMap,
|
||||
currentModelId: value,
|
||||
});
|
||||
|
||||
return (
|
||||
<UIModelSelect
|
||||
modelList={modelList}
|
||||
disabled={isReadonly}
|
||||
value={value}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, type CSSProperties } from 'react';
|
||||
|
||||
import { groupBy } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { ModelOptionItem } from '@coze-studio/components';
|
||||
import { Select } from '@coze-arch/coze-design';
|
||||
import { type OptionProps } from '@coze-arch/bot-semi/Select';
|
||||
import { type ModelTag, type Model } from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
import { getModelClassSortList } from '../../../utils/model/get-model-class-sort-list';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface UIModelSelectProps {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
value: string | undefined;
|
||||
onChange?: (value: string) => void;
|
||||
modelList: Model[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const UIModelSelect: React.FC<UIModelSelectProps> = ({
|
||||
className,
|
||||
style,
|
||||
value,
|
||||
onChange,
|
||||
modelList = [],
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
// 专业版有项目维度区分class,modal_class 与 modal_class_name存在一对多的情况,因此统一以model_class_name做分组
|
||||
// 以 modal_class_name 首次出现的顺序进行排序
|
||||
const modelClassSortList = getModelClassSortList(
|
||||
modelList.map(i => i.model_class_name ?? ''),
|
||||
);
|
||||
|
||||
const modelClassGroup = groupBy(modelList, model => model.model_class_name);
|
||||
const showEndPointName = modelList.some(model => model.endpoint_name);
|
||||
|
||||
// 搜索规则: 模型名称/接入点名称包含关键词(不区分大小写)
|
||||
const filterOption = (model: Model) => {
|
||||
const sugInput = inputValue.toUpperCase();
|
||||
return (
|
||||
model.name?.toUpperCase()?.includes(sugInput) ||
|
||||
model?.endpoint_name?.toUpperCase()?.includes(sugInput)
|
||||
);
|
||||
};
|
||||
|
||||
const getModelOptionLabel = (model: Model) => (
|
||||
<ModelOptionItem
|
||||
key={model.model_type}
|
||||
tokenLimit={model.model_quota?.token_limit}
|
||||
avatar={model.model_icon}
|
||||
name={model.name}
|
||||
descriptionGroupList={model.model_desc}
|
||||
searchWords={[inputValue]}
|
||||
endPointName={model?.endpoint_name}
|
||||
showEndPointName={showEndPointName}
|
||||
tags={model.model_tag_list
|
||||
?.filter(
|
||||
(item): item is ModelTag & Required<Pick<ModelTag, 'tag_name'>> =>
|
||||
!!item.tag_name,
|
||||
)
|
||||
.map(item => ({
|
||||
label: item.tag_name,
|
||||
color: 'yellow',
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
|
||||
const optionsList = modelClassSortList
|
||||
.map(stringClassId => {
|
||||
const groupMemberList = modelClassGroup[stringClassId];
|
||||
return {
|
||||
label: groupMemberList?.at(0)?.model_class_name,
|
||||
children: groupMemberList
|
||||
?.filter(model => filterOption(model))
|
||||
?.map(model => ({
|
||||
label: getModelOptionLabel(model),
|
||||
value: model.model_type?.toString(),
|
||||
})),
|
||||
};
|
||||
})
|
||||
.map(group => (
|
||||
// 修改key原因:详见https://semi.design/zh-CN/input/select - 分组模块
|
||||
// 1. 分组能力只能使用jsx
|
||||
// 2. 若Select的children需要动态更新,OptGroup上的key也需要进行更新,否则Select无法识别
|
||||
<Select.OptGroup key={`${inputValue}-${group.label}`} label={group.label}>
|
||||
{group.children?.map(option => (
|
||||
<Select.Option value={option.value} key={option.value}>
|
||||
{option.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select.OptGroup>
|
||||
));
|
||||
|
||||
if (modelList.length === 1) {
|
||||
const targetModel = modelList.at(0);
|
||||
return targetModel ? getModelOptionLabel(targetModel) : null;
|
||||
}
|
||||
|
||||
const renderSelectedItem = (optionNode: OptionProps) =>
|
||||
React.isValidElement(optionNode?.children) ? (
|
||||
<ModelOptionItem
|
||||
{...optionNode?.children?.props}
|
||||
showEndPointName={false}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<Select
|
||||
clickToHide
|
||||
disabled={disabled}
|
||||
className={classNames(styles.select, className)}
|
||||
style={style}
|
||||
value={value}
|
||||
dropdownClassName="max-w-[432px]"
|
||||
onChange={changedValue => {
|
||||
if (typeof changedValue === 'string') {
|
||||
onChange?.(changedValue);
|
||||
}
|
||||
}}
|
||||
onSearch={v => setInputValue(v)}
|
||||
filter
|
||||
renderSelectedItem={renderSelectedItem}
|
||||
>
|
||||
{optionsList}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
.button-radio {
|
||||
display: flex;
|
||||
|
||||
:global {
|
||||
.semi-radio {
|
||||
flex: 1;
|
||||
|
||||
.semi-radio-content {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type CSSProperties } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { RadioGroup } from '@coze-arch/bot-semi';
|
||||
import { ModelStyle } from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
import styles from './index.module.less';
|
||||
export interface PresetRadioOption {
|
||||
label: string;
|
||||
value: ModelStyle;
|
||||
}
|
||||
|
||||
const getOptions: () => PresetRadioOption[] = () => [
|
||||
{ label: I18n.t('model_config_generate_precise'), value: ModelStyle.Precise },
|
||||
{ label: I18n.t('model_config_generate_balance'), value: ModelStyle.Balance },
|
||||
{
|
||||
label: I18n.t('model_config_generate_creative'),
|
||||
value: ModelStyle.Creative,
|
||||
},
|
||||
{
|
||||
label: I18n.t('model_config_generate_customize'),
|
||||
value: ModelStyle.Custom,
|
||||
},
|
||||
];
|
||||
|
||||
export interface PresetRadioGroupProps {
|
||||
onChange: (value: ModelStyle) => void;
|
||||
value: ModelStyle;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const PresetRadioGroup: React.FC<PresetRadioGroupProps> = ({
|
||||
onChange,
|
||||
className,
|
||||
style,
|
||||
value,
|
||||
disabled,
|
||||
}) => (
|
||||
<RadioGroup
|
||||
disabled={disabled}
|
||||
className={classNames(styles['button-radio'], className)}
|
||||
style={style}
|
||||
options={getOptions()}
|
||||
value={value}
|
||||
onChange={e => {
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
type="button"
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type JSXComponent } from '@formily/react';
|
||||
import { InputSlider, type InputSliderProps } from '@coze-studio/components';
|
||||
import {
|
||||
Switch,
|
||||
RadioGroup,
|
||||
type RadioGroupProps,
|
||||
} from '@coze-arch/coze-design';
|
||||
import { UIInput } from '@coze-arch/bot-semi';
|
||||
|
||||
import {
|
||||
ModelFormComponent,
|
||||
ModelFormVoidFieldComponent,
|
||||
} from '../../constant/model-form-component';
|
||||
import {
|
||||
ModelFormGenerationDiversityGroupItem,
|
||||
ModelFormGroupItem,
|
||||
} from './group-item';
|
||||
import { ModelFormItem, type ModelFormItemProps } from './form-item';
|
||||
|
||||
export const modelFormComponentMap: Record<
|
||||
ModelFormComponent | ModelFormVoidFieldComponent,
|
||||
JSXComponent
|
||||
> = {
|
||||
[ModelFormComponent.Input]: UIInput,
|
||||
[ModelFormComponent.RadioButton]: RadioGroup,
|
||||
[ModelFormComponent.Switch]: Switch,
|
||||
[ModelFormComponent.SliderInputNumber]: InputSlider,
|
||||
[ModelFormComponent.ModelFormItem]: ModelFormItem,
|
||||
[ModelFormVoidFieldComponent.ModelFormGroupItem]: ModelFormGroupItem,
|
||||
[ModelFormVoidFieldComponent.ModelFormGenerationDiversityGroupItem]:
|
||||
ModelFormGenerationDiversityGroupItem,
|
||||
};
|
||||
|
||||
export interface ModelFormComponentPropsMap {
|
||||
[ModelFormComponent.Input]: Record<string, never>;
|
||||
[ModelFormComponent.RadioButton]: Pick<RadioGroupProps, 'options' | 'type'>;
|
||||
[ModelFormComponent.Switch]: Record<string, never>;
|
||||
[ModelFormComponent.SliderInputNumber]: Pick<
|
||||
InputSliderProps,
|
||||
'min' | 'max' | 'step' | 'decimalPlaces'
|
||||
>;
|
||||
[ModelFormComponent.ModelFormItem]: ModelFormItemProps;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { ModelOption, ModelOptionProps } from './model-option';
|
||||
export { ModelOptionGroup, ModelOptionGroupProps } from './model-option-group';
|
||||
export { ModelOptionThumb } from './model-option-thumb';
|
||||
export { ModelSelectUI, ModelSelectUIProps } from './model-select-ui';
|
||||
export { ModelSelect, ModelSelectProps } from './model-select';
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
IconCozRoleFill,
|
||||
IconCozLightbulbFill,
|
||||
IconCozChatFill,
|
||||
IconCozCodeFill,
|
||||
IconCozDocumentFill,
|
||||
IconCozImageFill,
|
||||
IconCozLightningFill,
|
||||
IconCozMusic,
|
||||
IconCozStarFill,
|
||||
IconCozVideoFill,
|
||||
IconCozWrenchFill,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { Avatar, Tooltip } from '@coze-arch/coze-design';
|
||||
import { type Model, ModelTagValue } from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
export function ModelOptionAvatar({ model }: { model: Model }) {
|
||||
return (
|
||||
<Tooltip
|
||||
trigger={
|
||||
model.model_status_details?.is_upcoming_deprecated ? 'hover' : 'custom'
|
||||
}
|
||||
content={I18n.t('model_list_model_deprecation_date', {
|
||||
date: model.model_status_details?.deprecated_date,
|
||||
})}
|
||||
>
|
||||
<span>
|
||||
<InnerImg
|
||||
model={model}
|
||||
bottomBanner={
|
||||
model.model_status_details?.is_upcoming_deprecated ? (
|
||||
<div
|
||||
className={cls(
|
||||
'absolute bottom-0 left-0',
|
||||
'w-full py-[1px] px-[3px] rounded-b-[6px]',
|
||||
'flex items-center justify-center text-center',
|
||||
'text-[10px] font-medium leading-[14px] break-all',
|
||||
'coz-mg-mask coz-fg-hglt-plus',
|
||||
)}
|
||||
>
|
||||
{I18n.t('model_list_willDeprecated')}
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function InnerImg({
|
||||
model: { model_status_details, model_icon },
|
||||
bottomBanner,
|
||||
}: {
|
||||
model: Model;
|
||||
bottomBanner?: ReactNode;
|
||||
}) {
|
||||
if (
|
||||
model_status_details?.is_new_model ||
|
||||
!model_status_details?.model_feature
|
||||
) {
|
||||
return (
|
||||
<Avatar
|
||||
className="shrink-0 rounded-[6px] border border-solid coz-stroke-primary"
|
||||
shape="square"
|
||||
// @ts-expect-error -- semi 类型定义有问题
|
||||
bottomSlot={
|
||||
bottomBanner
|
||||
? {
|
||||
render: () => bottomBanner,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
src={model_icon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const featureIcon = FEATURE_ICON_MAP[model_status_details.model_feature];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
'w-[48px] h-[48px] p-[13px] relative',
|
||||
'shrink-0 rounded-[6px] text-[22px]',
|
||||
featureIcon.color,
|
||||
featureIcon.bg,
|
||||
)}
|
||||
>
|
||||
{featureIcon.icon}
|
||||
{bottomBanner}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const FEATURE_ICON_MAP: Record<
|
||||
ModelTagValue,
|
||||
{ color: string; bg: string; icon: ReactNode }
|
||||
> = {
|
||||
[ModelTagValue.Flagship]: {
|
||||
icon: <IconCozStarFill />, //旗舰
|
||||
color: 'coz-fg-color-brand',
|
||||
bg: 'coz-mg-hglt',
|
||||
},
|
||||
[ModelTagValue.HighSpeed]: {
|
||||
icon: <IconCozLightningFill />, //高速
|
||||
color: 'coz-fg-color-blue',
|
||||
bg: 'coz-mg-color-blue',
|
||||
},
|
||||
[ModelTagValue.CostPerformance]: {
|
||||
icon: <IconCozChatFill />, //性价比
|
||||
color: 'coz-fg-color-blue',
|
||||
bg: 'coz-mg-color-blue',
|
||||
},
|
||||
[ModelTagValue.LongText]: {
|
||||
icon: <IconCozDocumentFill />, //长文本
|
||||
color: 'coz-fg-color-blue',
|
||||
bg: 'coz-mg-color-blue',
|
||||
},
|
||||
[ModelTagValue.RolePlaying]: {
|
||||
icon: <IconCozRoleFill />, //角色扮演
|
||||
color: 'coz-fg-color-blue',
|
||||
bg: 'coz-mg-color-blue',
|
||||
},
|
||||
[ModelTagValue.ImageUnderstanding]: {
|
||||
icon: <IconCozImageFill />, //图像
|
||||
color: 'coz-fg-color-purple',
|
||||
bg: 'coz-mg-color-purple',
|
||||
},
|
||||
[ModelTagValue.VideoUnderstanding]: {
|
||||
icon: <IconCozVideoFill />, //视频
|
||||
color: 'coz-fg-color-purple',
|
||||
bg: 'coz-mg-color-purple',
|
||||
},
|
||||
[ModelTagValue.AudioUnderstanding]: {
|
||||
icon: <IconCozMusic />, //音频
|
||||
color: 'coz-fg-color-purple',
|
||||
bg: 'coz-mg-color-purple',
|
||||
},
|
||||
[ModelTagValue.CodeSpecialization]: {
|
||||
icon: <IconCozCodeFill />, //代码专精
|
||||
color: 'coz-fg-color-cyan',
|
||||
bg: 'coz-mg-color-cyan',
|
||||
},
|
||||
[ModelTagValue.ToolInvocation]: {
|
||||
icon: <IconCozWrenchFill />, //工具调用
|
||||
color: 'coz-fg-color-cyan',
|
||||
bg: 'coz-mg-color-cyan',
|
||||
},
|
||||
[ModelTagValue.Reasoning]: {
|
||||
icon: <IconCozLightbulbFill />, //推理
|
||||
color: 'coz-fg-color-cyan',
|
||||
bg: 'coz-mg-color-cyan',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type PropsWithChildren } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import {
|
||||
IconCozStarFill,
|
||||
IconCozQuestionMarkCircle,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { Avatar, Divider, Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
export type ModelOptionGroupProps =
|
||||
| {
|
||||
/** 新模型专区 */
|
||||
type: 'new';
|
||||
name: string;
|
||||
tips?: string;
|
||||
}
|
||||
| {
|
||||
/** 普通系列模型 */
|
||||
type?: 'normal';
|
||||
icon: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
tips?: string;
|
||||
};
|
||||
|
||||
export function ModelOptionGroup({
|
||||
children,
|
||||
...props
|
||||
}: PropsWithChildren<ModelOptionGroupProps>) {
|
||||
return (
|
||||
<section>
|
||||
<div className="pt-[12px] pl-[16px] pb-[2px]">
|
||||
{props.type === 'new' ? (
|
||||
<div className="flex items-center gap-[4px] coz-fg-hglt">
|
||||
<IconCozStarFill />
|
||||
<span className="text-[12px] leading-[16px]">{props.name}</span>
|
||||
{props.tips ? (
|
||||
<Tooltip content={props.tips}>
|
||||
<IconCozQuestionMarkCircle className="cursor-pointer coz-fg-secondary" />
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-[6px]">
|
||||
<Avatar
|
||||
shape="square"
|
||||
className="w-[14px] h-[14px] rounded-[3px] !cursor-default border border-solid coz-stroke-primary"
|
||||
src={props.icon}
|
||||
/>
|
||||
<div
|
||||
className={cls(
|
||||
'flex items-center gap-[4px]',
|
||||
'text-[12px] leading-[16px]',
|
||||
)}
|
||||
>
|
||||
<span className="coz-fg-secondary">{props.name}</span>
|
||||
{props.tips ? (
|
||||
<Tooltip content={props.tips}>
|
||||
<IconCozQuestionMarkCircle className="cursor-pointer coz-fg-secondary" />
|
||||
</Tooltip>
|
||||
) : null}
|
||||
<Divider layout="vertical" />
|
||||
<span className="coz-fg-dim">{props.desc}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Avatar, Tag } from '@coze-arch/coze-design';
|
||||
import { type Model } from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
/** 极简版 ModelOption,用于 Button 展示或 Select 已选栏 */
|
||||
export function ModelOptionThumb({ model }: { model: Model }) {
|
||||
return (
|
||||
<div className="px-[4px] flex items-center gap-[4px]">
|
||||
<Avatar
|
||||
shape="square"
|
||||
size="extra-extra-small"
|
||||
src={model.model_icon}
|
||||
className="rounded-[4px] border border-solid coz-stroke-primary"
|
||||
/>
|
||||
<span className="text-[14px] leading-[20px] coz-fg-primary">
|
||||
{model.name}
|
||||
</span>
|
||||
{model.model_status_details?.is_upcoming_deprecated ? (
|
||||
<Tag size="mini" color="yellow">
|
||||
{I18n.t('model_list_willDeprecated')}
|
||||
</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
.model-option {
|
||||
&:last-of-type,
|
||||
&.model-option_selected,
|
||||
&:hover,
|
||||
&:has(+ &.model-option_selected),
|
||||
&:has(+ &:hover) {
|
||||
.model-info-border {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
// UI 要改 coze design 默认样式,无奈
|
||||
:global(.coz-tag.coz-tag-mini) {
|
||||
padding-right: 3px;
|
||||
padding-left: 3px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable complexity -- ignore */
|
||||
import { type PropsWithChildren, useRef } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { useHover } from 'ahooks';
|
||||
import {
|
||||
useBenefitAvailable,
|
||||
PremiumPaywallScene,
|
||||
usePremiumPaywallModal,
|
||||
} from '@coze-studio/premium-components-adapter';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type Model, ModelTagClass } from '@coze-arch/bot-api/developer_api';
|
||||
import {
|
||||
useBotCreatorContext,
|
||||
BotCreatorScene,
|
||||
} from '@coze-agent-ide/bot-creator-context';
|
||||
import {
|
||||
IconCozLongArrowTopRight,
|
||||
IconCozSetting,
|
||||
IconCozLongArrowUpCircle,
|
||||
IconCozDiamondFill,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { IconButton, Tag, Tooltip, Typography } from '@coze-arch/coze-design';
|
||||
import { OverflowList } from '@blueprintjs/core';
|
||||
|
||||
import { ModelOptionAvatar } from '../model-option-avatar';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export type ModelOptionProps = {
|
||||
model: Model;
|
||||
selected?: boolean;
|
||||
disabled?: boolean;
|
||||
/** 返回是否切换成功 */
|
||||
onClick: () => boolean;
|
||||
} & (
|
||||
| {
|
||||
enableConfig?: false;
|
||||
}
|
||||
| {
|
||||
enableConfig: true;
|
||||
onConfigClick: () => void;
|
||||
}
|
||||
) &
|
||||
(
|
||||
| {
|
||||
enableJumpDetail?: false;
|
||||
}
|
||||
| {
|
||||
enableJumpDetail: true;
|
||||
/**
|
||||
* 点击跳转模型管理页面
|
||||
*
|
||||
* 因为该组件定位是纯 UI 组件,且不同模块 space id 获取的方式不尽相同,因此跳转行为和 url 的拼接就不内置了
|
||||
*/
|
||||
onDetailClick: (modelId: string) => void;
|
||||
}
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function
|
||||
export function ModelOption({
|
||||
model,
|
||||
selected,
|
||||
disabled,
|
||||
onClick,
|
||||
...props
|
||||
}: ModelOptionProps) {
|
||||
/** 这个 ref 纯粹为了判断是否 hover */
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
const isHovering = useHover(ref);
|
||||
const { scene } = useBotCreatorContext();
|
||||
|
||||
const featureTags = model.model_tag_list
|
||||
?.filter(t => t.tag_class === ModelTagClass.ModelFeature && t.tag_name)
|
||||
.map(t => t.tag_name);
|
||||
const functionTags = model.model_tag_list
|
||||
?.filter(t => t.tag_class === ModelTagClass.ModelFunction && t.tag_name)
|
||||
.map(t => t.tag_name);
|
||||
|
||||
// 付费墙,社区版不支持该功能
|
||||
const isProModel =
|
||||
model.model_status_details?.is_new_model ||
|
||||
model.model_status_details?.is_advanced_model;
|
||||
const isNewModelAvailable = useBenefitAvailable({
|
||||
scene: PremiumPaywallScene.NewModel,
|
||||
});
|
||||
const { node: premiumPaywallModal, open: openPremiumPaywallModal } =
|
||||
usePremiumPaywallModal({ scene: PremiumPaywallScene.NewModel });
|
||||
|
||||
return (
|
||||
<>
|
||||
<article
|
||||
ref={ref}
|
||||
className={cls(
|
||||
'pl-[16px] pr-[12px] w-full relative',
|
||||
'flex gap-[16px] items-center rounded-[12px]',
|
||||
selected
|
||||
? 'coz-mg-hglt hover:coz-mg-hglt-hovered'
|
||||
: 'hover:coz-mg-secondary-hovered active:coz-mg-secondary-pressed',
|
||||
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
||||
// 以下 cls 只为实现 hover、active、last 时隐藏上下分割线(注意分割线在 model-info-border,设计师的小心思)
|
||||
styles['model-option'],
|
||||
// @ts-expect-error -- 不知道为什么会报错
|
||||
{ [styles['model-option_selected']]: selected },
|
||||
)}
|
||||
onClick={() => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
if (isProModel && !isNewModelAvailable) {
|
||||
openPremiumPaywallModal();
|
||||
return;
|
||||
}
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
<ModelOptionAvatar model={model} />
|
||||
<div
|
||||
className={cls(
|
||||
'h-[80px] py-[12px] w-full',
|
||||
'flex flex-col overflow-hidden',
|
||||
'border-0 border-b border-solid coz-stroke-primary',
|
||||
styles['model-info-border'],
|
||||
)}
|
||||
style={
|
||||
isHovering
|
||||
? {
|
||||
mask: calcMaskStyle([
|
||||
props.enableConfig,
|
||||
props.enableJumpDetail,
|
||||
]),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div className="w-full flex items-center gap-[6px] overflow-hidden">
|
||||
<Typography.Title fontSize="14px" ellipsis={{ showTooltip: true }}>
|
||||
{model.name}
|
||||
</Typography.Title>
|
||||
<div className="shrink-0 flex gap-[6px]">
|
||||
{/* 抖音分身场景下不展示改 tag,社区版暂不支持该功能 */}
|
||||
{model.model_status_details?.is_free_model &&
|
||||
scene !== BotCreatorScene.DouyinBot ? (
|
||||
<Tag size="mini" color="primary" className="!coz-mg-plus">
|
||||
{I18n.t('model_list_free')}
|
||||
</Tag>
|
||||
) : null}
|
||||
{isProModel && !isNewModelAvailable ? (
|
||||
<IconCozDiamondFill className="coz-fg-hglt" />
|
||||
) : null}
|
||||
{featureTags?.length
|
||||
? featureTags.map(feature => (
|
||||
<Tag
|
||||
key={feature}
|
||||
size="mini"
|
||||
color="primary"
|
||||
className="!bg-transparent !border border-solid coz-stroke-plus"
|
||||
>
|
||||
{feature}
|
||||
</Tag>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center text-[12px] leading-[16px] coz-fg-dim overflow-hidden">
|
||||
<ModelTag isFirst>
|
||||
{((model.model_quota?.token_limit || 0) / 1024).toFixed(0)}K
|
||||
</ModelTag>
|
||||
<ModelTag
|
||||
isLast={!functionTags?.length}
|
||||
className="flex items-center gap-[4px]"
|
||||
>
|
||||
<span>{model.model_name}</span>
|
||||
{model.model_status_details?.update_info ? (
|
||||
<Tooltip content={model.model_status_details.update_info}>
|
||||
<IconCozLongArrowUpCircle className="ml-[2px] coz-fg-hglt-green" />
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</ModelTag>
|
||||
{functionTags?.length ? (
|
||||
<Tooltip content={functionTags.join(IS_OVERSEA ? ', ' : '、')}>
|
||||
<span className="overflow-hidden">
|
||||
<OverflowList
|
||||
items={functionTags}
|
||||
visibleItemRenderer={(item, idx) => (
|
||||
<ModelTag
|
||||
key={idx}
|
||||
isLast={idx === functionTags.length - 1}
|
||||
>
|
||||
{item}
|
||||
</ModelTag>
|
||||
)}
|
||||
overflowRenderer={restItems => (
|
||||
<span className="pl-[6px] flex items-center">{`+${restItems.length}`}</span>
|
||||
)}
|
||||
collapseFrom="end"
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
<Typography.Text
|
||||
className="mt-[4px] text-[12px] leading-[16px] coz-fg-secondary"
|
||||
ellipsis={{ showTooltip: true }}
|
||||
>
|
||||
{model.model_brief_desc}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
{isHovering ? (
|
||||
<div className="absolute right-[12px] h-full flex items-center gap-[3px]">
|
||||
{props.enableConfig ? (
|
||||
<IconButton
|
||||
icon={<IconCozSetting />}
|
||||
color="secondary"
|
||||
size="default"
|
||||
data-testid="model_select_option.config_btn"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
|
||||
// 付费墙拦截
|
||||
if (isProModel && !isNewModelAvailable) {
|
||||
openPremiumPaywallModal();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected) {
|
||||
props.onConfigClick();
|
||||
return;
|
||||
}
|
||||
const success = onClick();
|
||||
if (success) {
|
||||
setTimeout(() => props.onConfigClick());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{props.enableJumpDetail ? (
|
||||
<IconButton
|
||||
icon={<IconCozLongArrowTopRight />}
|
||||
color="secondary"
|
||||
size="default"
|
||||
data-testid="model_select_option.detail_btn"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
props.onDetailClick(String(model.model_type));
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</article>
|
||||
{premiumPaywallModal}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ModelTag({
|
||||
isFirst,
|
||||
isLast,
|
||||
className,
|
||||
children,
|
||||
}: PropsWithChildren<{
|
||||
isFirst?: boolean;
|
||||
isLast?: boolean;
|
||||
className?: string;
|
||||
}>) {
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
{ 'pl-[6px]': !isFirst },
|
||||
'shrink-0 flex items-center gap-[6px]',
|
||||
)}
|
||||
>
|
||||
<span className={className}>{children}</span>
|
||||
{isLast ? null : (
|
||||
<span className="h-[9px] border-0 border-r border-solid coz-stroke-primary" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* hover 展示若干图标(比如跳转模型详情页、详细配置)时,要对图标下的内容有个渐变遮罩效果
|
||||
* 该方法用于计算遮罩样式
|
||||
*/
|
||||
function calcMaskStyle(buttonVisible: Array<boolean | undefined>) {
|
||||
const btnNum = buttonVisible.reduce(
|
||||
(prevNum, showBtn) => prevNum + (showBtn ? 1 : 0),
|
||||
0,
|
||||
);
|
||||
if (btnNum === 0) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
const BTN_WIDTH = 32;
|
||||
const BTN_GAP = 3;
|
||||
/** 不随按钮数量变化的遮罩固定宽度 */
|
||||
const PRESET_PADDING = 16;
|
||||
/** 遮罩的渐变宽度 */
|
||||
const MASK_WIDTH = 24;
|
||||
|
||||
const gradientStart =
|
||||
btnNum * BTN_WIDTH + (btnNum - 1) * BTN_GAP + PRESET_PADDING;
|
||||
const gradientEnd = gradientStart + MASK_WIDTH;
|
||||
return `linear-gradient(to left, rgba(0,0,0,0), rgba(0,0,0,0) ${gradientStart}px, #fff ${gradientEnd}px)`;
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode, useRef, useState } from 'react';
|
||||
|
||||
import { isBoolean } from 'lodash-es';
|
||||
import cls from 'classnames';
|
||||
import { Popover, type PopoverProps } from '@coze-arch/coze-design';
|
||||
import { IconCozArrowDown } from '@coze-arch/bot-icons';
|
||||
import { type Model } from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
import { PopoverModelListView } from '../popover-model-list-view';
|
||||
import {
|
||||
type ModelConfigProps,
|
||||
PopoverModelConfigView,
|
||||
} from '../popover-model-config-view';
|
||||
import { ModelOptionThumb } from '../model-option-thumb';
|
||||
|
||||
export interface ModelSelectUIProps {
|
||||
/**
|
||||
* 是否禁止弹出 popover
|
||||
*
|
||||
* 目前内部实现既支持 disabled 时直接不允许弹出 popover(与历史逻辑一致)
|
||||
* 也支持允许弹出 popover 和查看详细配置但禁止编辑
|
||||
* 需求变更时可灵活修改
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/** 当前选中的模型 */
|
||||
selectedModelId: string;
|
||||
/**
|
||||
* 是否展示跳转到模型详情页(/space/:space_id/model/:model_id)按钮
|
||||
* @default false
|
||||
*/
|
||||
enableJumpDetail?:
|
||||
| {
|
||||
spaceId: string;
|
||||
}
|
||||
| false;
|
||||
/**
|
||||
* 模型选择的变更事件
|
||||
*
|
||||
* 返回值表示是否成功切换,对部分后续事件会有影响,比如自动关闭 popover
|
||||
* 不显式 return 则视为 true
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- 要实现【要么不用 return,要么必须 return boolean】没别的办法了啊
|
||||
onModelChange: (model: Model) => boolean | void;
|
||||
modelList: Model[];
|
||||
/**
|
||||
* 允许业务侧自定义触发器展示,命名对齐 semi select 组件
|
||||
*
|
||||
* @param model - 当 selectedModelId 找不到对应的 model 时,这里则会传入 undefined
|
||||
*/
|
||||
triggerRender?: (model?: Model, popoverVisible?: boolean) => ReactNode;
|
||||
/**
|
||||
* workflow 等不允许详细配置的业务会有 clickToHide 的诉求
|
||||
* @default false
|
||||
*/
|
||||
clickToHide?: boolean;
|
||||
/** @default bottomLeft */
|
||||
popoverPosition?: PopoverProps['position'];
|
||||
/** @default true */
|
||||
popoverAutoAdjustOverflow?: boolean;
|
||||
/** trigger 的 className。若传入 triggerRender 则完全由 triggerRender 接管渲染,该参数不再起作用 */
|
||||
className?: string;
|
||||
popoverClassName?: string;
|
||||
/**
|
||||
* 若业务侧自行在组件外部插入 Modal,则点击 Modal 也会触发 onClickOutSide 导致 popover 关闭
|
||||
* 若不希望 popover 意外关闭,则需要将 Modal 通过 modalSlot 传入
|
||||
*
|
||||
* (甚至不需要设置 getPopupContainer,此时 Modal 的挂载层和 ModelSelect 的 Popover 的挂载层依然不同,但却神秘地不会再触发 onClickOutSide 了,semi 牛逼)
|
||||
*/
|
||||
modalSlot?: ReactNode;
|
||||
|
||||
/** 模型详细配置信息,不传则隐藏详细配置的按钮入口 */
|
||||
modelConfigProps?: ModelConfigProps;
|
||||
|
||||
/** 弹窗有多种渲染场景,提供选项来定制渲染层级已避免覆盖 */
|
||||
zIndex?: number;
|
||||
|
||||
/** 模型列表额外头部插槽 */
|
||||
modelListExtraHeaderSlot?: ReactNode;
|
||||
|
||||
/** 是否默认展开模型列表 */
|
||||
defaultOpen?: boolean;
|
||||
}
|
||||
|
||||
export function ModelSelectUI({
|
||||
className,
|
||||
disabled,
|
||||
enableJumpDetail,
|
||||
popoverClassName,
|
||||
selectedModelId,
|
||||
modelList,
|
||||
onModelChange,
|
||||
triggerRender,
|
||||
modelListExtraHeaderSlot,
|
||||
clickToHide = false,
|
||||
popoverPosition = 'bottomLeft',
|
||||
popoverAutoAdjustOverflow = true,
|
||||
modalSlot,
|
||||
modelConfigProps,
|
||||
zIndex = 999,
|
||||
defaultOpen = false,
|
||||
}: ModelSelectUIProps) {
|
||||
/** 为了实现 Popover 跟 Select 宽度一致,通过该 ref 获取 Select 宽度(若传入 triggerRender 则不再需要保持一致) */
|
||||
const selectRef = useRef<HTMLDivElement>(null);
|
||||
const [popoverVisible, setPopoverVisible] = useState(defaultOpen);
|
||||
const [detailConfigVisible, setDetailConfigVisible] = useState(false);
|
||||
|
||||
const selectedModel = modelList.find(
|
||||
({ model_type }) => selectedModelId === String(model_type),
|
||||
);
|
||||
|
||||
// 需要实现 group + custom option 效果,Select 组件兼容性不佳,不得不自行实现 Popover
|
||||
return (
|
||||
<Popover
|
||||
zIndex={zIndex}
|
||||
stopPropagation
|
||||
autoAdjustOverflow={popoverAutoAdjustOverflow}
|
||||
visible={popoverVisible}
|
||||
trigger="click"
|
||||
position={popoverPosition}
|
||||
onClickOutSide={() => {
|
||||
setPopoverVisible(false);
|
||||
setDetailConfigVisible(false);
|
||||
}}
|
||||
className={cls('!p-0')}
|
||||
content={
|
||||
<div
|
||||
className={cls(
|
||||
'w-[480px] max-h-[50vh] !p-0 overflow-hidden',
|
||||
popoverClassName,
|
||||
)}
|
||||
style={
|
||||
selectRef.current
|
||||
? { width: selectRef.current.clientWidth }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{modelConfigProps ? (
|
||||
<PopoverModelConfigView
|
||||
disabled={disabled}
|
||||
visible={detailConfigVisible}
|
||||
selectedModel={selectedModel}
|
||||
onClose={() => setDetailConfigVisible(false)}
|
||||
modelConfigProps={modelConfigProps}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<PopoverModelListView
|
||||
// 用 hidden 而不是直接条件性挂载以便保留 scrollTop,设计师的小心思
|
||||
hidden={detailConfigVisible}
|
||||
disabled={disabled}
|
||||
selectedModelId={selectedModelId}
|
||||
selectedModel={selectedModel}
|
||||
modelList={modelList}
|
||||
extraHeaderSlot={modelListExtraHeaderSlot}
|
||||
onModelClick={(m: Model) => {
|
||||
const res = onModelChange(m);
|
||||
const success = isBoolean(res) ? res : true;
|
||||
if (success && clickToHide) {
|
||||
setPopoverVisible(false);
|
||||
setDetailConfigVisible(false);
|
||||
}
|
||||
return success;
|
||||
}}
|
||||
enableJumpDetail={!!enableJumpDetail}
|
||||
onDetailClick={modelId => {
|
||||
if (!enableJumpDetail) {
|
||||
return;
|
||||
}
|
||||
window.open(
|
||||
`/space/${enableJumpDetail.spaceId}/model/${modelId}`,
|
||||
'_blank',
|
||||
);
|
||||
}}
|
||||
enableConfig={!!modelConfigProps}
|
||||
onConfigClick={() => {
|
||||
if (!modelConfigProps) {
|
||||
return;
|
||||
}
|
||||
setDetailConfigVisible(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
{modalSlot}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{triggerRender ? (
|
||||
<span
|
||||
className={disabled ? 'cursor-not-allowed' : 'cursor-pointer'}
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
setPopoverVisible(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{triggerRender(selectedModel, popoverVisible)}
|
||||
</span>
|
||||
) : (
|
||||
<div
|
||||
ref={selectRef}
|
||||
className={cls(
|
||||
'w-full p-[4px] flex items-center justify-between rounded-[8px]',
|
||||
'overflow-hidden cursor-pointer border border-solid',
|
||||
'hover:coz-mg-secondary-hovered active:coz-mg-secondary-pressed',
|
||||
popoverVisible ? 'coz-stroke-hglt' : 'coz-stroke-primary',
|
||||
className,
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
setPopoverVisible(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ModelOptionThumb
|
||||
model={selectedModel || { name: selectedModelId }}
|
||||
/>
|
||||
<IconCozArrowDown className="coz-fg-secondary" />
|
||||
</div>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { isBoolean } from 'lodash-es';
|
||||
import { useMount } from 'ahooks';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Modal } from '@coze-arch/coze-design';
|
||||
import {
|
||||
usePremiumStore,
|
||||
usePremiumType,
|
||||
} from '@coze-studio/premium-store-adapter';
|
||||
import {
|
||||
useBenefitAvailable,
|
||||
usePremiumManageModal,
|
||||
PremiumPaywallScene,
|
||||
} from '@coze-studio/premium-components-adapter';
|
||||
|
||||
import { ModelSelectUI, type ModelSelectUIProps } from '../model-select-ui';
|
||||
|
||||
export interface ModelSelectProps extends ModelSelectUIProps {
|
||||
/**
|
||||
* 是否允许选择高级模型/新模型,否则内置弹窗拦截
|
||||
* 当不传或设置为 auto 时,则由组件内置判断用户是否是付费用户(国内专业版,海外 premium)
|
||||
* @default auto
|
||||
*/
|
||||
canSelectSuperiorModel?: boolean | 'auto';
|
||||
}
|
||||
|
||||
/**
|
||||
* 该组件相比 ModelSelectUI 单纯多了付费拦截功能
|
||||
*/
|
||||
export function ModelSelect({
|
||||
onModelChange,
|
||||
canSelectSuperiorModel: canSelectSuperiorModelProps,
|
||||
modalSlot,
|
||||
modelListExtraHeaderSlot,
|
||||
...restProps
|
||||
}: ModelSelectProps) {
|
||||
const { fetchPremiumPlan, fetchPremiumPlans } = usePremiumStore(
|
||||
useShallow(s => ({
|
||||
fetchPremiumPlans: s.fetchPremiumPlans,
|
||||
fetchPremiumPlan: s.fetchPremiumPlan,
|
||||
})),
|
||||
);
|
||||
// 国内:是否允许使用新模型/高级模型
|
||||
const isBenefitAvailable = useBenefitAvailable({
|
||||
scene: PremiumPaywallScene.NewModel,
|
||||
});
|
||||
|
||||
/** 海外是否为 premium */
|
||||
const { isFree } = usePremiumType();
|
||||
|
||||
const canSelectSuperiorModel = isBoolean(canSelectSuperiorModelProps)
|
||||
? canSelectSuperiorModelProps
|
||||
: IS_OVERSEA
|
||||
? !isFree
|
||||
: isBenefitAvailable;
|
||||
const [upgradeModalState, setUpgradeModalState] = useState<{
|
||||
type?: 'new' | 'advance';
|
||||
visible: boolean;
|
||||
}>({ visible: false });
|
||||
|
||||
const { node: premiumManageModal, open: openPremiumModal } =
|
||||
usePremiumManageModal();
|
||||
|
||||
useMount(() => {
|
||||
if (IS_OVERSEA) {
|
||||
fetchPremiumPlans().then(fetchPremiumPlan);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<ModelSelectUI
|
||||
modelListExtraHeaderSlot={modelListExtraHeaderSlot}
|
||||
onModelChange={m => {
|
||||
const isFreeModel =
|
||||
!m.model_status_details?.is_new_model &&
|
||||
!m.model_status_details?.is_advanced_model;
|
||||
if (canSelectSuperiorModel || isFreeModel) {
|
||||
return onModelChange(m);
|
||||
}
|
||||
|
||||
setUpgradeModalState({
|
||||
visible: true,
|
||||
type: m.model_status_details?.is_new_model ? 'new' : 'advance',
|
||||
});
|
||||
return false;
|
||||
}}
|
||||
modalSlot={
|
||||
<>
|
||||
<Modal
|
||||
// ModelSelect 用到的 Popover 组件弹层默认 z-index 为 1030
|
||||
zIndex={1031}
|
||||
visible={upgradeModalState.visible}
|
||||
title={
|
||||
upgradeModalState.type === 'new'
|
||||
? I18n.t('model_list_upgrade_to_pro_version')
|
||||
: I18n.t('model_list_upgrade_to_pro_version_advancedModel')
|
||||
}
|
||||
cancelText={I18n.t('Cancel')}
|
||||
okText={I18n.t('model_list_upgrade_button')}
|
||||
onOk={() => {
|
||||
if (IS_CN_REGION) {
|
||||
openPremiumModal();
|
||||
// 这么操作是为了在关闭动画过程中防止 modal 内容跳变
|
||||
setUpgradeModalState(s => ({ ...s, visible: false }));
|
||||
} else {
|
||||
window.open('/premium', '_blank');
|
||||
}
|
||||
setUpgradeModalState(s => ({ ...s, visible: false }));
|
||||
}}
|
||||
onCancel={() =>
|
||||
setUpgradeModalState(s => ({ ...s, visible: false }))
|
||||
}
|
||||
>
|
||||
{upgradeModalState.type === 'new'
|
||||
? I18n.t('model_list_ensure_service_quality')
|
||||
: I18n.t('model_list_upgrade_to_pro_advanced_tips')}
|
||||
</Modal>
|
||||
{modalSlot}
|
||||
{premiumManageModal}
|
||||
</>
|
||||
}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode, useEffect, useMemo } from 'react';
|
||||
|
||||
import { omit } from 'lodash-es';
|
||||
import cls from 'classnames';
|
||||
import { useCreation } from 'ahooks';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
IconCozArrowLeft,
|
||||
IconCozWarningCircleFill,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { IconButton, Loading } from '@coze-arch/coze-design';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
import { type Model, type ModelInfo } from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
import { modelFormComponentMap } from '../../model-form/type';
|
||||
import { primitiveExhaustiveCheck } from '../../../utils/exhaustive-check';
|
||||
import {
|
||||
useHandleModelForm,
|
||||
type UseHandleModelFormProps,
|
||||
} from '../../../hooks/model-form/use-handle-model-form';
|
||||
import { type ModelFormContextProps } from '../../../context/model-form-context/type';
|
||||
import { ModelFormProvider } from '../../../context/model-form-context/context';
|
||||
import {
|
||||
type FormilyCoreType,
|
||||
type FormilyReactType,
|
||||
} from '../../../context/formily-context/type';
|
||||
import { useFormily } from '../../../context/formily-context';
|
||||
|
||||
export interface ModelConfigProps
|
||||
extends Pick<ModelFormContextProps, 'hideDiversityCollapseButton'> {
|
||||
modelStore: UseHandleModelFormProps['modelStore'];
|
||||
/**
|
||||
* 模型配置更新
|
||||
*
|
||||
* 需要注意切换模型时,会先触发 onModelChange,由外部传入更新后的 selectedModelId,此后内部会计算新模型的 config 并触发 onConfigChange
|
||||
*
|
||||
* 理想数据流是切换模型触发 onModelChange 后,外部一并传入新的 selectedModelId 和 currentConfig。或者由 onModelChange 同时抛出新的 modelId 和 config。
|
||||
* 目前这样虽然有点挫,但由于历史设计原因,改造成上述方式成本略高,暂保持现状。
|
||||
*/
|
||||
onConfigChange: (value: ModelInfo) => void;
|
||||
currentConfig: ModelInfo;
|
||||
/** 当前 agent 是 single 还是 mulit */
|
||||
agentType: 'single' | 'multi';
|
||||
/** 明确diff类型, 透传给getSchema。model-diff情况下不展示携带上下文轮数影响 */
|
||||
diffType?: 'prompt-diff' | 'model-diff';
|
||||
}
|
||||
|
||||
interface PopoverModelConfigViewProps {
|
||||
/**
|
||||
* 需要持续保留表单实例,以便复用无比复杂的「切换模型时初始化详细配置」的逻辑
|
||||
*
|
||||
* 理想做法是在外层业务侧 onModelChange 时重置 config 值
|
||||
* 但一是该逻辑过于复杂,难以独立抽出初始化方法;
|
||||
* 二是 useHandleModelForm 使用成本又极高,不适合放到最外层业务侧去调用
|
||||
*/
|
||||
visible: boolean;
|
||||
disabled?: boolean;
|
||||
selectedModel?: Model;
|
||||
onClose: () => void;
|
||||
modelConfigProps: ModelConfigProps;
|
||||
}
|
||||
|
||||
/** Popover 的 模型配置状态,对应列表状态。单纯为了避免组件过大而做的拆分 */
|
||||
export function PopoverModelConfigView({
|
||||
visible,
|
||||
disabled,
|
||||
selectedModel,
|
||||
onClose,
|
||||
modelConfigProps,
|
||||
}: PopoverModelConfigViewProps) {
|
||||
const formilyInitState = useInitFormily();
|
||||
return (
|
||||
<div
|
||||
className={cls('h-full p-[16px] flex flex-col gap-[12px] overflow-auto', {
|
||||
hidden: !visible,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center gap-[6px]">
|
||||
<IconButton
|
||||
icon={<IconCozArrowLeft />}
|
||||
color="secondary"
|
||||
// size="small"
|
||||
onClick={e => {
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
<span className="text-[16px] leading-[22px] font-medium coz-fg-plus">
|
||||
{I18n.t('model_list_model_setting', { model: selectedModel?.name })}
|
||||
</span>
|
||||
</div>
|
||||
{formilyInitState.success ? (
|
||||
<ModelForm
|
||||
disabled={disabled}
|
||||
currentModelId={
|
||||
selectedModel?.model_type ? String(selectedModel?.model_type) : ''
|
||||
}
|
||||
modelConfigProps={modelConfigProps}
|
||||
{...formilyInitState.formilyPkg}
|
||||
/>
|
||||
) : (
|
||||
formilyInitState.node
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ModelFormProps {
|
||||
disabled?: boolean;
|
||||
currentModelId: string;
|
||||
formilyCore: FormilyCoreType;
|
||||
formilyReact: FormilyReactType;
|
||||
modelConfigProps: ModelConfigProps;
|
||||
}
|
||||
function ModelForm({
|
||||
disabled,
|
||||
currentModelId,
|
||||
formilyCore,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- FormProvider 不适合用别的格式
|
||||
formilyReact: { createSchemaField, FormProvider },
|
||||
modelConfigProps: {
|
||||
currentConfig,
|
||||
onConfigChange,
|
||||
modelStore,
|
||||
agentType,
|
||||
hideDiversityCollapseButton,
|
||||
diffType,
|
||||
},
|
||||
}: ModelFormProps) {
|
||||
const { createForm } = formilyCore;
|
||||
const form = useCreation(() => createForm(), [currentModelId]);
|
||||
const SchemaField = useCreation(
|
||||
() => createSchemaField({ components: modelFormComponentMap }),
|
||||
[],
|
||||
);
|
||||
|
||||
const { getSchema, handleFormInit, handleFormUnmount } = useHandleModelForm({
|
||||
currentModelId,
|
||||
editable: !disabled,
|
||||
getModelRecord: () => currentConfig,
|
||||
onValuesChange: ({ values }) => {
|
||||
onConfigChange(values);
|
||||
},
|
||||
modelStore,
|
||||
});
|
||||
|
||||
const schema = useMemo(
|
||||
() =>
|
||||
getSchema({
|
||||
currentModelId,
|
||||
isSingleAgent: agentType === 'single',
|
||||
diffType,
|
||||
}),
|
||||
[currentModelId, agentType, diffType],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// 在 promise executor 中执行回调,其中的错误会异步产生 promise rejection ,而不是导致页面白屏
|
||||
new Promise(() => handleFormInit(form, formilyCore));
|
||||
|
||||
return handleFormUnmount;
|
||||
}, [form]);
|
||||
|
||||
return (
|
||||
<ModelFormProvider
|
||||
hideDiversityCollapseButton={hideDiversityCollapseButton}
|
||||
>
|
||||
<FormProvider form={form}>
|
||||
<SchemaField schema={schema} />
|
||||
</FormProvider>
|
||||
</ModelFormProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function useInitFormily():
|
||||
| {
|
||||
success: true;
|
||||
formilyPkg: {
|
||||
formilyCore: FormilyCoreType;
|
||||
formilyReact: FormilyReactType;
|
||||
};
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
node: ReactNode;
|
||||
} {
|
||||
const { formilyModule, retryImportFormily } = useFormily();
|
||||
|
||||
if (formilyModule.status === 'loading' || formilyModule.status === 'unInit') {
|
||||
return { success: false, node: <Loading loading /> };
|
||||
}
|
||||
|
||||
if (formilyModule.status === 'error') {
|
||||
return {
|
||||
success: false,
|
||||
node: (
|
||||
<div className="h-full flex items-center gap-y-[8px] text-[14px]">
|
||||
<IconCozWarningCircleFill
|
||||
// 该值迁移自 src/components/model-form/index.tsx
|
||||
className="text-[#FF2710]"
|
||||
/>
|
||||
<div className="font-semibold leading-[22px]">
|
||||
<span>{I18n.t('model_form_fail_text')}</span>
|
||||
<span
|
||||
// 该值迁移自 src/components/model-form/index.tsx
|
||||
className="cursor-pointer text-[#4D53E8]"
|
||||
onClick={retryImportFormily}
|
||||
>
|
||||
{I18n.t('model_form_fail_retry')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (formilyModule.status === 'ready') {
|
||||
return {
|
||||
success: true,
|
||||
formilyPkg: omit(formilyModule, ['status']),
|
||||
};
|
||||
}
|
||||
|
||||
primitiveExhaustiveCheck(formilyModule.status);
|
||||
throw new CustomError('normal_error', 'unrecognized formilyModule.status');
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
|
||||
import { groupBy } from 'lodash-es';
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type Model } from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
import { ModelOptionGroup } from '../model-option-group';
|
||||
import { ModelOption } from '../model-option';
|
||||
|
||||
/** Popover 的 模型列表状态,对应详细配置状态。单纯为了避免组件过大而做的拆分 */
|
||||
export function PopoverModelListView({
|
||||
hidden,
|
||||
disabled,
|
||||
selectedModelId,
|
||||
selectedModel,
|
||||
modelList,
|
||||
extraHeaderSlot,
|
||||
onModelClick,
|
||||
onDetailClick,
|
||||
onConfigClick,
|
||||
enableConfig,
|
||||
enableJumpDetail,
|
||||
}: {
|
||||
/** 是否将列表设置为 display: none(为了保留 scrollTop 信息) */
|
||||
hidden: boolean;
|
||||
disabled?: boolean;
|
||||
selectedModelId: string;
|
||||
selectedModel: Model | undefined;
|
||||
modelList: Model[];
|
||||
/** 额外头部插槽 */
|
||||
extraHeaderSlot?: ReactNode;
|
||||
/** 返回是否切换成功 */
|
||||
onModelClick: (model: Model) => boolean;
|
||||
onDetailClick: (modelId: string) => void;
|
||||
onConfigClick: (model: Model) => void;
|
||||
enableConfig?: boolean;
|
||||
enableJumpDetail?: boolean;
|
||||
}) {
|
||||
const { modelGroups } = useMemo(() => {
|
||||
/** 开源版本不进行分类 平铺展示 */
|
||||
if (IS_OPEN_SOURCE) {
|
||||
return { modelGroups: [modelList] };
|
||||
}
|
||||
const modelSeriesGroups = groupBy(
|
||||
modelList,
|
||||
model => model.model_series?.series_name,
|
||||
);
|
||||
return {
|
||||
modelGroups: Object.values(modelSeriesGroups).filter(
|
||||
(group): group is Model[] => !!group?.length,
|
||||
),
|
||||
};
|
||||
}, [modelList]);
|
||||
|
||||
const renderModelOption = (model: Model) => (
|
||||
<ModelOption
|
||||
key={model.model_type}
|
||||
model={model}
|
||||
disabled={disabled}
|
||||
selected={String(model.model_type) === selectedModelId}
|
||||
onClick={() => onModelClick(model)}
|
||||
enableJumpDetail={enableJumpDetail}
|
||||
onDetailClick={onDetailClick}
|
||||
enableConfig={
|
||||
enableConfig &&
|
||||
// 在 disabled 状态下,只能查看选中模型的详细配置
|
||||
(!disabled || String(model.model_type) === selectedModelId)
|
||||
}
|
||||
onConfigClick={() => {
|
||||
onConfigClick(model);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
'max-h-[inherit]', // https://stackoverflow.com/questions/14262938/child-with-max-height-100-overflows-parent
|
||||
'p-[8px] flex flex-col gap-[8px] overflow-auto',
|
||||
{
|
||||
hidden,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between pl-4 pr-2 box-content h-[32px] pb-2 pt-1">
|
||||
<div className="text-xxl font-medium coz-fg-plus">
|
||||
{I18n.t('model_selection')}
|
||||
</div>
|
||||
{extraHeaderSlot}
|
||||
</div>
|
||||
{selectedModel?.model_status_details?.is_upcoming_deprecated ? (
|
||||
<section className="p-[12px] pl-[16px] rounded-[8px] coz-mg-hglt-yellow">
|
||||
<div className="text-[14px] leading-[20px] font-medium coz-fg-plus">
|
||||
{I18n.t('model_list_model_deprecation_notice')}
|
||||
</div>
|
||||
<div className="text-[12px] leading-[16px] coz-fg-primary">
|
||||
{I18n.t('model_list_model_switch_announcement', {
|
||||
model_deprecated: selectedModel.name,
|
||||
date: selectedModel.model_status_details.deprecated_date,
|
||||
model_up: selectedModel.model_status_details.replace_model_name,
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{modelGroups.map((group, idx) => {
|
||||
if (IS_OPEN_SOURCE) {
|
||||
return group.map(renderModelOption);
|
||||
}
|
||||
return (
|
||||
<ModelOptionGroup
|
||||
key={group[0]?.model_series?.series_name ?? idx}
|
||||
type={
|
||||
group[0]?.model_status_details?.is_new_model ? 'new' : 'normal'
|
||||
}
|
||||
icon={group[0]?.model_series?.icon_url || ''}
|
||||
name={group[0]?.model_series?.series_name || ''}
|
||||
desc={I18n.t('model_list_model_company', {
|
||||
company: group[0]?.model_series?.model_vendor || '',
|
||||
})}
|
||||
tips={group[0]?.model_series?.model_tips || ''}
|
||||
>
|
||||
{group.map(renderModelOption)}
|
||||
</ModelOptionGroup>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
.form-wrapper {
|
||||
position: relative;
|
||||
box-sizing: content-box;
|
||||
width: 432px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useMultiAgentStore } from '@coze-studio/bot-detail-store/multi-agent';
|
||||
import { useModelStore } from '@coze-studio/bot-detail-store/model';
|
||||
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
import { useBotEditor } from '@coze-agent-ide/bot-editor-context-store';
|
||||
|
||||
import { ModelForm } from '../../model-form';
|
||||
import { useAgentModelCapabilityCheckAndAlert } from '../../model-capability-confirm-model';
|
||||
import { ReportEventNames } from '../../../report-events/report-event-names';
|
||||
import { useHandleModelForm } from '../../../hooks/model-form/use-handle-model-form';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const MultiAgentModelForm: React.FC<{ agentId: string }> = ({
|
||||
agentId,
|
||||
}) => {
|
||||
const { agent, setMultiAgentByImmer } = useMultiAgentStore(
|
||||
useShallow(state => ({
|
||||
agent: state.agents.find(item => item.id === agentId),
|
||||
setMultiAgentByImmer: state.setMultiAgentByImmer,
|
||||
})),
|
||||
);
|
||||
const { ShortMemPolicy } = useModelStore(
|
||||
useShallow(state => ({
|
||||
ShortMemPolicy: state.config.ShortMemPolicy,
|
||||
})),
|
||||
);
|
||||
const { storeSet } = useBotEditor();
|
||||
const modelStore = storeSet.useModelStore(
|
||||
useShallow(state => ({
|
||||
onlineModelList: state.onlineModelList,
|
||||
offlineModelMap: state.offlineModelMap,
|
||||
getModelPreset: state.getModelPreset,
|
||||
})),
|
||||
);
|
||||
const isReadonly = useBotDetailIsReadonly();
|
||||
|
||||
if (!agent) {
|
||||
throw new CustomError(
|
||||
ReportEventNames.FailedGetAgentById,
|
||||
`agentId: ${agentId}`,
|
||||
);
|
||||
}
|
||||
const [modelId, setModelId] = useState(agent.model.model ?? '');
|
||||
|
||||
const { getSchema, handleFormInit, handleFormUnmount } = useHandleModelForm({
|
||||
currentModelId: modelId,
|
||||
editable: !isReadonly,
|
||||
getModelRecord: () => agent.model,
|
||||
onValuesChange: ({ values }) => {
|
||||
setMultiAgentByImmer(({ agents }) => {
|
||||
const target = agents?.find(item => item.id === agentId);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
target.model = {
|
||||
model: modelId,
|
||||
...values,
|
||||
ShortMemPolicy,
|
||||
};
|
||||
});
|
||||
},
|
||||
modelStore,
|
||||
});
|
||||
|
||||
const schema = useMemo(
|
||||
() =>
|
||||
getSchema({
|
||||
currentModelId: modelId,
|
||||
isSingleAgent: false,
|
||||
}),
|
||||
[modelId],
|
||||
);
|
||||
|
||||
const checkAndAlert = useAgentModelCapabilityCheckAndAlert();
|
||||
|
||||
return (
|
||||
<div className={styles['form-wrapper']}>
|
||||
<ModelForm
|
||||
schema={schema}
|
||||
currentModelId={modelId}
|
||||
onModelChange={async newId => {
|
||||
const res = await checkAndAlert(newId, agent);
|
||||
if (res) {
|
||||
setModelId(newId);
|
||||
}
|
||||
}}
|
||||
onFormInit={handleFormInit}
|
||||
onFormUnmount={handleFormUnmount}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
.form-wrapper {
|
||||
position: relative;
|
||||
box-sizing: content-box;
|
||||
width: 432px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
height: 32px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 32px;
|
||||
/* stylelint-disable-next-line custom-property-pattern */
|
||||
color: var(--Fg-Primary-COZ_fg_plus);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC, useMemo, useState } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { useModelStore } from '@coze-studio/bot-detail-store/model';
|
||||
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
|
||||
import { useBotEditor } from '@coze-agent-ide/bot-editor-context-store';
|
||||
|
||||
import { ModelForm } from '../model-form';
|
||||
import { useHandleModelForm } from '../../hooks/model-form/use-handle-model-form';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const SingleAgentModelForm: FC<{
|
||||
onBeforeSwitchModel?: (modelId: string) => Promise<boolean>;
|
||||
}> = ({ onBeforeSwitchModel }) => {
|
||||
const { model, setModelByImmer } = useModelStore(
|
||||
useShallow(state => ({
|
||||
model: state,
|
||||
setModelByImmer: state.setModelByImmer,
|
||||
})),
|
||||
);
|
||||
const { storeSet } = useBotEditor();
|
||||
const modelStore = storeSet.useModelStore(
|
||||
useShallow(state => ({
|
||||
onlineModelList: state.onlineModelList,
|
||||
offlineModelMap: state.offlineModelMap,
|
||||
getModelPreset: state.getModelPreset,
|
||||
})),
|
||||
);
|
||||
const isReadonly = useBotDetailIsReadonly();
|
||||
|
||||
const [modelId, setModelId] = useState(model.config.model ?? '');
|
||||
const { getSchema, handleFormInit, handleFormUnmount } = useHandleModelForm({
|
||||
currentModelId: modelId,
|
||||
editable: !isReadonly,
|
||||
getModelRecord: () => model.config,
|
||||
onValuesChange: ({ values }) => {
|
||||
setModelByImmer(draft => {
|
||||
draft.config = {
|
||||
model: modelId,
|
||||
...values,
|
||||
};
|
||||
});
|
||||
},
|
||||
modelStore,
|
||||
});
|
||||
|
||||
const schema = useMemo(
|
||||
() =>
|
||||
getSchema({
|
||||
currentModelId: modelId,
|
||||
isSingleAgent: true,
|
||||
}),
|
||||
[modelId],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles['form-wrapper']}
|
||||
data-testid="bot.ide.bot_creator.model_config_form"
|
||||
>
|
||||
<div className={styles['form-title']}>{I18n.t('model_config_title')}</div>
|
||||
<ModelForm
|
||||
schema={schema}
|
||||
currentModelId={modelId}
|
||||
onModelChange={async newId => {
|
||||
const res = onBeforeSwitchModel
|
||||
? await onBeforeSwitchModel(newId)
|
||||
: true;
|
||||
if (res) {
|
||||
setModelId(newId);
|
||||
}
|
||||
}}
|
||||
onFormInit={handleFormInit}
|
||||
onFormUnmount={handleFormUnmount}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export enum ModelFormComponent {
|
||||
SliderInputNumber = 'SliderInputNumber',
|
||||
Input = 'Input',
|
||||
|
||||
Switch = 'Switch',
|
||||
|
||||
RadioButton = 'RadioButton',
|
||||
|
||||
ModelFormItem = 'ModelFormItem',
|
||||
}
|
||||
|
||||
export enum ModelFormVoidFieldComponent {
|
||||
ModelFormGroupItem = 'ModelFormGroupItem',
|
||||
|
||||
ModelFormGenerationDiversityGroupItem = 'ModelFormGenerationDiversityGroupItem',
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type PropsWithChildren,
|
||||
createContext,
|
||||
useState,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
import { type FormilyModule, type FormilyContextProps } from './type';
|
||||
|
||||
export const FormilyContext = createContext<FormilyContextProps>({
|
||||
formilyModule: { status: 'unInit', formilyReact: null, formilyCore: null },
|
||||
retryImportFormily: () => void 0,
|
||||
});
|
||||
|
||||
export const FormilyProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const [formilyModule, setFormilyModule] = useState<FormilyModule>({
|
||||
status: 'unInit',
|
||||
formilyCore: null,
|
||||
formilyReact: null,
|
||||
});
|
||||
|
||||
const importFormily = async () => {
|
||||
setFormilyModule({
|
||||
formilyCore: null,
|
||||
formilyReact: null,
|
||||
status: 'loading',
|
||||
});
|
||||
try {
|
||||
const [formilyCore, formilyReact] = await Promise.all([
|
||||
import('@formily/core'),
|
||||
import('@formily/react'),
|
||||
]);
|
||||
setFormilyModule({
|
||||
status: 'ready',
|
||||
formilyCore,
|
||||
formilyReact,
|
||||
});
|
||||
} catch (error) {
|
||||
setFormilyModule({
|
||||
status: 'error',
|
||||
formilyCore: null,
|
||||
formilyReact: null,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
importFormily();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FormilyContext.Provider
|
||||
value={{
|
||||
formilyModule,
|
||||
retryImportFormily: importFormily,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FormilyContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { FormilyContext } from './context';
|
||||
|
||||
export const useFormily = () => {
|
||||
const context = useContext(FormilyContext);
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @coze-arch/no-batch-import-or-export */
|
||||
import type * as FormilyReact from '@formily/react';
|
||||
import type * as FomilyCore from '@formily/core';
|
||||
|
||||
export type FormilyReactType = typeof FormilyReact;
|
||||
export type FormilyCoreType = typeof FomilyCore;
|
||||
|
||||
export type FormilyModule =
|
||||
| {
|
||||
status: 'unInit' | 'loading' | 'error';
|
||||
formilyCore: null;
|
||||
formilyReact: null;
|
||||
}
|
||||
| {
|
||||
status: 'ready';
|
||||
formilyCore: FormilyCoreType;
|
||||
formilyReact: FormilyReactType;
|
||||
};
|
||||
|
||||
export interface FormilyContextProps {
|
||||
formilyModule: FormilyModule;
|
||||
retryImportFormily: () => void;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type PropsWithChildren, createContext, useState, useRef } from 'react';
|
||||
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import { type ModelFormContextProps } from './type';
|
||||
|
||||
export const ModelFromContext = createContext<ModelFormContextProps>({
|
||||
customizeValueMap: {},
|
||||
isGenerationDiversityOpen: false,
|
||||
setCustomizeValues: () => 0,
|
||||
setGenerationDiversityOpen: () => 0,
|
||||
});
|
||||
|
||||
export const ModelFormProvider: React.FC<
|
||||
PropsWithChildren<Pick<ModelFormContextProps, 'hideDiversityCollapseButton'>>
|
||||
> = ({ hideDiversityCollapseButton = false, children }) => {
|
||||
const [isGenerationDiversityOpen, setGenerationDiversityOpen] = useState(
|
||||
hideDiversityCollapseButton,
|
||||
); // 隐藏展开收起按钮时则始终展开
|
||||
const customizeValueMapRef = useRef<
|
||||
ModelFormContextProps['customizeValueMap']
|
||||
>({});
|
||||
const setCustomizeValues: ModelFormContextProps['setCustomizeValues'] = (
|
||||
modelId,
|
||||
customizeValues,
|
||||
) => {
|
||||
customizeValueMapRef.current[modelId] = cloneDeep(customizeValues);
|
||||
};
|
||||
return (
|
||||
<ModelFromContext.Provider
|
||||
value={{
|
||||
hideDiversityCollapseButton,
|
||||
isGenerationDiversityOpen,
|
||||
setCustomizeValues,
|
||||
customizeValueMap: customizeValueMapRef.current,
|
||||
setGenerationDiversityOpen,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ModelFromContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { ModelFromContext, ModelFormProvider } from './context';
|
||||
|
||||
export { ModelFormProvider };
|
||||
export const useModelForm = () => useContext(ModelFromContext);
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Dispatch, type SetStateAction } from 'react';
|
||||
|
||||
export interface ModelFormContextProps {
|
||||
isGenerationDiversityOpen: boolean;
|
||||
customizeValueMap: Record<string, Record<string, unknown>>;
|
||||
setGenerationDiversityOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setCustomizeValues: (
|
||||
modelId: string,
|
||||
customizeValues: this['customizeValueMap'][string],
|
||||
) => void;
|
||||
/**
|
||||
* 是否展示多样性设置区域的展开收起按钮
|
||||
*
|
||||
* 需求将详细配置区域放到了独立面板中,因此高度足够展示所有选项,不再需要折叠
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
hideDiversityCollapseButton?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import { isObject, merge, pick } from 'lodash-es';
|
||||
import { useCreation, useDebounceFn } from 'ahooks';
|
||||
import {
|
||||
type ModelInfo,
|
||||
ModelStyle,
|
||||
type ModelParameter,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
import {
|
||||
getModelById,
|
||||
type ModelAction,
|
||||
type ModelState,
|
||||
} from '@coze-agent-ide/bot-editor-context-store';
|
||||
|
||||
import { useGetSchema } from '../model/use-get-schema';
|
||||
import { getFixedModelFormValues } from '../../utils/model/get-fixed-model-form-values';
|
||||
import { getDiversityPresetValueByStyle } from '../../utils/model/get-diversity-preset-value-by-style';
|
||||
import { convertModelInfoToFlatObject } from '../../utils/model/convert-model-info-to-flat-object';
|
||||
import { convertFormValueToModelInfo } from '../../utils/model/convert-form-value-to-model-info';
|
||||
import { fieldInitStrategies } from '../../utils/field-init-strategy';
|
||||
import { primitiveExhaustiveCheck } from '../../utils/exhaustive-check';
|
||||
import { useModelForm } from '../../context/model-form-context';
|
||||
import { type ModelFormProps } from '../../components/model-form';
|
||||
|
||||
const specialFieldKeyList = [
|
||||
// 这个字段是个嵌套结构 需要专门转换
|
||||
'HistoryRound',
|
||||
// 这个字段需要带到表单中进行变化 表单中有组件会监听这个数据 但是他不属于 model_parameter
|
||||
'model_style',
|
||||
];
|
||||
|
||||
export interface UseHandleModelFormProps {
|
||||
currentModelId: string;
|
||||
onValuesChange: (props: { values: ModelInfo }) => void;
|
||||
getModelRecord: () => ModelInfo;
|
||||
editable: boolean;
|
||||
/**
|
||||
* 原本在 hook 内部调用 useBotEditor 获取 modelStore,这期暂且粗暴地挪出去
|
||||
* @todo 理想形态是彻底与 store 解耦,传入 get_type_list 接口返回值即可,或提供一个将接口返回值转换成所需结构的方法
|
||||
*/
|
||||
modelStore: Pick<
|
||||
ModelState & ModelAction,
|
||||
'onlineModelList' | 'offlineModelMap' | 'getModelPreset'
|
||||
>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 此处数据的流转没有做成由顶层模型设置数据完全控制表单数据,有以下几个原因:
|
||||
* 1. 需求临时变动,需要按照模型唯独记录用户设置
|
||||
* 2. 顶层数据源的设计是单例的,只能存最近模型的一份数据
|
||||
*
|
||||
* 最终设计是,由中间的表单层记录数据,通过表单的 onChange 来更新顶层数据源
|
||||
* 所以表单数据和顶层数据源不一定是一致的
|
||||
*
|
||||
* 每次切换模型,都会初始化表单。我在初始化的过程中抹平顶层数据源和表单数据的不一致
|
||||
* 并统一通过表单的 onChange 来更新顶层数据
|
||||
*/
|
||||
// eslint-disable-next-line max-lines-per-function, @coze-arch/max-line-per-function -- q
|
||||
export const useHandleModelForm = ({
|
||||
currentModelId,
|
||||
onValuesChange,
|
||||
getModelRecord,
|
||||
editable,
|
||||
modelStore,
|
||||
}: UseHandleModelFormProps) => {
|
||||
const effectId = useCreation(() => nanoid(), []);
|
||||
const formRef = useRef<Parameters<ModelFormProps['onFormInit']>[0]>();
|
||||
const { onlineModelList, offlineModelMap, getModelPreset } = modelStore;
|
||||
|
||||
const [currentModelStyle, setCurrentModelStyle] = useState<ModelStyle>();
|
||||
const { customizeValueMap, setCustomizeValues } = useModelForm();
|
||||
|
||||
const getSchema = useGetSchema();
|
||||
|
||||
const handleValuesChange = (inputValues: unknown) => {
|
||||
if (!isObject(inputValues)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const values = inputValues as Record<string, unknown>;
|
||||
const formModelInfo = convertFormValueToModelInfo(values);
|
||||
|
||||
if (currentModelStyle === ModelStyle.Custom) {
|
||||
setCustomizeValues(currentModelId, values);
|
||||
}
|
||||
|
||||
onValuesChange({ values: formModelInfo });
|
||||
};
|
||||
const handleStyleChange = (values: unknown) => {
|
||||
if (
|
||||
isObject(values) &&
|
||||
'model_style' in values &&
|
||||
typeof values.model_style === 'number'
|
||||
) {
|
||||
setCurrentModelStyle(values.model_style);
|
||||
}
|
||||
};
|
||||
const { run: debouncedHandleValuesChange } = useDebounceFn(
|
||||
handleValuesChange,
|
||||
{ wait: 200 },
|
||||
);
|
||||
|
||||
const convertFormValuesOnInit = (modelParameterList: ModelParameter[]) => {
|
||||
const presetValues = getModelPreset(currentModelId);
|
||||
if (!presetValues) {
|
||||
throw new Error(`failed to get presetValues, modelId: ${currentModelId}`);
|
||||
}
|
||||
|
||||
const modelRecord = getModelRecord();
|
||||
const { defaultValues } = presetValues;
|
||||
|
||||
// 服务端给新创建 bot 刷数据, 老数据无对应字段 fallback 到 custom
|
||||
const configModelStyle: ModelStyle =
|
||||
modelRecord.model_style ?? ModelStyle.Custom;
|
||||
const flattedModelValues = convertModelInfoToFlatObject(modelRecord);
|
||||
|
||||
const presetDiversityValue = getDiversityPresetValueByStyle(
|
||||
configModelStyle,
|
||||
presetValues,
|
||||
);
|
||||
|
||||
const customizeValue = customizeValueMap[currentModelId];
|
||||
|
||||
/**
|
||||
* 前端内存级别按照 Record<ModelId, Values> 保存用户的 custom 设置
|
||||
* 如果没有查找到用户的设置, 则沿用当前 bot/agent 的模型设置数据
|
||||
*/
|
||||
const expectValue = merge(
|
||||
{},
|
||||
flattedModelValues,
|
||||
configModelStyle === ModelStyle.Custom
|
||||
? customizeValue
|
||||
: presetDiversityValue,
|
||||
);
|
||||
|
||||
const mergeValues = merge(
|
||||
{
|
||||
model_style: configModelStyle,
|
||||
},
|
||||
defaultValues,
|
||||
expectValue,
|
||||
);
|
||||
|
||||
const fixedValues = getFixedModelFormValues(
|
||||
pick(mergeValues, Object.keys(defaultValues).concat(specialFieldKeyList)),
|
||||
modelParameterList,
|
||||
);
|
||||
|
||||
return fixedValues;
|
||||
};
|
||||
const handleModelStyleChange = () => {
|
||||
if (typeof currentModelStyle === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const modelPresetValues = getModelPreset(currentModelId);
|
||||
if (!modelPresetValues) {
|
||||
return;
|
||||
}
|
||||
if (currentModelStyle === ModelStyle.Custom) {
|
||||
const customizeValue = customizeValueMap[currentModelId];
|
||||
if (!customizeValue) {
|
||||
return;
|
||||
}
|
||||
formRef.current?.setValues(customizeValue);
|
||||
return;
|
||||
}
|
||||
const { balance, creative, precise } = modelPresetValues;
|
||||
if (currentModelStyle === ModelStyle.Balance) {
|
||||
balance && formRef.current?.setValues(balance);
|
||||
return;
|
||||
}
|
||||
if (currentModelStyle === ModelStyle.Precise) {
|
||||
precise && formRef.current?.setValues(precise);
|
||||
return;
|
||||
}
|
||||
if (currentModelStyle === ModelStyle.Creative) {
|
||||
creative && formRef.current?.setValues(creative);
|
||||
return;
|
||||
}
|
||||
|
||||
primitiveExhaustiveCheck(currentModelStyle);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
formRef.current?.setFormState({ editable });
|
||||
}, [editable]);
|
||||
|
||||
useEffect(() => {
|
||||
handleModelStyleChange();
|
||||
}, [currentModelStyle]);
|
||||
|
||||
const handleFormInit: ModelFormProps['onFormInit'] = (form, formilyCore) => {
|
||||
const localeModel = getModelById({
|
||||
onlineModelList,
|
||||
offlineModelMap,
|
||||
id: currentModelId,
|
||||
});
|
||||
const parameterList = localeModel?.model_params ?? [];
|
||||
|
||||
form.addEffects(effectId, () => {
|
||||
formilyCore.onFormValuesChange(localeForm => {
|
||||
handleStyleChange(localeForm.values);
|
||||
debouncedHandleValuesChange(localeForm.values);
|
||||
});
|
||||
parameterList.forEach(param => {
|
||||
fieldInitStrategies.forEach(strategy => {
|
||||
strategy.execute(param, formilyCore);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
form.setValues(convertFormValuesOnInit(parameterList), 'overwrite');
|
||||
form.setFormState({ editable });
|
||||
formRef.current = form;
|
||||
};
|
||||
const handleFormUnmount = () => formRef.current?.removeEffects(effectId);
|
||||
return {
|
||||
getSchema,
|
||||
handleFormInit,
|
||||
handleFormUnmount,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { uniq } from 'lodash-es';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { useMultiAgentStore } from '@coze-studio/bot-detail-store/multi-agent';
|
||||
import { useModelStore as useBotDetailModelStore } from '@coze-studio/bot-detail-store/model';
|
||||
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
|
||||
import { SpaceApi } from '@coze-arch/bot-space-api';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
import { BotMode, ModelScene } from '@coze-arch/bot-api/developer_api';
|
||||
import { useBotEditor } from '@coze-agent-ide/bot-editor-context-store';
|
||||
import {
|
||||
useBotCreatorContext,
|
||||
BotCreatorScene,
|
||||
} from '@coze-agent-ide/bot-creator-context';
|
||||
|
||||
import { ReportEventNames } from '../../report-events/report-event-names';
|
||||
|
||||
export const useGetModelList = () => {
|
||||
const {
|
||||
storeSet: { useModelStore },
|
||||
} = useBotEditor();
|
||||
const mode = useBotInfoStore(state => state.mode);
|
||||
|
||||
const { scene } = useBotCreatorContext();
|
||||
|
||||
const getModelList = async () => {
|
||||
const model = useBotDetailModelStore.getState();
|
||||
const multiAgent = useMultiAgentStore.getState();
|
||||
const agentList = multiAgent.agents;
|
||||
|
||||
const singleAgentModelId = model.config.model ?? '';
|
||||
|
||||
const agentModelIdList = uniq(
|
||||
agentList
|
||||
.map(agent => agent.model.model)
|
||||
.filter((id): id is string => Boolean(id)),
|
||||
);
|
||||
|
||||
const expectedIdList: string[] = {
|
||||
[BotMode.SingleMode]: [singleAgentModelId],
|
||||
[BotMode.MultiMode]: agentModelIdList,
|
||||
[BotMode.WorkflowMode]: [],
|
||||
}[mode];
|
||||
|
||||
const res = await SpaceApi.GetTypeList({
|
||||
cur_model_ids: expectedIdList,
|
||||
model: true,
|
||||
// 社区版暂不支持该功能
|
||||
...(scene === BotCreatorScene.DouyinBot && {
|
||||
model_scene: ModelScene.Douyin,
|
||||
}),
|
||||
});
|
||||
|
||||
const modelList = res.data.model_list;
|
||||
|
||||
if (!modelList) {
|
||||
throw new CustomError(
|
||||
ReportEventNames.GetTypeListError,
|
||||
'get model list undefined',
|
||||
);
|
||||
}
|
||||
|
||||
return modelList;
|
||||
};
|
||||
|
||||
useRequest(getModelList, {
|
||||
onSuccess: modelList => {
|
||||
const { setOnlineModelList, setOfflineModelMap } =
|
||||
useModelStore.getState();
|
||||
|
||||
setOnlineModelList(modelList.filter(model => !model.is_offline));
|
||||
|
||||
setOfflineModelMap(
|
||||
Object.fromEntries(
|
||||
modelList
|
||||
.filter(model => model.is_offline)
|
||||
.map(model => [model.model_type, model]),
|
||||
),
|
||||
);
|
||||
},
|
||||
refreshDeps: [mode],
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useBotEditor } from '@coze-agent-ide/bot-editor-context-store';
|
||||
|
||||
import { getFixedSingleAgentSchema } from '../../utils/model/get-fixed-single-agent-schema';
|
||||
import { convertModelParamsToSchema } from '../../utils/model/convert-model-params-to-schema';
|
||||
|
||||
export const useGetSchema = () => {
|
||||
const {
|
||||
storeSet: { useModelStore },
|
||||
} = useBotEditor();
|
||||
|
||||
return ({
|
||||
currentModelId,
|
||||
isSingleAgent,
|
||||
diffType,
|
||||
}: {
|
||||
currentModelId: string;
|
||||
isSingleAgent: boolean;
|
||||
diffType?: 'prompt-diff' | 'model-diff';
|
||||
}) => {
|
||||
const { getModelById } = useModelStore.getState();
|
||||
|
||||
const modelParams = getModelById(currentModelId)?.model_params ?? [];
|
||||
|
||||
const schema = convertModelParamsToSchema({ model_params: modelParams });
|
||||
|
||||
if (!isSingleAgent || diffType === 'model-diff') {
|
||||
return schema;
|
||||
}
|
||||
|
||||
return getFixedSingleAgentSchema(schema);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useModelStore as useBotDetailModelStore } from '@coze-studio/bot-detail-store/model';
|
||||
import {
|
||||
getModelById,
|
||||
useBotEditor,
|
||||
} from '@coze-agent-ide/bot-editor-context-store';
|
||||
|
||||
export const useGetSingleAgentCurrentModel = () => {
|
||||
const {
|
||||
storeSet: { useModelStore },
|
||||
} = useBotEditor();
|
||||
|
||||
const { onlineModelList, offlineModelMap } = useModelStore(
|
||||
useShallow(state => ({
|
||||
onlineModelList: state.onlineModelList,
|
||||
offlineModelMap: state.offlineModelMap,
|
||||
})),
|
||||
);
|
||||
const { model } = useBotDetailModelStore(state => state.config);
|
||||
return getModelById({
|
||||
onlineModelList,
|
||||
offlineModelMap,
|
||||
id: model ?? '',
|
||||
});
|
||||
};
|
||||
39
frontend/packages/agent-ide/model-manager/src/index.ts
Normal file
39
frontend/packages/agent-ide/model-manager/src/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { ModelForm, ModelFormProps } from './components/model-form';
|
||||
|
||||
export { convertFormValueToModelInfo } from './utils/model/convert-form-value-to-model-info';
|
||||
export { convertModelInfoToFlatObject } from './utils/model/convert-model-info-to-flat-object';
|
||||
|
||||
export { useGetSingleAgentCurrentModel } from './hooks/model/use-get-single-agent-current-model';
|
||||
export { PresetRadioGroup } from './components/model-form/preset-radio-group';
|
||||
export { MultiAgentModelForm } from './components/multi-agent/model-form';
|
||||
export { useGetModelList } from './hooks/model/use-get-model-list';
|
||||
export { useModelForm, ModelFormProvider } from './context/model-form-context';
|
||||
export { FormilyProvider } from './context/formily-context/context';
|
||||
export { getModelClassSortList } from './utils/model/get-model-class-sort-list';
|
||||
export { getModelOptionList } from './utils/model/get-model-option-list';
|
||||
|
||||
export { SingleAgentModelForm } from './components/single-agent-model-form';
|
||||
export { ModelFormItem } from './components/model-form/form-item';
|
||||
export { UIModelSelect } from './components/model-form/model-select/ui-model-select';
|
||||
|
||||
export {
|
||||
useModelCapabilityCheckAndConfirm,
|
||||
useModelCapabilityCheckModal,
|
||||
useAgentModelCapabilityCheckModal,
|
||||
} from './components/model-capability-confirm-model';
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export enum ReportEventNames {
|
||||
ImportFormilyModuleError = 'import_formily_module_error',
|
||||
GetTypeListError = 'get_type_list_error',
|
||||
FailedGetAgentById = 'failed_get_agent_by_id',
|
||||
}
|
||||
17
frontend/packages/agent-ide/model-manager/src/typings.d.ts
vendored
Normal file
17
frontend/packages/agent-ide/model-manager/src/typings.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const primitiveExhaustiveCheck = (_: never) => 0;
|
||||
export const recordExhaustiveCheck = (_: Record<string, never>) => 0;
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DataField, Form, GeneralField } from '@formily/core';
|
||||
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
|
||||
import {
|
||||
ModelStyle,
|
||||
type ModelParameter,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
import { type FormilyCoreType } from '../context/formily-context/type';
|
||||
|
||||
interface FieldInitStrategy {
|
||||
execute: (parameter: ModelParameter, formilyCore: FormilyCoreType) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户修改模型“生成多样性”参数时,切换为“自定义”模式
|
||||
*/
|
||||
class CustomModelStyleStrategy implements FieldInitStrategy {
|
||||
handler: (_field: DataField, form: Form) => void = (_field, form) =>
|
||||
form.setValues({ model_style: ModelStyle.Custom });
|
||||
|
||||
execute: FieldInitStrategy['execute'] = (
|
||||
param,
|
||||
{ onFieldInputValueChange },
|
||||
) => {
|
||||
if (param.param_class?.class_id === 1) {
|
||||
onFieldInputValueChange(param.name, this.handler);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function isDataField(field: GeneralField): field is DataField {
|
||||
return Object.prototype.hasOwnProperty.call(field, 'value');
|
||||
}
|
||||
|
||||
/**
|
||||
* 当某个字段值小于阈值时,使用 field.feedbacks 显示提示文案
|
||||
* https://core.formilyjs.org/zh-CN/api/models/field#ifieldfeedback
|
||||
*/
|
||||
class MinimumValueStrategy implements FieldInitStrategy {
|
||||
constructor(
|
||||
private name: string,
|
||||
private min: number,
|
||||
private i18nKey: I18nKeysNoOptionsType,
|
||||
) {}
|
||||
|
||||
validator: (field: GeneralField, _form: Form) => void = (field, _form) => {
|
||||
if (isDataField(field)) {
|
||||
field.setSelfWarnings(
|
||||
(field.value ?? 0) < this.min ? [I18n.t(this.i18nKey)] : undefined,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
execute: FieldInitStrategy['execute'] = (
|
||||
param,
|
||||
{ onFieldInit, onFieldValueChange },
|
||||
) => {
|
||||
if (param.name === this.name) {
|
||||
onFieldInit(this.name, this.validator);
|
||||
onFieldValueChange(this.name, this.validator);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const fieldInitStrategies = [
|
||||
new CustomModelStyleStrategy(),
|
||||
// 重复语句惩罚 < 0 时,显示提示文案
|
||||
new MinimumValueStrategy('frequency_penalty', 0, 'model_setting_alert'),
|
||||
// 最大回复长度 < 100 时,显示提示文案
|
||||
new MinimumValueStrategy('max_tokens', 100, 'model_setting_alert_2'),
|
||||
];
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type NestedObject = Record<string, never>;
|
||||
|
||||
export function flattenObject(obj: NestedObject): NestedObject {
|
||||
const flatten = Object.keys(obj).reduce((acc: NestedObject, key: string) => {
|
||||
const target = obj[key];
|
||||
if (
|
||||
typeof target === 'object' &&
|
||||
target !== null &&
|
||||
!Array.isArray(target)
|
||||
) {
|
||||
Object.assign(acc, flattenObject(target));
|
||||
} else {
|
||||
Object.assign(acc, { [key]: target });
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return flatten;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Agent } from '@coze-studio/bot-detail-store';
|
||||
import { type Dataset, FormatType } from '@coze-arch/bot-api/knowledge';
|
||||
import {
|
||||
type Model,
|
||||
ModelFuncConfigStatus,
|
||||
ModelFuncConfigType,
|
||||
RecognitionMode,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
interface AgentModelFuncConfigCheckContext {
|
||||
// agent 中的 dataset 可能缺少元信息,需要获取完整数据的方法
|
||||
getDatasetById: (id: string) => Dataset | undefined;
|
||||
config: Model['func_config'];
|
||||
}
|
||||
|
||||
type GetAgentHasValidDataByFuncConfigType = (
|
||||
agent: Agent,
|
||||
context: AgentModelFuncConfigCheckContext,
|
||||
) => boolean;
|
||||
|
||||
const getAgentHasValidDataMethodMap: {
|
||||
[key in ModelFuncConfigType]?: GetAgentHasValidDataByFuncConfigType;
|
||||
} = {
|
||||
[ModelFuncConfigType.KnowledgeText]: (agent, { getDatasetById }) =>
|
||||
!!agent.skills.knowledge.dataSetList?.some(
|
||||
item =>
|
||||
(getDatasetById(item.dataset_id ?? '') ?? item).format_type ===
|
||||
FormatType.Text,
|
||||
),
|
||||
[ModelFuncConfigType.KnowledgeTable]: (agent, { getDatasetById }) =>
|
||||
!!agent.skills.knowledge.dataSetList?.some(
|
||||
item =>
|
||||
(getDatasetById(item.dataset_id ?? '') ?? item).format_type ===
|
||||
FormatType.Table,
|
||||
),
|
||||
[ModelFuncConfigType.KnowledgePhoto]: (agent, { getDatasetById }) =>
|
||||
!!agent.skills.knowledge.dataSetList?.some(
|
||||
item =>
|
||||
(getDatasetById(item.dataset_id ?? '') ?? item).format_type ===
|
||||
FormatType.Image,
|
||||
),
|
||||
[ModelFuncConfigType.KnowledgeAutoCall]: agent =>
|
||||
!!agent.skills.knowledge.dataSetInfo.auto,
|
||||
[ModelFuncConfigType.KnowledgeOnDemandCall]: agent =>
|
||||
!agent.skills.knowledge.dataSetInfo.auto,
|
||||
[ModelFuncConfigType.Plugin]: agent => agent.skills.pluginApis.length > 0,
|
||||
[ModelFuncConfigType.Workflow]: agent => agent.skills.workflows.length > 0,
|
||||
[ModelFuncConfigType.MultiAgentRecognize]: agent =>
|
||||
agent.jump_config.recognition === RecognitionMode.FunctionCall,
|
||||
};
|
||||
|
||||
export const agentModelFuncConfigCheck = ({
|
||||
config,
|
||||
agent,
|
||||
context,
|
||||
}: {
|
||||
config: Model['func_config'];
|
||||
agent: Agent;
|
||||
context: AgentModelFuncConfigCheckContext;
|
||||
}) => {
|
||||
if (!config) {
|
||||
return { poorSupported: [], notSupported: [] };
|
||||
}
|
||||
const poorSupported: ModelFuncConfigType[] = [];
|
||||
const notSupported: ModelFuncConfigType[] = [];
|
||||
Object.entries(config).forEach(([type, status]) => {
|
||||
const hasValidData = getAgentHasValidDataMethodMap[
|
||||
type as unknown as ModelFuncConfigType
|
||||
]?.(agent, context);
|
||||
if (hasValidData) {
|
||||
if (status === ModelFuncConfigStatus.NotSupport) {
|
||||
notSupported.push(type as unknown as ModelFuncConfigType);
|
||||
}
|
||||
if (status === ModelFuncConfigStatus.PoorSupport) {
|
||||
poorSupported.push(type as unknown as ModelFuncConfigType);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { poorSupported, notSupported };
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type ShortMemPolicy,
|
||||
type ModelInfo,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
export const convertFormValueToModelInfo = (
|
||||
values: Record<string, unknown>,
|
||||
): ModelInfo => {
|
||||
const { HistoryRound, ContextContentType, ...rest } = values;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- 不适用这个 case
|
||||
const ShortMemPolicy: ShortMemPolicy = {};
|
||||
|
||||
if (typeof HistoryRound === 'number') {
|
||||
ShortMemPolicy.HistoryRound = HistoryRound;
|
||||
}
|
||||
|
||||
if (typeof ContextContentType === 'number') {
|
||||
ShortMemPolicy.ContextContentType = ContextContentType;
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
ShortMemPolicy,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ModelInfo } from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
import { type NestedObject, flattenObject } from '../flatten-object';
|
||||
|
||||
export const convertModelInfoToFlatObject = (modelInfo: ModelInfo) =>
|
||||
flattenObject(modelInfo as NestedObject);
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { groupBy, merge, uniqBy } from 'lodash-es';
|
||||
import { type SchemaTypes, type ISchema } from '@formily/react';
|
||||
import {
|
||||
type Model,
|
||||
ModelParamType,
|
||||
type ModelParameter,
|
||||
type ModelParamClass,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
import { convertModelValueType } from '@coze-agent-ide/bot-editor-context-store';
|
||||
|
||||
import {
|
||||
primitiveExhaustiveCheck,
|
||||
recordExhaustiveCheck,
|
||||
} from '../exhaustive-check';
|
||||
import {
|
||||
ModelFormComponent,
|
||||
ModelFormVoidFieldComponent,
|
||||
} from '../../constant/model-form-component';
|
||||
import { type ModelFormComponentPropsMap } from '../../components/model-form/type';
|
||||
|
||||
const precisionToStep = (precision: number | undefined) => {
|
||||
if (!precision) {
|
||||
return 1;
|
||||
}
|
||||
return Number(`0.${'0'.repeat(precision - 1)}1`);
|
||||
};
|
||||
|
||||
const getParamType = ({ type }: Pick<ModelParameter, 'type'>): SchemaTypes => {
|
||||
switch (type) {
|
||||
case ModelParamType.Float:
|
||||
case ModelParamType.Int: {
|
||||
return 'number';
|
||||
}
|
||||
case ModelParamType.Boolean: {
|
||||
return 'boolean';
|
||||
}
|
||||
default: {
|
||||
return 'string';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getParamComponent = ({
|
||||
type,
|
||||
options,
|
||||
}: Pick<ModelParameter, 'type' | 'options'>): ModelFormComponent => {
|
||||
if (options?.length) {
|
||||
return ModelFormComponent.RadioButton;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case ModelParamType.Float:
|
||||
case ModelParamType.Int: {
|
||||
return ModelFormComponent.SliderInputNumber;
|
||||
}
|
||||
case ModelParamType.Boolean: {
|
||||
return ModelFormComponent.Switch;
|
||||
}
|
||||
default: {
|
||||
return ModelFormComponent.Input;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getComponentProps = (
|
||||
component: ModelFormComponent,
|
||||
{
|
||||
max,
|
||||
min,
|
||||
label,
|
||||
precision,
|
||||
options,
|
||||
desc,
|
||||
type,
|
||||
...rest
|
||||
}: Pick<
|
||||
ModelParameter,
|
||||
'max' | 'min' | 'label' | 'precision' | 'options' | 'desc' | 'type'
|
||||
>,
|
||||
): ModelFormComponentPropsMap[ModelFormComponent] => {
|
||||
recordExhaustiveCheck(rest);
|
||||
if (component === ModelFormComponent.RadioButton) {
|
||||
return {
|
||||
type: 'button',
|
||||
options: options?.map(item => {
|
||||
const convertedValue = convertModelValueType(item.value ?? '', type);
|
||||
if (typeof convertedValue === 'boolean') {
|
||||
return {
|
||||
label: item.label,
|
||||
value: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: item.label,
|
||||
value: convertedValue,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (component === ModelFormComponent.Input) {
|
||||
return {};
|
||||
}
|
||||
if (component === ModelFormComponent.SliderInputNumber) {
|
||||
const numberedMax = Number(max);
|
||||
const numberedMin = Number(min);
|
||||
return {
|
||||
max: numberedMax,
|
||||
min: numberedMin,
|
||||
step: precisionToStep(precision),
|
||||
decimalPlaces: precision,
|
||||
};
|
||||
}
|
||||
if (component === ModelFormComponent.Switch) {
|
||||
return {};
|
||||
}
|
||||
if (component === ModelFormComponent.ModelFormItem) {
|
||||
return {
|
||||
label,
|
||||
popoverContent: desc,
|
||||
};
|
||||
}
|
||||
|
||||
primitiveExhaustiveCheck(component);
|
||||
return {};
|
||||
};
|
||||
|
||||
export const convertModelParamsToSchema = ({
|
||||
model_params: modelParams,
|
||||
}: Required<Pick<Model, 'model_params'>>): ISchema => {
|
||||
const paramClassList = uniqBy(
|
||||
modelParams
|
||||
.map(param => param.param_class)
|
||||
.filter((paramClass): paramClass is ModelParamClass =>
|
||||
Boolean(paramClass),
|
||||
),
|
||||
paramClass => paramClass.class_id,
|
||||
).sort((a, b) => (a.class_id ?? 0) - (b.class_id ?? 0));
|
||||
|
||||
const paramDictionary = groupBy(
|
||||
modelParams,
|
||||
param => param.param_class?.class_id,
|
||||
);
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
};
|
||||
|
||||
paramClassList.forEach((paramClass, index) => {
|
||||
const voidField: ISchema = {
|
||||
type: 'void',
|
||||
properties: {},
|
||||
'x-decorator':
|
||||
paramClass.class_id === 1
|
||||
? ModelFormVoidFieldComponent.ModelFormGenerationDiversityGroupItem
|
||||
: ModelFormVoidFieldComponent.ModelFormGroupItem,
|
||||
'x-decorator-props': {
|
||||
title: paramClass.label,
|
||||
},
|
||||
'x-index': index + 1,
|
||||
};
|
||||
const parameterList = paramDictionary[paramClass.class_id ?? ''];
|
||||
if (!parameterList) {
|
||||
return;
|
||||
}
|
||||
parameterList.forEach((modelParam, paramIndex) => {
|
||||
const paramField = modelParam.name;
|
||||
const component = getParamComponent(modelParam);
|
||||
const componentProps = getComponentProps(component, modelParam);
|
||||
const decoratorProps = getComponentProps(
|
||||
ModelFormComponent.ModelFormItem,
|
||||
modelParam,
|
||||
);
|
||||
const paramSchema: ISchema = {
|
||||
type: getParamType(modelParam),
|
||||
'x-component': component,
|
||||
'x-component-props': componentProps,
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-decorator-props': decoratorProps,
|
||||
'x-index': paramIndex + 1,
|
||||
};
|
||||
|
||||
merge(voidField.properties, { [paramField]: paramSchema });
|
||||
});
|
||||
merge(schema.properties, { [paramClass.class_id ?? '']: voidField });
|
||||
});
|
||||
|
||||
return schema;
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ModelStyle } from '@coze-arch/bot-api/developer_api';
|
||||
import { type ModelPresetValues } from '@coze-agent-ide/bot-editor-context-store';
|
||||
|
||||
import { primitiveExhaustiveCheck } from '../exhaustive-check';
|
||||
|
||||
export const getDiversityPresetValueByStyle = (
|
||||
style: ModelStyle,
|
||||
presetValues: ModelPresetValues,
|
||||
) => {
|
||||
if (style === ModelStyle.Balance) {
|
||||
return presetValues.balance;
|
||||
}
|
||||
if (style === ModelStyle.Creative) {
|
||||
return presetValues.creative;
|
||||
}
|
||||
if (style === ModelStyle.Precise) {
|
||||
return presetValues.precise;
|
||||
}
|
||||
if (style === ModelStyle.Custom) {
|
||||
return;
|
||||
}
|
||||
primitiveExhaustiveCheck(style);
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import {
|
||||
ModelParamType,
|
||||
type ModelParameter,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
import { convertModelValueType } from '@coze-agent-ide/bot-editor-context-store';
|
||||
|
||||
export const getFixedModelFormValues = (
|
||||
values: Record<string, unknown>,
|
||||
modelParameterList: ModelParameter[],
|
||||
) => {
|
||||
const draft = cloneDeep(values);
|
||||
|
||||
Object.keys(draft).forEach(key => {
|
||||
const targetParameter = modelParameterList.find(
|
||||
parameter => parameter.name === key,
|
||||
);
|
||||
if (!targetParameter) {
|
||||
return;
|
||||
}
|
||||
const value = draft[key];
|
||||
const parameterType = targetParameter.type;
|
||||
const { options } = targetParameter;
|
||||
|
||||
// 修正 枚举 类型的参数不在枚举范围内
|
||||
// IDL 无法写范型 转换成 string 比较
|
||||
if (options?.length) {
|
||||
if (options.findIndex(option => option.value === String(value)) >= 0) {
|
||||
return;
|
||||
}
|
||||
draft[key] = convertModelValueType(
|
||||
options.at(0)?.value ?? '',
|
||||
parameterType,
|
||||
);
|
||||
}
|
||||
|
||||
// 修正 number 类型的参数超过最大、最小值
|
||||
if (
|
||||
parameterType === ModelParamType.Float ||
|
||||
parameterType === ModelParamType.Int
|
||||
) {
|
||||
if (typeof value !== 'number') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { max, min } = targetParameter;
|
||||
|
||||
const numberedMax = Number(max);
|
||||
const numberedMin = Number(min);
|
||||
if (max && value > numberedMax) {
|
||||
draft[key] = numberedMax;
|
||||
}
|
||||
if (min && value < numberedMin) {
|
||||
draft[key] = numberedMin;
|
||||
}
|
||||
}
|
||||
});
|
||||
return draft;
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cloneDeep, isObject } from 'lodash-es';
|
||||
import { type ISchema } from '@formily/react';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { ModelFormComponent } from '../../constant/model-form-component';
|
||||
import { type ModelFormComponentPropsMap } from '../../components/model-form/type';
|
||||
|
||||
export const getFixedSingleAgentSchema = (schema: ISchema): ISchema => {
|
||||
const clonedSchema = cloneDeep(schema);
|
||||
const { properties } = clonedSchema;
|
||||
if (!properties || typeof properties === 'string') {
|
||||
return clonedSchema;
|
||||
}
|
||||
Object.entries(properties).forEach(([classId, voidField]) => {
|
||||
if (!isObject(voidField)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!voidField.properties) {
|
||||
return;
|
||||
}
|
||||
if (classId !== '2') {
|
||||
return;
|
||||
}
|
||||
|
||||
const decoratorProps: ModelFormComponentPropsMap[ModelFormComponent.ModelFormItem] =
|
||||
{
|
||||
label: I18n.t('model_config_history_round'),
|
||||
popoverContent: I18n.t('model_config_history_round_explain'),
|
||||
};
|
||||
const componentProps: ModelFormComponentPropsMap[ModelFormComponent.SliderInputNumber] =
|
||||
{
|
||||
step: 1,
|
||||
max: 100,
|
||||
min: 0,
|
||||
decimalPlaces: 0,
|
||||
};
|
||||
const historyRoundFiled: ISchema = {
|
||||
type: 'number',
|
||||
'x-component': ModelFormComponent.SliderInputNumber,
|
||||
'x-decorator': ModelFormComponent.ModelFormItem,
|
||||
'x-component-props': componentProps,
|
||||
'x-decorator-props': decoratorProps,
|
||||
'x-index': 0,
|
||||
};
|
||||
Object.assign(voidField.properties, {
|
||||
HistoryRound: historyRoundFiled,
|
||||
});
|
||||
});
|
||||
return clonedSchema;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { uniq } from 'lodash-es';
|
||||
/**
|
||||
* 以 class id 首次出现的顺序进行排序
|
||||
*/
|
||||
export const getModelClassSortList = (classIdList: string[]) =>
|
||||
uniq(classIdList);
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Model } from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
export const getModelOptionList = ({
|
||||
onlineModelList,
|
||||
offlineModelMap,
|
||||
currentModelId,
|
||||
}: {
|
||||
onlineModelList: Model[];
|
||||
offlineModelMap: Record<string, Model>;
|
||||
currentModelId: string | undefined;
|
||||
}) => {
|
||||
if (!currentModelId) {
|
||||
return onlineModelList;
|
||||
}
|
||||
const specialModel = offlineModelMap[currentModelId];
|
||||
if (!specialModel) {
|
||||
return onlineModelList;
|
||||
}
|
||||
return onlineModelList.concat([specialModel]);
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"compilerOptions": {
|
||||
"types": [],
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../arch/bot-api/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-env/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-error/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-flags/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-hooks/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-md-box-adapter/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-space-api/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-typings/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-utils/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/i18n/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../bot-editor-context-store/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../components/bot-icons/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../components/bot-semi/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/eslint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/stylelint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/ts-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/vitest-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../context/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../studio/components/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../studio/premium/premium-components-adapter/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../studio/premium/premium-store-adapter/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../studio/stores/bot-detail/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../tool-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../tool/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/packages/agent-ide/model-manager/tsconfig.json
Normal file
15
frontend/packages/agent-ide/model-manager/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
],
|
||||
"exclude": ["**/*"]
|
||||
}
|
||||
24
frontend/packages/agent-ide/model-manager/tsconfig.misc.json
Normal file
24
frontend/packages/agent-ide/model-manager/tsconfig.misc.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"include": [
|
||||
"__tests__",
|
||||
"__tests__/**/*.json",
|
||||
"vitest.config.ts",
|
||||
"tailwind.config.ts"
|
||||
],
|
||||
"exclude": ["./dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"rootDir": "./",
|
||||
"outDir": "./dist",
|
||||
"types": ["vitest/globals"],
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"noUncheckedIndexedAccess": true
|
||||
}
|
||||
}
|
||||
27
frontend/packages/agent-ide/model-manager/vitest.config.ts
Normal file
27
frontend/packages/agent-ide/model-manager/vitest.config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { defineConfig } from '@coze-arch/vitest-config';
|
||||
|
||||
export default defineConfig(
|
||||
{
|
||||
dirname: __dirname,
|
||||
preset: 'web',
|
||||
},
|
||||
{
|
||||
fixSemi: true,
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user