Merge branch 'develop' into feature/ontology_zy
This commit is contained in:
@@ -20,7 +20,7 @@ import type {
|
||||
} from './types'
|
||||
import type { Variable } from './components/VariableList/types'
|
||||
import type { KnowledgeConfig } from './components/Knowledge/types'
|
||||
import type { Model } from '@/views/ModelManagement/types'
|
||||
import type { ModelListItem } from '@/views/ModelManagement/types'
|
||||
import { getModelList } from '@/api/models';
|
||||
import { saveAgentConfig } from '@/api/application'
|
||||
import Knowledge from './components/Knowledge/Knowledge'
|
||||
@@ -96,8 +96,8 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [data, setData] = useState<Config | null>(null);
|
||||
const modelConfigModalRef = useRef<ModelConfigModalRef>(null)
|
||||
const [modelList, setModelList] = useState<Model[]>([])
|
||||
const [defaultModel, setDefaultModel] = useState<Model | null>(null)
|
||||
const [modelList, setModelList] = useState<ModelListItem[]>([])
|
||||
const [defaultModel, setDefaultModel] = useState<ModelListItem | null>(null)
|
||||
const [chatList, setChatList] = useState<ChatData[]>([])
|
||||
const values = Form.useWatch<Config>([], form)
|
||||
const [isSave, setIsSave] = useState(false)
|
||||
@@ -212,7 +212,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
...data.knowledge_retrieval,
|
||||
...knowledgeRest,
|
||||
knowledge_bases: knowledge_bases.map(item => ({
|
||||
kb_id: item.id,
|
||||
kb_id: item.kb_id || item.id,
|
||||
...(item.config || {})
|
||||
}))
|
||||
} as KnowledgeConfig : null,
|
||||
@@ -237,9 +237,9 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
})
|
||||
}
|
||||
const getModels = () => {
|
||||
getModelList({ type: 'llm,chat', pagesize: 100, page: 1 })
|
||||
getModelList({ type: 'llm,chat', pagesize: 100, page: 1, is_active: true })
|
||||
.then(res => {
|
||||
const response = res as { items: Model[] }
|
||||
const response = res as { items: ModelListItem[] }
|
||||
setModelList(response.items)
|
||||
})
|
||||
}
|
||||
@@ -249,7 +249,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
useEffect(() => {
|
||||
if (values?.default_model_config_id && modelList.length > 0) {
|
||||
const filterValue = modelList.find(item => item.id === values.default_model_config_id)
|
||||
setDefaultModel(filterValue as Model | null)
|
||||
setDefaultModel(filterValue as ModelListItem | null)
|
||||
setChatList([{
|
||||
label: filterValue?.name || '',
|
||||
model_config_id: filterValue?.id || '',
|
||||
|
||||
@@ -225,7 +225,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
||||
<Form.Item name="default_model_config_id" noStyle>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'llm,chat', pagesize: 100 }}
|
||||
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
|
||||
86
web/src/views/ApplicationConfig/Statistics.tsx
Normal file
86
web/src/views/ApplicationConfig/Statistics.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { type FC, useState, useEffect } from 'react';
|
||||
import { Row, Col, Flex, DatePicker } from 'antd';
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
import type { Application } from '@/views/ApplicationManagement/types'
|
||||
import { getAppStatistics } from '@/api/application';
|
||||
import LineCard from './components/LineCard'
|
||||
import type { StatisticsData, StatisticsItem } from './types'
|
||||
|
||||
const TotalObj: Record<string, keyof StatisticsData> = {
|
||||
daily_conversations: 'total_conversations',
|
||||
daily_new_users: 'total_new_users',
|
||||
daily_api_calls: 'total_api_calls',
|
||||
daily_tokens: 'total_tokens',
|
||||
}
|
||||
const Statistics: FC<{ application: Application | null }> = ({ application }) => {
|
||||
const [data, setData] = useState<StatisticsData>({
|
||||
daily_conversations: [],
|
||||
total_conversations: 0,
|
||||
daily_new_users: [],
|
||||
total_new_users: 0,
|
||||
daily_api_calls: [],
|
||||
total_api_calls: 0,
|
||||
daily_tokens: [],
|
||||
total_tokens: 0
|
||||
})
|
||||
const [query, setQuery] = useState({
|
||||
start_date: dayjs().subtract(6, 'd'),
|
||||
end_date: dayjs().subtract(0, 'd'),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
getData()
|
||||
}, [application, query])
|
||||
const getData = () => {
|
||||
if (!application?.id) {
|
||||
return
|
||||
}
|
||||
const params = {
|
||||
start_date: query.start_date.startOf('d').valueOf(),
|
||||
end_date: query.end_date.endOf('d').valueOf(),
|
||||
}
|
||||
|
||||
getAppStatistics(application.id, params)
|
||||
.then(res => {
|
||||
setData(res as StatisticsData)
|
||||
})
|
||||
}
|
||||
const handleChange = (date: [Dayjs | null, Dayjs | null] | null) => {
|
||||
if (!date || !date[0] || !date[1]) return
|
||||
setQuery({
|
||||
start_date: date[0],
|
||||
end_date: date[1],
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div className="rb:w-250 rb:mt-5 rb:pb-5 rb:mx-auto">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Flex justify="end">
|
||||
<RangePicker defaultValue={[query.start_date, query.end_date]} onChange={handleChange} />
|
||||
</Flex>
|
||||
</Col>
|
||||
{Object.entries(data).map(([key, value]) => {
|
||||
if (key.includes('total')) {
|
||||
return null
|
||||
}
|
||||
const totalKey = TotalObj[key];
|
||||
return (
|
||||
<Col span={12} key={key}>
|
||||
<LineCard
|
||||
type={key}
|
||||
total={totalKey ? (data[totalKey] as number) : 0}
|
||||
chartData={value as StatisticsItem[]}
|
||||
/>
|
||||
</Col>
|
||||
)
|
||||
})}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default Statistics;
|
||||
@@ -181,7 +181,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'llm,chat', pagesize: 100 }}
|
||||
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
|
||||
@@ -17,7 +17,7 @@ import CopyModal from './CopyModal'
|
||||
|
||||
const { Header } = Layout;
|
||||
|
||||
const tabKeys = ['arrangement', 'api', 'release']
|
||||
const tabKeys = ['arrangement', 'api', 'release', 'statistics']
|
||||
const menuIcons: Record<string, string> = {
|
||||
edit: editIcon,
|
||||
copy: copyIcon,
|
||||
|
||||
@@ -66,7 +66,7 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
|
||||
useEffect(() => {
|
||||
if (values?.retrieve_type) {
|
||||
const fieldsToReset = Object.keys(values).filter(key =>
|
||||
key !== 'kb_id' && key !== 'retrieve_type'
|
||||
key !== 'kb_id' && key !== 'retrieve_type' && key !== 'top_k'
|
||||
) as (keyof KnowledgeConfigForm)[];
|
||||
form.resetFields(fieldsToReset);
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'rerank', pagesize: 100 }}
|
||||
params={{ type: 'rerank', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
|
||||
127
web/src/views/ApplicationConfig/components/LineCard.tsx
Normal file
127
web/src/views/ApplicationConfig/components/LineCard.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { type FC, useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactEcharts from 'echarts-for-react';
|
||||
import * as echarts from 'echarts';
|
||||
import Empty from '@/components/Empty'
|
||||
|
||||
import Card from './Card'
|
||||
import type { StatisticsItem } from '../types'
|
||||
|
||||
interface LineCardProps {
|
||||
chartData: StatisticsItem[];
|
||||
type: string;
|
||||
total: number;
|
||||
}
|
||||
|
||||
const SeriesConfig = {
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 3
|
||||
},
|
||||
showSymbol: true,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'top'
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
}
|
||||
|
||||
const ColorObj: Record<string, string> = {
|
||||
daily_conversations: '#FFB048',
|
||||
daily_new_users: '#4DA8FF',
|
||||
daily_api_calls: '#155EEF',
|
||||
daily_tokens: '#AD88FF'
|
||||
}
|
||||
|
||||
const LineCard: FC<LineCardProps> = ({ chartData, type, total }) => {
|
||||
const { t } = useTranslation()
|
||||
const chartRef = useRef<ReactEcharts>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
}, [chartData])
|
||||
|
||||
const getSeries = () => {
|
||||
return [{
|
||||
...SeriesConfig,
|
||||
name: t(`application.${type}`),
|
||||
data: chartData.map(vo => vo.count),
|
||||
areaStyle: {
|
||||
opacity: 0.8,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: ColorObj[type] },
|
||||
{ offset: 1, color: '#FFFFFF' }
|
||||
])
|
||||
},
|
||||
}]
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={<div>{t(`application.${type}`)} <span className="rb:text-[#155EEF] rb:font-medium rb:text-[18px]">{total}</span></div>}
|
||||
>
|
||||
{chartData && chartData.length > 0 ? (
|
||||
<ReactEcharts
|
||||
ref={chartRef}
|
||||
option={{
|
||||
color: [ColorObj[type]],
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
extraCssText: 'box-shadow: 0px 2px 6px 0px rgba(33,35,50,0.16); border-radius: 8px;',
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
crossStyle: {
|
||||
color: '#5F6266',
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#5F6266',
|
||||
},
|
||||
label: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
grid: {
|
||||
top: 10,
|
||||
left: 15,
|
||||
right: 40,
|
||||
bottom: 0,
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: chartData.map(item => item.date),
|
||||
boundaryGap: false,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: '#A8A9AA',
|
||||
fontFamily: 'PingFangSC, PingFang SC',
|
||||
align: 'right',
|
||||
lineHeight: 17,
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
}
|
||||
},
|
||||
},
|
||||
series: getSeries()
|
||||
}}
|
||||
style={{ height: '265px', width: '100%', minWidth: '100%', boxSizing: 'border-box' }}
|
||||
opts={{ renderer: 'canvas' }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
) : <Empty size={120} className="rb:mt-12 rb:mb-20.25" />}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default LineCard
|
||||
@@ -9,6 +9,7 @@ import ReleasePage from './ReleasePage'
|
||||
import Cluster from './Cluster'
|
||||
import { getApplication } from '@/api/application'
|
||||
import Workflow from '@/views/Workflow';
|
||||
import Statistics from './Statistics'
|
||||
|
||||
const ApplicationConfig: React.FC = () => {
|
||||
const { id } = useParams();
|
||||
@@ -68,6 +69,7 @@ const ApplicationConfig: React.FC = () => {
|
||||
{activeTab === 'arrangement' && application?.type === 'workflow' && <Workflow ref={workflowRef} />}
|
||||
{activeTab === 'api' && <Api application={application} />}
|
||||
{activeTab === 'release' && <ReleasePage data={application as Application} refresh={getApplicationInfo} />}
|
||||
{activeTab === 'statistics' && <Statistics application={application} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -150,4 +150,19 @@ export interface AiPromptForm {
|
||||
}
|
||||
export interface ChatVariableConfigModalRef {
|
||||
handleOpen: (values: Variable[]) => void;
|
||||
}
|
||||
|
||||
export interface StatisticsItem {
|
||||
count: number;
|
||||
date: string;
|
||||
}
|
||||
export interface StatisticsData {
|
||||
daily_conversations: StatisticsItem[];
|
||||
daily_new_users: StatisticsItem[];
|
||||
daily_api_calls: StatisticsItem[];
|
||||
daily_tokens: StatisticsItem[];
|
||||
total_conversations: number;
|
||||
total_new_users: number;
|
||||
total_api_calls: number;
|
||||
total_tokens: number;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ const configList = [
|
||||
key: 'emotion_model_id',
|
||||
type: 'customSelect',
|
||||
url: getModelListUrl,
|
||||
params: { type: 'chat,llm', page: 1, pagesize: 100 }, // chat,llm
|
||||
params: { type: 'chat,llm', page: 1, pagesize: 100, is_active: true }, // chat,llm
|
||||
},
|
||||
{
|
||||
key: 'emotion_min_intensity',
|
||||
|
||||
@@ -39,7 +39,7 @@ const MemberManagement: React.FC = () => {
|
||||
onOk: () => {
|
||||
deleteMember(member.id)
|
||||
.then(() => {
|
||||
message.success(t('member.deleteSuccess'));
|
||||
message.success(t('common.deleteSuccess'));
|
||||
refreshTable();
|
||||
})
|
||||
}
|
||||
@@ -93,7 +93,7 @@ const MemberManagement: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rb:flex rb:justify-end rb:mb-[12px]">
|
||||
<div className="rb:flex rb:justify-end rb:mb-3">
|
||||
<Button type="primary" onClick={() => handleEdit()}>
|
||||
{t('member.createMember')}
|
||||
</Button>
|
||||
|
||||
@@ -45,7 +45,7 @@ const searchSwitchList = [
|
||||
]
|
||||
|
||||
export interface TestParams {
|
||||
group_id: string;
|
||||
end_user_id: string;
|
||||
message: string;
|
||||
search_switch: string;
|
||||
history: { role: string; content: string }[];
|
||||
@@ -107,7 +107,7 @@ const MemoryConversation: FC = () => {
|
||||
setLoading(true)
|
||||
readService({
|
||||
message: msg,
|
||||
group_id: userId,
|
||||
end_user_id: userId,
|
||||
search_switch: search_switch,
|
||||
history: [],
|
||||
})
|
||||
@@ -204,7 +204,7 @@ const MemoryConversation: FC = () => {
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-[22px] rb:mb-6">{log.title}</div>
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-5.5 rb:mb-6">{log.title}</div>
|
||||
{log.type === 'problem_split' && Array.isArray(log.data) && log.data.length > 0
|
||||
? <Space size={12} direction="vertical" style={{width: '100%'}}>
|
||||
{log.data.map(vo => (
|
||||
|
||||
@@ -1093,606 +1093,4 @@ export const groupDataByType = (data: any[], groupKey: string) => {
|
||||
})
|
||||
|
||||
return grouped
|
||||
}
|
||||
|
||||
export const mockTestResult = {
|
||||
"generated_at": "2025-12-12T09:48:43.389893",
|
||||
"entities": {
|
||||
"extracted_count": 148
|
||||
},
|
||||
"dedup": {
|
||||
"total_merged_count": 39,
|
||||
"breakdown": {
|
||||
"exact": 30,
|
||||
"fuzzy": 0,
|
||||
"llm": 9
|
||||
},
|
||||
"impact": [
|
||||
{
|
||||
"name": "记忆熊",
|
||||
"type": "Person",
|
||||
"appear_count": 9,
|
||||
"merge_count": 8
|
||||
},
|
||||
{
|
||||
"name": "宋朝",
|
||||
"type": "Organization",
|
||||
"appear_count": 5,
|
||||
"merge_count": 2
|
||||
},
|
||||
{
|
||||
"name": "军费",
|
||||
"type": "EconomicMetric",
|
||||
"appear_count": 2,
|
||||
"merge_count": 1
|
||||
},
|
||||
{
|
||||
"name": "学生",
|
||||
"type": "Person",
|
||||
"appear_count": 6,
|
||||
"merge_count": 5
|
||||
},
|
||||
{
|
||||
"name": "废除丞相制度",
|
||||
"type": "Event",
|
||||
"appear_count": 6,
|
||||
"merge_count": 3
|
||||
},
|
||||
{
|
||||
"name": "六部",
|
||||
"type": "Organization",
|
||||
"appear_count": 4,
|
||||
"merge_count": 3
|
||||
},
|
||||
{
|
||||
"name": "六部缺乏协调机制",
|
||||
"type": "Concept",
|
||||
"appear_count": 2,
|
||||
"merge_count": 1
|
||||
},
|
||||
{
|
||||
"name": "丞相",
|
||||
"type": "Position",
|
||||
"appear_count": 4,
|
||||
"merge_count": 1
|
||||
},
|
||||
{
|
||||
"name": "总理",
|
||||
"type": "Position",
|
||||
"appear_count": 2,
|
||||
"merge_count": 1
|
||||
},
|
||||
{
|
||||
"name": "各部委",
|
||||
"type": "Organization",
|
||||
"appear_count": 2,
|
||||
"merge_count": 1
|
||||
},
|
||||
{
|
||||
"name": "六部直接对皇帝负责",
|
||||
"type": "AdministrativeStructure",
|
||||
"appear_count": 2,
|
||||
"merge_count": 1
|
||||
},
|
||||
{
|
||||
"name": "秦国",
|
||||
"type": "Organization",
|
||||
"appear_count": 5,
|
||||
"merge_count": 2
|
||||
},
|
||||
{
|
||||
"name": "文官集团",
|
||||
"type": "Organization",
|
||||
"appear_count": 2,
|
||||
"merge_count": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"disambiguation": {
|
||||
"block_count": 1,
|
||||
"effects": [
|
||||
{
|
||||
"left": {
|
||||
"name": "节度使",
|
||||
"type": "Role"
|
||||
},
|
||||
"right": {
|
||||
"name": "节度使",
|
||||
"type": "Person"
|
||||
},
|
||||
"result": "成功区分"
|
||||
}
|
||||
]
|
||||
},
|
||||
"memory": {
|
||||
"chunks": 2
|
||||
},
|
||||
"triplets": {
|
||||
"count": 88
|
||||
},
|
||||
"core_entities": [
|
||||
{
|
||||
"type": "Organization",
|
||||
"type_cn": "组织",
|
||||
"count": 16,
|
||||
"entities": [
|
||||
"厂卫机构",
|
||||
"西厂",
|
||||
"东厂",
|
||||
"工部",
|
||||
"地方军阀"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Event",
|
||||
"type_cn": "事件",
|
||||
"count": 12,
|
||||
"entities": [
|
||||
"均田制瓦解",
|
||||
"无法批阅完所有政务",
|
||||
"废除丞相制度",
|
||||
"持续战争",
|
||||
"政令执行困难"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Condition",
|
||||
"type_cn": "Condition",
|
||||
"count": 9,
|
||||
"entities": [
|
||||
"缺乏协作机制",
|
||||
"作战效率低下",
|
||||
"厢军装备不足",
|
||||
"军权分散",
|
||||
"军事专业化难以提升"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Person",
|
||||
"type_cn": "人物",
|
||||
"count": 8,
|
||||
"entities": [
|
||||
"官员",
|
||||
"宦官",
|
||||
"节度使",
|
||||
"皇帝",
|
||||
"文士"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Concept",
|
||||
"type_cn": "Concept",
|
||||
"count": 8,
|
||||
"entities": [
|
||||
"行政紧张",
|
||||
"军力不足",
|
||||
"秦国统一六国的原因",
|
||||
"六部缺乏协调机制",
|
||||
"专业分工"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Action",
|
||||
"type_cn": "Action",
|
||||
"count": 6,
|
||||
"entities": [
|
||||
"再花钱募兵",
|
||||
"建立军功爵制度",
|
||||
"裁撤兵员",
|
||||
"削减装备",
|
||||
"建立法律制度"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Outcome",
|
||||
"type_cn": "Outcome",
|
||||
"count": 5,
|
||||
"entities": [
|
||||
"打仗更吃亏",
|
||||
"提升国家组织能力",
|
||||
"降低行政效率",
|
||||
"士兵效忠个人而非国家",
|
||||
"政令推行困难"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "EconomicMetric",
|
||||
"type_cn": "EconomicMetric",
|
||||
"count": 4,
|
||||
"entities": [
|
||||
"财政",
|
||||
"财政支出",
|
||||
"支出",
|
||||
"军费"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Statement",
|
||||
"type_cn": "Statement",
|
||||
"count": 3,
|
||||
"entities": [
|
||||
"没有银子",
|
||||
"禁军由文官控制导致作战效率低下",
|
||||
"武器没材料"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "State",
|
||||
"type_cn": "State",
|
||||
"count": 3,
|
||||
"entities": [
|
||||
"军队更弱",
|
||||
"理解不足",
|
||||
"不足"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "HistoricalPeriod",
|
||||
"type_cn": "HistoricalPeriod",
|
||||
"count": 3,
|
||||
"entities": [
|
||||
"春秋战国史",
|
||||
"唐朝史",
|
||||
"宋朝"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Attribute",
|
||||
"type_cn": "Attribute",
|
||||
"count": 3,
|
||||
"entities": [
|
||||
"资源丰富",
|
||||
"易守难攻",
|
||||
"政策连续性强"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Right",
|
||||
"type_cn": "Right",
|
||||
"count": 3,
|
||||
"entities": [
|
||||
"军事指挥权",
|
||||
"财政调度权",
|
||||
"募兵权"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Policy",
|
||||
"type_cn": "Policy",
|
||||
"count": 2,
|
||||
"entities": [
|
||||
"商鞅变法",
|
||||
"禁军由文官控制"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "MilitaryCondition",
|
||||
"type_cn": "MilitaryCondition",
|
||||
"count": 2,
|
||||
"entities": [
|
||||
"军力不足",
|
||||
"缺乏战略纵深"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Role",
|
||||
"type_cn": "Role",
|
||||
"count": 2,
|
||||
"entities": [
|
||||
"节度使",
|
||||
"协调中枢"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Position",
|
||||
"type_cn": "Position",
|
||||
"count": 2,
|
||||
"entities": [
|
||||
"总理",
|
||||
"丞相"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "PoliticalCharacteristic",
|
||||
"type_cn": "PoliticalCharacteristic",
|
||||
"count": 2,
|
||||
"entities": [
|
||||
"旧贵族势力弱",
|
||||
"中央集权程度高"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Phenomenon",
|
||||
"type_cn": "Phenomenon",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"宋朝军事弱势"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Factor",
|
||||
"type_cn": "Factor",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"制度性因素"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "EconomicFactor",
|
||||
"type_cn": "EconomicFactor",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"财政压力"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "EconomicIndicator",
|
||||
"type_cn": "EconomicIndicator",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"财政支出"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "MilitaryStrategy",
|
||||
"type_cn": "MilitaryStrategy",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"对外战略被动"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "MilitaryCapability",
|
||||
"type_cn": "MilitaryCapability",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"机动能力弱"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "PersonGroup",
|
||||
"type_cn": "PersonGroup",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"武将"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "EconomicCondition",
|
||||
"type_cn": "EconomicCondition",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"财政压力"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "InstitutionalPolicy",
|
||||
"type_cn": "InstitutionalPolicy",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"废除丞相制度"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "StateOfAffairs",
|
||||
"type_cn": "StateOfAffairs",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"中央决策高度集中于皇帝"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Institution",
|
||||
"type_cn": "Institution",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"科举"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Function",
|
||||
"type_cn": "Function",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"统筹大事小情"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "AdministrativeStructure",
|
||||
"type_cn": "AdministrativeStructure",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"六部直接对皇帝负责"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "AdministrativeProblem",
|
||||
"type_cn": "AdministrativeProblem",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"皇帝一人批不完政务"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Behavior",
|
||||
"type_cn": "Behavior",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"互相推诿责任"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Resource",
|
||||
"type_cn": "Resource",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"银子"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Situation",
|
||||
"type_cn": "Situation",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"没人拍板"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "HistoricalState",
|
||||
"type_cn": "HistoricalState",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"秦国"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Location",
|
||||
"type_cn": "地点",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"关中"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "HistoricalEvent",
|
||||
"type_cn": "HistoricalEvent",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"安史之乱"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "PoliticalAction",
|
||||
"type_cn": "PoliticalAction",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"中央整顿"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "PoliticalPhenomenon",
|
||||
"type_cn": "PoliticalPhenomenon",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"藩镇割据加剧"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "EconomicEntity",
|
||||
"type_cn": "EconomicEntity",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"中央财政"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "System",
|
||||
"type_cn": "System",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"募兵制"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "WorkRole",
|
||||
"type_cn": "WorkRole",
|
||||
"count": 1,
|
||||
"entities": [
|
||||
"掌控禁军"
|
||||
]
|
||||
}
|
||||
],
|
||||
"triplet_samples": [
|
||||
{
|
||||
"subject": "记忆熊",
|
||||
"predicate": "MENTIONS",
|
||||
"predicate_cn": "提到",
|
||||
"object": "宋朝军事弱势"
|
||||
},
|
||||
{
|
||||
"subject": "宋朝军事弱势",
|
||||
"predicate": "RESULTED_IN",
|
||||
"predicate_cn": "resulted in",
|
||||
"object": "制度性因素"
|
||||
},
|
||||
{
|
||||
"subject": "记忆熊",
|
||||
"predicate": "MENTIONS",
|
||||
"predicate_cn": "提到",
|
||||
"object": "禁军由文官控制导致作战效率低下"
|
||||
},
|
||||
{
|
||||
"subject": "禁军由文官控制",
|
||||
"predicate": "RESULTED_IN",
|
||||
"predicate_cn": "resulted in",
|
||||
"object": "作战效率低下"
|
||||
},
|
||||
{
|
||||
"subject": "记忆熊",
|
||||
"predicate": "MENTIONS",
|
||||
"predicate_cn": "提到",
|
||||
"object": "厢军装备不足"
|
||||
},
|
||||
{
|
||||
"subject": "记忆熊",
|
||||
"predicate": "MENTIONS",
|
||||
"predicate_cn": "提到",
|
||||
"object": "宋朝"
|
||||
},
|
||||
{
|
||||
"subject": "记忆熊",
|
||||
"predicate": "MENTIONS",
|
||||
"predicate_cn": "提到",
|
||||
"object": "军费"
|
||||
}
|
||||
],
|
||||
"self_reflexion": [
|
||||
{
|
||||
"conflict": {
|
||||
"data": [
|
||||
{
|
||||
"id": "76be6d82d8804beda6baa3d3447d6cbc",
|
||||
"statement": "学生对\"六部缺乏协调机制\"的具体影响表示理解不足。",
|
||||
"group_id": "group_123",
|
||||
"chunk_id": "4a0804127d35456f86d4f06e1fa458f7",
|
||||
"created_at": "2025-12-12 09:48:00.166068",
|
||||
"expired_at": null,
|
||||
"valid_at": null,
|
||||
"invalid_at": null,
|
||||
"entity_ids": []
|
||||
}
|
||||
],
|
||||
"conflict": true,
|
||||
"conflict_memory": {
|
||||
"id": "e268a6fff35543fab471986c188e023e",
|
||||
"statement": "学生对\"六部缺乏协调机制\"的具体影响表示理解不足。",
|
||||
"group_id": "group_123",
|
||||
"chunk_id": "e6cb5f56020e4a8d925d148e1d2fbda0",
|
||||
"created_at": "2025-12-12 09:48:00.166068",
|
||||
"expired_at": null,
|
||||
"valid_at": null,
|
||||
"invalid_at": null,
|
||||
"entity_ids": []
|
||||
}
|
||||
},
|
||||
"reflexion": {
|
||||
"reason": "同一学生在不同时间点重复提出对'六部缺乏协调机制'具体影响的理解困难,表明原有解释未能有效解决其认知障碍,存在记忆冗余与教学反馈失效的冲突。",
|
||||
"solution": "保留后出现的记忆记录(chunk_id为4a0804127d35456f86d4f06e1fa458f7)作为最新学习状态,将其设为有效;将前次相同内容的记忆(id为e268a6fff35543fab471986c188e023e)标记为失效,避免重复干预,并基于后续完整解释优化知识呈现逻辑。"
|
||||
},
|
||||
"resolved": {
|
||||
"original_memory_id": "e268a6fff35543fab471986c188e023e",
|
||||
"resolved_memory": {
|
||||
"id": "e268a6fff35543fab471986c188e023e",
|
||||
"statement": "学生对\"六部缺乏协调机制\"的具体影响表示理解不足。",
|
||||
"group_id": "group_123",
|
||||
"chunk_id": "e6cb5f56020e4a8d925d148e1d2fbda0",
|
||||
"created_at": "2025-12-12 09:48:00.166068",
|
||||
"expired_at": null,
|
||||
"valid_at": null,
|
||||
"invalid_at": "2025-12-12 09:48:00.166068",
|
||||
"entity_ids": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { type FC, useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Row, Col, Space, Switch, Select, InputNumber, Slider, App, Form } from 'antd'
|
||||
import { Row, Col, Space, Select, InputNumber, Slider, App, Form } from 'antd'
|
||||
import clsx from 'clsx'
|
||||
import Card from './components/Card'
|
||||
import type { ConfigForm, Variable } from './types'
|
||||
import { getMemoryExtractionConfig, updateMemoryExtractionConfig } from '@/api/memory'
|
||||
import Markdown from '@/components/Markdown'
|
||||
import { getModelList } from '@/api/models';
|
||||
import type { Model } from '@/views/ModelManagement/types'
|
||||
import type { ModelListItem } from '@/views/ModelManagement/types'
|
||||
import { configList } from './constant'
|
||||
import Result from './components/Result'
|
||||
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
|
||||
@@ -43,7 +43,7 @@ const MemoryExtractionEngine: FC = () => {
|
||||
const values = Form.useWatch<ConfigForm>([], form)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [iterationPeriodDisabled, setIterationPeriodDisabled] = useState(false)
|
||||
const [modelList, setModelList] = useState<Model[]>([])
|
||||
const [modelList, setModelList] = useState<ModelListItem[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (values?.reflexion_range === 'database') {
|
||||
@@ -55,9 +55,9 @@ const MemoryExtractionEngine: FC = () => {
|
||||
}, [values])
|
||||
|
||||
const getModels = () => {
|
||||
getModelList({ type: 'llm,chat', pagesize: 100, page: 1 })
|
||||
getModelList({ type: 'llm,chat', pagesize: 100, page: 1, is_active: true })
|
||||
.then(res => {
|
||||
const response = res as { items: Model[] }
|
||||
const response = res as { items: ModelListItem[] }
|
||||
setModelList(response.items)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ export interface Memory {
|
||||
include_dialogue_context: boolean;
|
||||
max_context: string;
|
||||
lambda_mem: string;
|
||||
lambda_mem: string;
|
||||
offset: string;
|
||||
state: boolean;
|
||||
created_at: string;
|
||||
|
||||
92
web/src/views/ModelManagement/Group.tsx
Normal file
92
web/src/views/ModelManagement/Group.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||
import clsx from 'clsx'
|
||||
import { Button } from 'antd'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ProviderModelItem, ModelListItem, DescriptionItem, BaseRef } from './types'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import { getModelNewList } from '@/api/models'
|
||||
import PageEmpty from '@/components/Empty/PageEmpty';
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
|
||||
const Group = forwardRef <BaseRef,{ query: any; handleEdit: (data: ModelListItem) => void; }>(({ query, handleEdit }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [list, setList] = useState<ModelListItem[]>([])
|
||||
useEffect(() => {
|
||||
getList()
|
||||
}, [query])
|
||||
const getList = () => {
|
||||
getModelNewList({
|
||||
...query,
|
||||
is_composite: true,
|
||||
is_active: true,
|
||||
})
|
||||
.then(res => {
|
||||
const response = res as ProviderModelItem[]
|
||||
setList(response[0]?.models || [])
|
||||
})
|
||||
}
|
||||
const formatData = (data: ModelListItem) => {
|
||||
return [
|
||||
{
|
||||
key: 'type',
|
||||
label: t(`modelNew.type`),
|
||||
children: data.type ? t(`modelNew.${data.type}`) : '-',
|
||||
},
|
||||
{
|
||||
key: 'is_active',
|
||||
label: t(`modelNew.status`),
|
||||
children: data.is_active ? t(`common.statusEnabled`) : t(`common.statusDisabled`),
|
||||
},
|
||||
{
|
||||
key: 'created_at',
|
||||
label: t(`modelNew.created_at`),
|
||||
children: data.created_at ? formatDateTime(data.created_at, 'YYYY-MM-DD HH:mm:ss') : '-',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getList,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
{list.length === 0
|
||||
? <PageEmpty />
|
||||
:(
|
||||
<div className="rb:grid rb:grid-cols-4 rb:gap-4">
|
||||
{list.map(item => (
|
||||
<RbCard
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
avatarUrl={item.logo}
|
||||
avatar={
|
||||
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
{item.name[0]}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{formatData(item)?.map((description: DescriptionItem) => (
|
||||
<div
|
||||
key={description.key}
|
||||
className="rb:flex rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3"
|
||||
>
|
||||
<span className="rb:whitespace-nowrap">{(description.label as string)}</span>
|
||||
<span className={clsx({
|
||||
"rb:text-[#212332]": description.key !== 'is_active',
|
||||
"rb:text-[#369F21] rb:font-medium": description.key === 'is_active' && item.is_active,
|
||||
})}>{(description.children as string)}</span>
|
||||
</div>
|
||||
))}
|
||||
<Button className="rb:mt-2" type="primary" ghost block onClick={() => handleEdit(item)}>{t('modelNew.configureBtn')}</Button>
|
||||
</RbCard>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default Group
|
||||
86
web/src/views/ModelManagement/List.tsx
Normal file
86
web/src/views/ModelManagement/List.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { useRef, useState, useEffect, type FC } from 'react';
|
||||
import { Button, Flex, Row, Col } from 'antd'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ProviderModelItem, KeyConfigModalRef, ModelListDetailRef } from './types'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import { getModelNewList } from '@/api/models'
|
||||
import PageEmpty from '@/components/Empty/PageEmpty';
|
||||
import Tag from '@/components/Tag';
|
||||
import KeyConfigModal from './components/KeyConfigModal'
|
||||
import ModelListDetail from './components/ModelListDetail'
|
||||
import { getLogoUrl } from './utils'
|
||||
|
||||
const ModelList: FC<{ query: any }> = ({ query }) => {
|
||||
const { t } = useTranslation();
|
||||
const keyConfigModalRef = useRef<KeyConfigModalRef>(null)
|
||||
const modelListDetailRef = useRef<ModelListDetailRef>(null)
|
||||
const [list, setList] = useState<ProviderModelItem[]>([])
|
||||
useEffect(() => {
|
||||
getList()
|
||||
}, [query])
|
||||
const getList = () => {
|
||||
getModelNewList({
|
||||
...query,
|
||||
is_composite: false,
|
||||
})
|
||||
.then(res => {
|
||||
setList((res || []) as ProviderModelItem[])
|
||||
})
|
||||
}
|
||||
|
||||
const handleShowModel = (vo: ProviderModelItem) => {
|
||||
modelListDetailRef.current?.handleOpen(vo)
|
||||
}
|
||||
const handleKeyConfig = (vo: ProviderModelItem) => {
|
||||
keyConfigModalRef.current?.handleOpen(vo)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{list.length === 0
|
||||
? <PageEmpty />
|
||||
:(
|
||||
<div className="rb:grid rb:grid-cols-4 rb:gap-4">
|
||||
{list.map(item => (
|
||||
<RbCard
|
||||
key={item.provider}
|
||||
title={t(`modelNew.${item.provider}`)}
|
||||
avatarUrl={getLogoUrl(item.logo)}
|
||||
avatar={
|
||||
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
{item.provider[0].toUpperCase()}
|
||||
</div>
|
||||
}
|
||||
bodyClassName="rb:relative rb:pb-[64px]! rb:h-[calc(100%-64px)]!"
|
||||
>
|
||||
<Flex gap={8} wrap>{item.tags.map(tag => <Tag key={tag}>{t(`modelNew.${tag}`)}</Tag>)}</Flex>
|
||||
<div className="rb:absolute rb:bottom-4 rb:left-6 rb:right-6">
|
||||
<Row gutter={12}>
|
||||
<Col span={12}>
|
||||
<Button block onClick={() => handleShowModel(item)}>{t('modelNew.showModel')}</Button>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Button type="primary" ghost block onClick={() => handleKeyConfig(item)}>{t('modelNew.keyConfig')}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</RbCard>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<KeyConfigModal
|
||||
ref={keyConfigModalRef}
|
||||
refresh={getList}
|
||||
/>
|
||||
<ModelListDetail
|
||||
ref={modelListDetailRef}
|
||||
refresh={getList}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelList
|
||||
104
web/src/views/ModelManagement/Square.tsx
Normal file
104
web/src/views/ModelManagement/Square.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useRef, useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||
import { Button, Space, App, Divider, Flex, Tooltip } from 'antd'
|
||||
import { UsergroupAddOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ModelPlaza, ModelPlazaItem, ModelSquareDetailRef, BaseRef } from './types'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import { getModelPlaza, addModelPlaza } from '@/api/models'
|
||||
import PageEmpty from '@/components/Empty/PageEmpty';
|
||||
import Tag from '@/components/Tag';
|
||||
import ModelSquareDetail from './components/ModelSquareDetail'
|
||||
import { getLogoUrl } from './utils'
|
||||
|
||||
const ModelSquare = forwardRef <BaseRef, { query: any; handleEdit: (vo?: ModelPlazaItem) => void; }>(({ query, handleEdit }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp()
|
||||
const modelSquareDetailRef = useRef<ModelSquareDetailRef>(null)
|
||||
const [list, setList] = useState<ModelPlaza[]>([])
|
||||
useEffect(() => {
|
||||
getList()
|
||||
}, [query])
|
||||
const getList = () => {
|
||||
getModelPlaza(query)
|
||||
.then(res => {
|
||||
setList((res as ModelPlaza[]) || [])
|
||||
})
|
||||
}
|
||||
|
||||
const handleMore = (vo: ModelPlaza) => {
|
||||
modelSquareDetailRef.current?.handleOpen(vo)
|
||||
}
|
||||
const handleAdd = (item: ModelPlazaItem) => {
|
||||
addModelPlaza(item.id)
|
||||
.then(() => {
|
||||
message.success(`${item.name}${t('modelNew.addSuccess')}`)
|
||||
getList()
|
||||
})
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getList,
|
||||
}));
|
||||
return (
|
||||
<>
|
||||
{list.length === 0
|
||||
? <PageEmpty />
|
||||
: list.map(vo => (
|
||||
<div key={vo.provider}>
|
||||
<div className="rb:flex rb:justify-between rb:items-center rb:bg-[rgba(21,94,239,0.12)] rb:px-4 rb:py-2.5 rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">
|
||||
<div className="rb:font-medium">{t(`modelNew.${vo.provider}`)}</div>
|
||||
<Button type="link" onClick={() => handleMore(vo)}>{t('modelNew.viewAll')}({t(`modelNew.modelCount`, { count: vo.models.length })})></Button>
|
||||
</div>
|
||||
|
||||
<div className="rb:grid rb:grid-cols-3 rb:gap-4">
|
||||
{vo.models.slice(0, 6).map(item => (
|
||||
<RbCard
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
subTitle={<Space size={8}>
|
||||
<Tag className="rb:mt-1">{t(`modelNew.${item.type}`)}</Tag>
|
||||
{item.is_official && <Tag color="success" className="rb:mt-1">{t(`modelNew.official`)}</Tag>}
|
||||
</Space>}
|
||||
avatarUrl={getLogoUrl(item.logo)}
|
||||
avatar={
|
||||
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
{item.name[0]}
|
||||
</div>
|
||||
}
|
||||
bodyClassName="rb:relative rb:pb-[80px]! rb:h-[calc(100%-64px)]!"
|
||||
>
|
||||
<Tooltip title={item.description}>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:font-regular rb:wrap-break-word rb:line-clamp-2 rb:mt-3">{item.description}</div>
|
||||
</Tooltip>
|
||||
<Flex gap={8} wrap className="rb:mt-3!">{item.tags.map((tag, tagIndex) => <Tag key={tagIndex}>{tag}</Tag>)}</Flex>
|
||||
<div className="rb:absolute rb:bottom-4 rb:left-6 rb:right-6">
|
||||
<Divider size="middle" />
|
||||
<Flex justify="space-between">
|
||||
<Space size={8}><UsergroupAddOutlined /> {item.add_count}</Space>
|
||||
<Space>
|
||||
{!item.is_official && <Button type="primary" disabled={item.is_deprecated} onClick={() => handleEdit(item)}>{t('modelNew.edit')}</Button>}
|
||||
{item.is_added
|
||||
? <Button type="primary" disabled>{t('modelNew.added')}</Button>
|
||||
: <Button type="primary" ghost disabled={item.is_deprecated} onClick={() => handleAdd(item)}>{item.is_deprecated ? t('modelNew.deprecated') : `+ ${t('common.add')}`}</Button>
|
||||
}
|
||||
</Space>
|
||||
</Flex>
|
||||
</div>
|
||||
</RbCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
<ModelSquareDetail
|
||||
ref={modelSquareDetailRef}
|
||||
refresh={getList}
|
||||
handleEdit={handleEdit}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default ModelSquare
|
||||
@@ -1,171 +0,0 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ModelFormData, Model, ConfigModalRef, ConfigModalProps } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import { updateModel, addModel, modelTypeUrl, modelProviderUrl } from '@/api/models'
|
||||
|
||||
const ConfigModal = forwardRef<ConfigModalRef, ConfigModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [model, setModel] = useState<Model>({} as Model);
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [form] = Form.useForm<ModelFormData>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const values = Form.useWatch<ModelFormData>([], form);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setModel({} as Model);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOpen = (model?: Model) => {
|
||||
if (model) {
|
||||
setIsEdit(true);
|
||||
setModel(model);
|
||||
// 设置表单值
|
||||
const apiKeyInfo = model.api_keys[0]
|
||||
form.setFieldsValue({
|
||||
provider: apiKeyInfo.provider,
|
||||
model_name: apiKeyInfo.model_name,
|
||||
api_key: apiKeyInfo.api_key,
|
||||
api_base: apiKeyInfo.api_base
|
||||
});
|
||||
} else {
|
||||
setIsEdit(false);
|
||||
form.resetFields();
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
const data = {
|
||||
name: values.name,
|
||||
type: values.type,
|
||||
api_keys: {
|
||||
provider: values.provider,
|
||||
model_name: values.model_name,
|
||||
api_key: values.api_key,
|
||||
api_base: values.api_base
|
||||
},
|
||||
}
|
||||
setLoading(true)
|
||||
const res = isEdit
|
||||
? updateModel(model.api_keys[0].id, {
|
||||
provider: values.provider,
|
||||
model_name: values.model_name,
|
||||
api_key: values.api_key,
|
||||
api_base: values.api_base
|
||||
} as ModelFormData)
|
||||
: addModel(data as ModelFormData)
|
||||
|
||||
res.then(() => {
|
||||
if (refresh) {
|
||||
refresh();
|
||||
}
|
||||
handleClose()
|
||||
message.success(isEdit ? t('common.updateSuccess') : t('common.createSuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={isEdit ? `${model.name} - ${t('model.modelConfiguration')}` : t('model.createModel')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t(`common.${isEdit ? 'save' : 'create'}`)}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{}}
|
||||
>
|
||||
{!isEdit && (
|
||||
<>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('model.displayName')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('model.displayName') }) }]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="type"
|
||||
label={t('model.type')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('model.type') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={modelTypeUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({ label: t(`model.${item}`), value: item }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
<Form.Item
|
||||
name="provider"
|
||||
label={t('model.provider')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('model.provider') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={modelProviderUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({ label: t(`model.${item}`), value: item }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="model_name"
|
||||
label={t('model.modelName')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('model.modelName') }) }]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="api_key"
|
||||
label={t('model.apiKey')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('model.apiKey') }) }]}
|
||||
>
|
||||
<Input.Password placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="api_base"
|
||||
label={t('model.apiEndpoint')}
|
||||
>
|
||||
<Input placeholder="https://api.example.com/v1" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ConfigModal;
|
||||
168
web/src/views/ModelManagement/components/CustomModelModal.tsx
Normal file
168
web/src/views/ModelManagement/components/CustomModelModal.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App, Select } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { CustomModelForm, ModelPlazaItem, CustomModelModalRef, CustomModelModalProps } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import UploadImages from '@/components/Upload/UploadImages'
|
||||
import { updateCustomModel, addCustomModel, modelTypeUrl, modelProviderUrl } from '@/api/models'
|
||||
import { getFileLink } from '@/api/fileStorage'
|
||||
|
||||
const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [model, setModel] = useState<ModelPlazaItem>({} as ModelPlazaItem);
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [form] = Form.useForm<CustomModelForm>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const formValues = Form.useWatch([], form)
|
||||
|
||||
const handleClose = () => {
|
||||
setModel({} as ModelPlazaItem);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOpen = (model?: ModelPlazaItem) => {
|
||||
if (model) {
|
||||
setIsEdit(true);
|
||||
setModel(model);
|
||||
form.setFieldsValue({
|
||||
...model,
|
||||
logo: model.logo ? { url: model.logo, uid: model.logo, status: 'done', name: 'logo' } : undefined
|
||||
});
|
||||
} else {
|
||||
setIsEdit(false);
|
||||
form.resetFields();
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
const handleUpdate = (data: CustomModelForm) => {
|
||||
setLoading(true)
|
||||
const { type, provider, ...rest} = data
|
||||
const res = isEdit ? updateCustomModel(model.id, rest) : addCustomModel(data)
|
||||
|
||||
res.then(() => {
|
||||
refresh && refresh()
|
||||
handleClose()
|
||||
message.success(isEdit ? t('common.updateSuccess') : t('common.createSuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
}
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
setLoading(true)
|
||||
const { logo, ...rest } = values;
|
||||
let formData: CustomModelForm = {
|
||||
...rest
|
||||
}
|
||||
formData.is_official = false;
|
||||
|
||||
if (typeof logo === 'object' && logo?.response?.data.file_id) {
|
||||
getFileLink(logo?.response?.data.file_id)
|
||||
.then(res => {
|
||||
const logoRes = res as { url: string }
|
||||
formData.logo = logoRes.url
|
||||
handleUpdate(formData)
|
||||
})
|
||||
.catch(() => {
|
||||
handleUpdate(formData)
|
||||
})
|
||||
} else {
|
||||
formData.logo = typeof logo === 'string' ? logo : logo.url
|
||||
handleUpdate(formData)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
console.log('formValues', formValues)
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={isEdit ? `${model.name} - ${t('modelNew.modelConfiguration')}` : t('modelNew.createCustomModel')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t(`common.${isEdit ? 'save' : 'create'}`)}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="logo"
|
||||
label={t('modelNew.logo')}
|
||||
valuePropName="fileList"
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<UploadImages />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('modelNew.name')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.name') }) }]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="type"
|
||||
label={t('modelNew.type')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.type') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={modelTypeUrl}
|
||||
hasAll={false}
|
||||
disabled={isEdit}
|
||||
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="provider"
|
||||
label={t('modelNew.provider')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.provider') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={modelProviderUrl}
|
||||
hasAll={false}
|
||||
disabled={isEdit}
|
||||
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label={t('modelNew.description')}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="tags"
|
||||
label={t('modelNew.tags')}
|
||||
>
|
||||
<Select mode="tags" placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default CustomModelModal;
|
||||
173
web/src/views/ModelManagement/components/GroupModelModal.tsx
Normal file
173
web/src/views/ModelManagement/components/GroupModelModal.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App, Select } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ModelListItem, CompositeModelForm, GroupModelModalRef, GroupModelModalProps, ModelApiKey } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import { updateCompositeModel, modelTypeUrl, addCompositeModel } from '@/api/models'
|
||||
import UploadImages from '@/components/Upload/UploadImages'
|
||||
import ModelImplement from './ModelImplement'
|
||||
import { getFileLink } from '@/api/fileStorage'
|
||||
|
||||
const GroupModelModal = forwardRef<GroupModelModalRef, GroupModelModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [model, setModel] = useState<ModelListItem>({} as ModelListItem);
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [form] = Form.useForm<CompositeModelForm>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const type = Form.useWatch(['type'], form)
|
||||
|
||||
const handleClose = () => {
|
||||
setModel({} as ModelListItem);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOpen = (model?: ModelListItem) => {
|
||||
if (model) {
|
||||
setIsEdit(true);
|
||||
setModel(model);
|
||||
form.setFieldsValue({
|
||||
...model,
|
||||
api_key_ids: model.api_keys,
|
||||
logo: model.logo ? { url: model.logo, uid: model.logo, status: 'done', name: 'logo' } : undefined
|
||||
})
|
||||
} else {
|
||||
setIsEdit(false);
|
||||
form.resetFields();
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
const { api_key_ids = [], logo, ...rest } = values
|
||||
|
||||
const formData: CompositeModelForm = {
|
||||
...rest,
|
||||
api_key_ids: api_key_ids.map(vo => (vo as ModelApiKey).id)
|
||||
}
|
||||
|
||||
if (logo?.response?.data.file_id) {
|
||||
getFileLink(logo?.response?.data.file_id).then(res => {
|
||||
const logoRes = res as { url: string }
|
||||
formData.logo = logoRes.url
|
||||
handleUpdate(formData)
|
||||
}).catch(() => {
|
||||
handleUpdate(formData)
|
||||
})
|
||||
} else {
|
||||
formData.logo = typeof logo === 'string' ? logo : logo.url
|
||||
handleUpdate(formData)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
const handleUpdate = (data: CompositeModelForm) => {
|
||||
setLoading(true)
|
||||
const { type, ...rest } = data
|
||||
const res = isEdit
|
||||
? updateCompositeModel(model.id, { ...rest })
|
||||
: addCompositeModel(data)
|
||||
|
||||
res.then(() => {
|
||||
refresh?.();
|
||||
handleClose()
|
||||
message.success(isEdit ? t('common.updateSuccess') : t('common.createSuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={isEdit ? `${model.name} - ${t('modelNew.modelConfiguration')}` : t('modelNew.createGroupModel')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t(`common.${isEdit ? 'save' : 'create'}`)}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{ balance_strategy: 'none' }}
|
||||
>
|
||||
<Form.Item
|
||||
name="logo"
|
||||
label={t('modelNew.logo')}
|
||||
valuePropName="fileList"
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<UploadImages />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('modelNew.name')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="type"
|
||||
label={t('modelNew.type')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.type') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={modelTypeUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({
|
||||
label: t(`modelNew.${typeof item === 'object' ? item.value : item}`),
|
||||
value: typeof item === 'object' ? item.value : item
|
||||
}))}
|
||||
disabled={isEdit}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label={t('modelNew.description')}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="load_balance_strategy"
|
||||
label={t('modelNew.load_balance_strategy')}
|
||||
>
|
||||
<Select
|
||||
options={['round_robin', 'none'].map(key => ({
|
||||
label: t(`modelNew.${key}`),
|
||||
value: key
|
||||
}))}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="api_key_ids">
|
||||
<ModelImplement type={type} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default GroupModelModal;
|
||||
92
web/src/views/ModelManagement/components/KeyConfigModal.tsx
Normal file
92
web/src/views/ModelManagement/components/KeyConfigModal.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { KeyConfigModalForm, ProviderModelItem, KeyConfigModalRef, KeyConfigModalProps } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { updateProviderApiKeys } from '@/api/models'
|
||||
|
||||
const KeyConfigModal = forwardRef<KeyConfigModalRef, KeyConfigModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [model, setModel] = useState<ProviderModelItem>({} as ProviderModelItem);
|
||||
const [form] = Form.useForm<KeyConfigModalForm>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleClose = () => {
|
||||
setModel({} as ProviderModelItem);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOpen = (vo: ProviderModelItem) => {
|
||||
setVisible(true);
|
||||
setModel(vo);
|
||||
};
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
setLoading(true)
|
||||
|
||||
updateProviderApiKeys({
|
||||
...values,
|
||||
provider: model.provider
|
||||
}).then((res) => {
|
||||
if (refresh) {
|
||||
refresh();
|
||||
}
|
||||
handleClose()
|
||||
message.success(res as string)
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={`${model.provider} - ${t('modelNew.keyConfig')}`}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t(`common.save`)}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="api_key"
|
||||
label={t('modelNew.api_key')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.apiKey') }) }]}
|
||||
>
|
||||
<Input.Password placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="api_base"
|
||||
label={t('modelNew.api_base')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_base') }) }]}
|
||||
>
|
||||
<Input placeholder="https://api.example.com/v1" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default KeyConfigModal;
|
||||
@@ -0,0 +1,181 @@
|
||||
import { forwardRef, useImperativeHandle, useState, useEffect } from 'react';
|
||||
import { Form, Cascader, App, type CascaderProps } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { SubModelModalForm, SubModelModalRef, SubModelModalProps } from './types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import { modelProviderUrl, getModelNewList } from '@/api/models'
|
||||
import type { ProviderModelItem } from '../../types'
|
||||
|
||||
const { SHOW_CHILD } = Cascader;
|
||||
|
||||
interface Option {
|
||||
value: string | number;
|
||||
label: string;
|
||||
children?: Option[];
|
||||
[key: string]: any;
|
||||
}
|
||||
const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
|
||||
refresh,
|
||||
type,
|
||||
groupedByProvider
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp()
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<SubModelModalForm>();
|
||||
const [selecteds, setSelecteds] = useState<any[]>([])
|
||||
const [modelList, setModelList] = useState<Option[]>([])
|
||||
const provider = Form.useWatch(['provider'], form)
|
||||
|
||||
useEffect(() => {
|
||||
if (provider && groupedByProvider) {
|
||||
const lastModels = groupedByProvider[provider] || []
|
||||
const list = lastModels.map(vo => [{ name: vo.model_name, id: vo.model_config_ids[0], value: vo.model_config_ids[0], provider }, { value: vo.id }])
|
||||
setSelecteds(list)
|
||||
form.setFieldValue('api_key_ids', lastModels.map(vo => [vo.model_config_ids[0], vo.id]))
|
||||
}
|
||||
}, [groupedByProvider, provider])
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
form.resetFields();
|
||||
setVisible(false);
|
||||
setSelecteds([])
|
||||
setModelList([])
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
form.resetFields()
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
refresh?.(selecteds.map(vo => ({
|
||||
...vo[0],
|
||||
model_name: vo[0].name,
|
||||
model_config_ids: [vo[0].id],
|
||||
id: vo[1].value,
|
||||
api_key: vo[1].label
|
||||
})))
|
||||
handleClose()
|
||||
})
|
||||
}
|
||||
const handleChange = (value: (string | number)[][], selectedOptions: Option[][]) => {
|
||||
const filterList = selectedOptions.filter(vo => vo.length === 1).map(item => item[0])
|
||||
const lastFilterLit = value.filter(vo => vo.length !== 1)
|
||||
if (filterList.length) {
|
||||
message.warning(`【${filterList.map(vo => vo.label)}】${t('modelNew.selectOneTip')}`)
|
||||
form.setFieldValue('api_key_ids', lastFilterLit)
|
||||
}
|
||||
setSelecteds(selectedOptions)
|
||||
}
|
||||
|
||||
const handleChangeProvider = (provider: string, api_key_ids?: any[]) => {
|
||||
form.setFieldValue('api_key_ids', undefined)
|
||||
if (provider) {
|
||||
getModelNewList({
|
||||
provider: provider,
|
||||
is_composite: false,
|
||||
is_active: true,
|
||||
type
|
||||
})
|
||||
.then(res => {
|
||||
const response = res as ProviderModelItem[]
|
||||
const list = response[0]?.models || []
|
||||
setModelList(list.map(vo => {
|
||||
const children = vo.api_keys.map(item => ({
|
||||
label: item.api_key,
|
||||
value: item.id,
|
||||
}))
|
||||
return {
|
||||
...vo,
|
||||
label: vo.name,
|
||||
value: vo.id,
|
||||
children: children
|
||||
}
|
||||
}))
|
||||
|
||||
if (api_key_ids?.length) {
|
||||
form.setFieldsValue({
|
||||
api_key_ids: api_key_ids
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setModelList([])
|
||||
}
|
||||
}
|
||||
const displayRender: CascaderProps<Option>['displayRender'] = (labels, selectedOptions = []) =>
|
||||
labels.map((label, i) => {
|
||||
const option = selectedOptions[i];
|
||||
if (i === labels.length - 1) {
|
||||
return (
|
||||
<span key={option?.value || i}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <span key={option?.value || i}>{label} / </span>;
|
||||
});
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('modelNew.implementConfig')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="provider"
|
||||
label={t('modelNew.provider')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.provider') }) }]}
|
||||
>
|
||||
<CustomSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
url={modelProviderUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({
|
||||
label: t(`modelNew.${typeof item === 'object' ? item.value : item}`),
|
||||
value: typeof item === 'object' ? item.value : item
|
||||
}))}
|
||||
onChange={(value) => handleChangeProvider(value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="api_key_ids"
|
||||
label={t('modelNew.api_key_ids')}
|
||||
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.api_key_ids') }) }]}
|
||||
>
|
||||
<Cascader
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={modelList}
|
||||
onChange={handleChange}
|
||||
multiple
|
||||
autoClearSearchValue
|
||||
className="rb:w-full!"
|
||||
showCheckedStrategy={SHOW_CHILD}
|
||||
changeOnSelect
|
||||
displayRender={displayRender}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default SubModelModal;
|
||||
@@ -0,0 +1,99 @@
|
||||
import { type FC, useRef } from "react";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flex, Button, Space, App } from 'antd'
|
||||
|
||||
import type { SubModelModalRef, ModelList } from './types'
|
||||
import SubModelModal from './SubModelModal'
|
||||
import Empty from '@/components/Empty'
|
||||
import Tag from '@/components/Tag'
|
||||
|
||||
interface ModelImplementProps {
|
||||
type?: string;
|
||||
value?: any;
|
||||
onChange?: (value: any) => void;
|
||||
}
|
||||
const ModelImplement: FC<ModelImplementProps> = ({ type, value, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const { modal, message } = App.useApp();
|
||||
const subModelModalRef = useRef<SubModelModalRef>(null)
|
||||
|
||||
const handleAdd = () => {
|
||||
if (!type || type.trim() === '') {
|
||||
message.warning(t('common.selectPlaceholder', { title: t('modelNew.type') }))
|
||||
return
|
||||
}
|
||||
subModelModalRef.current?.handleOpen()
|
||||
}
|
||||
const handleDelete = (vo: any) => {
|
||||
modal.confirm({
|
||||
title: t('common.confirmDeleteDesc', { name: [vo.model_name, vo.api_key].join(' / ') }),
|
||||
content: t('application.apiKeyDeleteContent'),
|
||||
okText: t('common.delete'),
|
||||
cancelText: t('common.cancel'),
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
onChange?.(value?.filter((item: any) => item.id !== vo.id))
|
||||
}
|
||||
})
|
||||
}
|
||||
const handleRefresh = (list: ModelList[]) => {
|
||||
const existingModels = value || [];
|
||||
let updatedModels = [...existingModels];
|
||||
|
||||
const provider = list[0].provider
|
||||
|
||||
updatedModels = updatedModels.filter(item => item.provider !== provider)
|
||||
updatedModels = [...updatedModels, ...list]
|
||||
|
||||
onChange?.([...updatedModels]);
|
||||
}
|
||||
|
||||
const groupedByProvider: Record<string, ModelList[]> = (value || []).reduce((acc: Record<string, ModelList[]>, item: ModelList) => {
|
||||
const provider = item.provider || 'unknown';
|
||||
if (!acc[provider]) acc[provider] = [];
|
||||
acc[provider].push(item);
|
||||
return acc;
|
||||
}, {} as Record<string, ModelList[]>);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Flex justify="space-between" align="center">
|
||||
{t('modelNew.modelImplement')}
|
||||
|
||||
<Space>
|
||||
<Button type="primary" onClick={handleAdd} className="rb:px-2! rb:h-6!">+ {t('modelNew.addImplement')}</Button>
|
||||
<Button size="small" className="rb:px-2! rb:h-6!">{t('modelNew.noAuth')}</Button>
|
||||
</Space>
|
||||
</Flex>
|
||||
|
||||
|
||||
<div className="rb:bg-[#F5F6F7] rb:rounded-lg rb:p-3 rb:mt-2">
|
||||
{!value || value.length === 0
|
||||
? <Empty size={88} />
|
||||
: value.map((item: any) => {
|
||||
return (
|
||||
<div key={item.id} className="rb:mb-4 rb:last:rb:mb-0 rb:bg-[#FBFDFF] rb:rounded-lg rb:p-3">
|
||||
<Flex gap={8} justify="space-between" align="center" className="rb:mb-2 rb:last:rb:mb-0">
|
||||
<div className="rb:font-medium">{item.model_name}</div>
|
||||
<div
|
||||
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => handleDelete(item)}
|
||||
></div>
|
||||
</Flex>
|
||||
<div className="rb:text-[#5B6167] rb:my-2">{item.api_key}</div>
|
||||
<Tag className="rb:mb-2">{t(`modelNew.${item.provider}`)}</Tag>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<SubModelModal
|
||||
ref={subModelModalRef}
|
||||
refresh={handleRefresh}
|
||||
type={type}
|
||||
groupedByProvider={groupedByProvider}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelImplement
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { ModelListItem } from '../../types'
|
||||
|
||||
export interface ModelList extends ModelListItem {
|
||||
api_key_id: string;
|
||||
}
|
||||
export interface SubModelModalForm {
|
||||
provider: string;
|
||||
api_key_ids: string[][];
|
||||
}
|
||||
export interface SubModelModalRef {
|
||||
handleOpen: () => void;
|
||||
}
|
||||
export interface SubModelModalProps {
|
||||
type?: string;
|
||||
refresh?: (vo: ModelList[]) => void;
|
||||
groupedByProvider?: Record<string, ModelList[]>
|
||||
}
|
||||
142
web/src/views/ModelManagement/components/ModelListDetail.tsx
Normal file
142
web/src/views/ModelManagement/components/ModelListDetail.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { useState, useImperativeHandle, forwardRef, useRef, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Switch, Row, Col, Space, Tooltip } from 'antd'
|
||||
|
||||
import type { ProviderModelItem, ModelListItem, ModelListDetailRef, MultiKeyConfigModalRef } from '../types';
|
||||
import RbDrawer from '@/components/RbDrawer';
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import Tag from '@/components/Tag';
|
||||
import PageEmpty from '@/components/Empty/PageEmpty';
|
||||
import MultiKeyConfigModal from './MultiKeyConfigModal'
|
||||
import { getModelNewList, updateModelStatus, modelTypeUrl } from '@/api/models'
|
||||
import { getLogoUrl } from '../utils'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
|
||||
interface ModelListDetailProps {
|
||||
refresh?: () => void;
|
||||
}
|
||||
|
||||
const ModelListDetail = forwardRef<ModelListDetailRef, ModelListDetailProps>(({ refresh }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [data, setData] = useState<ProviderModelItem>({} as ProviderModelItem)
|
||||
const [list, setList] = useState<ModelListItem[]>([])
|
||||
const multiKeyConfigModalRef = useRef<MultiKeyConfigModalRef>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [type, setType] = useState<string | undefined | null>(null)
|
||||
|
||||
const handleOpen = (vo: ProviderModelItem) => {
|
||||
setType(null)
|
||||
setOpen(true)
|
||||
getData(vo)
|
||||
}
|
||||
|
||||
const getData = (vo: ProviderModelItem) => {
|
||||
if (!vo.provider) return
|
||||
|
||||
getModelNewList({
|
||||
provider: vo.provider
|
||||
})
|
||||
.then(res => {
|
||||
const response = res as ProviderModelItem[]
|
||||
setData(response[0])
|
||||
setList(response[0].models)
|
||||
})
|
||||
}
|
||||
const handleKeyConfig = (vo: ModelListItem) => {
|
||||
multiKeyConfigModalRef.current?.handleOpen(vo, data.provider)
|
||||
}
|
||||
const handleChange = (vo: ModelListItem) => {
|
||||
setLoading(true)
|
||||
updateModelStatus(vo.id, { is_active: !vo.is_active })
|
||||
.finally(() => {
|
||||
getData(data)
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setType(null)
|
||||
setOpen(false)
|
||||
refresh?.()
|
||||
}
|
||||
const handleRefresh = () => {
|
||||
getData(data)
|
||||
}
|
||||
const handleTypeChange = (value: string) => {
|
||||
setType(value)
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
const filterList = useMemo(() => {
|
||||
if (!type) return list
|
||||
return list.filter(vo => vo.type === type)
|
||||
}, [type, list])
|
||||
|
||||
return (
|
||||
<RbDrawer
|
||||
title={<>{t(`modelNew.${data.provider}`)} {t('modelNew.modelList')} ({list.length}{t('modelNew.item')})</>}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<CustomSelect
|
||||
value={type}
|
||||
url={modelTypeUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
|
||||
onChange={handleTypeChange}
|
||||
className="rb:w-full"
|
||||
allowClear={true}
|
||||
placeholder={t('modelNew.type')}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{filterList.length === 0
|
||||
? <PageEmpty />
|
||||
: <div className="rb:grid rb:grid-cols-2 rb:gap-4 rb:mt-3">
|
||||
{filterList.map(item => (
|
||||
<RbCard
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
subTitle={<Space className="rb:mt-1!">
|
||||
<Tag>{t(`modelNew.${item.type}`)}</Tag>
|
||||
<Tag color="warning">{item.api_keys.length}{t('modelNew.apiKeyNum')}</Tag>
|
||||
</Space>}
|
||||
avatarUrl={getLogoUrl(item.logo)}
|
||||
avatar={
|
||||
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
{item.name[0]}
|
||||
</div>
|
||||
}
|
||||
extra={<Switch defaultChecked={item.is_active} disabled={loading} onChange={() => handleChange(item)} />}
|
||||
bodyClassName="rb:relative rb:pb-[64px]! rb:h-[calc(100%-64px)]!"
|
||||
>
|
||||
<Tooltip title={item.description}>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:font-regular rb:wrap-break-word rb:line-clamp-2">{item.description}</div>
|
||||
</Tooltip>
|
||||
<div className="rb:absolute rb:bottom-4 rb:left-6 rb:right-6">
|
||||
<Row gutter={12}>
|
||||
<Col span={24}>
|
||||
<Button type="primary" ghost block onClick={() => handleKeyConfig(item)}>{t('modelNew.keyConfig')}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</RbCard>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
|
||||
<MultiKeyConfigModal
|
||||
ref={multiKeyConfigModalRef}
|
||||
refresh={handleRefresh}
|
||||
/>
|
||||
</RbDrawer>
|
||||
);
|
||||
});
|
||||
|
||||
export default ModelListDetail;
|
||||
106
web/src/views/ModelManagement/components/ModelSquareDetail.tsx
Normal file
106
web/src/views/ModelManagement/components/ModelSquareDetail.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useState, useImperativeHandle, forwardRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Space, App, Flex, Tooltip, Divider } from 'antd'
|
||||
import { UsergroupAddOutlined } from '@ant-design/icons';
|
||||
|
||||
import type { ModelPlaza, ModelPlazaItem, ModelSquareDetailRef } from '../types';
|
||||
import RbDrawer from '@/components/RbDrawer';
|
||||
import { getModelPlaza, addModelPlaza } from '@/api/models'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import Tag from '@/components/Tag';
|
||||
import PageEmpty from '@/components/Empty/PageEmpty';
|
||||
import { getLogoUrl } from '../utils'
|
||||
|
||||
interface ModelSquareDetailProps {
|
||||
refresh: () => void;
|
||||
handleEdit: (vo: ModelPlazaItem) => void;
|
||||
}
|
||||
const ModelSquareDetail = forwardRef<ModelSquareDetailRef, ModelSquareDetailProps>(({ refresh, handleEdit }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp()
|
||||
const [model, setModel] = useState<ModelPlaza>({} as ModelPlaza)
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const [list, setList] = useState<ModelPlazaItem[]>([])
|
||||
|
||||
const handleOpen = (vo: ModelPlaza) => {
|
||||
setModel(vo)
|
||||
setOpen(true)
|
||||
getList(vo)
|
||||
}
|
||||
const handleClose = () => {
|
||||
setOpen(false)
|
||||
refresh()
|
||||
}
|
||||
const getList = (vo: ModelPlaza) => {
|
||||
getModelPlaza({ provider: vo.provider })
|
||||
.then(res => {
|
||||
const response = res as ModelPlaza[]
|
||||
setList(response.length > 0 ? response[0].models : [])
|
||||
})
|
||||
}
|
||||
const handleAdd = (item: ModelPlazaItem) => {
|
||||
addModelPlaza(item.id)
|
||||
.then(() => {
|
||||
message.success(`${item.name}${t('modelNew.addSuccess')}`)
|
||||
getList(model)
|
||||
})
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbDrawer
|
||||
title={<>{t(`modelNew.${model.provider}`)} {t('modelNew.modelList')} ({list.length}{t('modelNew.item')})</>}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<div className="rb:h-full rb:overflow-y-auto">
|
||||
{list.length === 0
|
||||
? <PageEmpty />
|
||||
: <div className="rb:grid rb:grid-cols-2 rb:gap-4">
|
||||
{list.map(item => (
|
||||
<RbCard
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
subTitle={<Space size={8}>
|
||||
<Tag className="rb:mt-1">{t(`modelNew.${item.type}`)}</Tag>
|
||||
{item.is_official && <Tag color="success" className="rb:mt-1">{t(`modelNew.official`)}</Tag>}
|
||||
</Space>}
|
||||
avatarUrl={getLogoUrl(item.logo)}
|
||||
avatar={
|
||||
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
{item.name[0]}
|
||||
</div>
|
||||
}
|
||||
bodyClassName="rb:relative rb:pb-[80px]! rb:h-[calc(100%-64px)]!"
|
||||
>
|
||||
<Tooltip title={item.description}>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:font-regular rb:wrap-break-word rb:line-clamp-2 rb:mt-3">{item.description}</div>
|
||||
</Tooltip>
|
||||
<Flex gap={8} wrap className="rb:mt-3!">{item.tags.map((tag, tagIndex) => <Tag key={tagIndex}>{tag}</Tag>)}</Flex>
|
||||
<div className="rb:absolute rb:bottom-4 rb:left-6 rb:right-6">
|
||||
<Divider size="middle" />
|
||||
<Flex justify="space-between">
|
||||
<Space size={8}><UsergroupAddOutlined /> {item.add_count}</Space>
|
||||
<Space>
|
||||
{!item.is_official && <Button type="primary" disabled={item.is_deprecated} onClick={() => handleEdit(item)}>{t('modelNew.edit')}</Button>}
|
||||
{item.is_added
|
||||
? <Button type="primary" disabled>{t('modelNew.added')}</Button>
|
||||
: <Button type="primary" ghost disabled={item.is_deprecated} onClick={() => handleAdd(item)}>{item.is_deprecated ? t('modelNew.deprecated') : `+ ${t('common.add')}`}</Button>
|
||||
}
|
||||
</Space>
|
||||
</Flex>
|
||||
</div>
|
||||
</RbCard>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</RbDrawer>
|
||||
);
|
||||
});
|
||||
|
||||
export default ModelSquareDetail;
|
||||
122
web/src/views/ModelManagement/components/MultiKeyConfigModal.tsx
Normal file
122
web/src/views/ModelManagement/components/MultiKeyConfigModal.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App, Button } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ModelListItem, MultiKeyForm, MultiKeyConfigModalRef, MultiKeyConfigModalProps } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { addModelApiKey, deleteModelApiKey, getModelInfo } from '@/api/models'
|
||||
|
||||
const MultiKeyConfigModal = forwardRef<MultiKeyConfigModalRef, MultiKeyConfigModalProps>(({ refresh }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [model, setModel] = useState<ModelListItem>({} as ModelListItem);
|
||||
const [form] = Form.useForm<MultiKeyForm>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleClose = () => {
|
||||
setModel({} as ModelListItem);
|
||||
refresh?.()
|
||||
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOpen = (vo: ModelListItem) => {
|
||||
setVisible(true);
|
||||
getData(vo)
|
||||
};
|
||||
|
||||
const getData = (vo: ModelListItem) => {
|
||||
if (!vo.id) return
|
||||
|
||||
getModelInfo(vo?.id)
|
||||
.then(res => {
|
||||
setModel(res as ModelListItem)
|
||||
})
|
||||
}
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
setLoading(true)
|
||||
addModelApiKey(model.id, {
|
||||
...values,
|
||||
model_config_id: model.id,
|
||||
model_name: model.name,
|
||||
provider: model.provider,
|
||||
}).then(() => {
|
||||
message.success(t('common.saveSuccess'))
|
||||
form.resetFields();
|
||||
getData(model)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
const handleDelete = (api_key_id: string) => {
|
||||
deleteModelApiKey(api_key_id)
|
||||
.then(() => {
|
||||
message.success(t('common.deleteSuccess'))
|
||||
getData(model)
|
||||
})
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={`${model.name} - ${t('modelNew.keyConfig')}`}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
{model.api_keys && model.api_keys.length > 0 && (
|
||||
<div className="rb:mb-4">
|
||||
{model.api_keys.map((key) => (
|
||||
<div key={key.id} className="rb:flex rb:items-center rb:justify-between rb:p-3 rb:bg-[#F5F6F7] rb:rounded-lg rb:mb-2">
|
||||
<div>
|
||||
<div className="rb:text-[#1D2129] rb:text-[14px] rb:font-medium">{key.api_key}</div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:mt-1">{key.api_base}</div>
|
||||
</div>
|
||||
<Button type="primary" danger ghost onClick={() => handleDelete(key.id)}>{t('common.remove')}</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="api_key"
|
||||
label={t('modelNew.api_key')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_key') }) }]}
|
||||
>
|
||||
<Input.Password placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="api_base"
|
||||
label={t('modelNew.api_base')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_base') }) }]}
|
||||
>
|
||||
<Input placeholder="https://api.example.com/v1" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" block onClick={handleSave} loading={loading}>+ {t('modelNew.add')}</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default MultiKeyConfigModal;
|
||||
@@ -1,99 +1,124 @@
|
||||
import { useState, useRef, type FC } from 'react';
|
||||
import { Row, Col, Button } from 'antd'
|
||||
import { Button, Flex, Space, type SegmentedProps, Form } from 'antd'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import ConfigModal from './components/ConfigModal'
|
||||
import type { Model, DescriptionItem, ConfigModalRef } from './types'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import GroupModelModal from './components/GroupModelModal'
|
||||
import type { ModelListItem, GroupModelModalRef, CustomModelModalRef, ModelPlazaItem, BaseRef, Query } from './types'
|
||||
import SearchInput from '@/components/SearchInput'
|
||||
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
import PageTabs from '@/components/PageTabs'
|
||||
import GroupModel from './Group'
|
||||
import ModelList from './List'
|
||||
import ModelSquare from './Square'
|
||||
import CustomModelModal from './components/CustomModelModal'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import { modelTypeUrl, modelProviderUrl } from '@/api/models'
|
||||
|
||||
const tabKeys = ['group', 'list', 'square']
|
||||
const ModelManagement: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [query, setQuery] = useState({})
|
||||
const configModalRef = useRef<ConfigModalRef>(null)
|
||||
const scrollListRef = useRef<PageScrollListRef>(null)
|
||||
const [activeTab, setActiveTab] = useState('group');
|
||||
const configModalRef = useRef<GroupModelModalRef>(null)
|
||||
const customModelModalRef = useRef<CustomModelModalRef>(null)
|
||||
const groupRef = useRef<BaseRef>(null)
|
||||
const squareRef = useRef<BaseRef>(null)
|
||||
const [form] = Form.useForm<Query>()
|
||||
const query = Form.useWatch([], form)
|
||||
|
||||
const formatData = (data: Model) => {
|
||||
return [
|
||||
{
|
||||
key: 'type',
|
||||
label: t(`model.type`),
|
||||
children: data.type || '-',
|
||||
},
|
||||
{
|
||||
key: 'provider',
|
||||
label: t(`model.provider`),
|
||||
children: data.api_keys[0].provider || '-',
|
||||
},
|
||||
{
|
||||
key: 'is_active',
|
||||
label: t(`model.status`),
|
||||
children: data.is_active ? t(`common.statusEnabled`) : t(`common.statusDisabled`),
|
||||
},
|
||||
{
|
||||
key: 'created',
|
||||
label: t(`model.created`),
|
||||
children: data.created_at ? formatDateTime(data.created_at, 'YYYY-MM-DD HH:mm:ss') : '-',
|
||||
},
|
||||
]
|
||||
const formatTabItems = () => {
|
||||
return tabKeys.map(value => ({
|
||||
value,
|
||||
label: t(`modelNew.${value}`),
|
||||
}))
|
||||
}
|
||||
const handleChangeTab = (value: SegmentedProps['value']) => {
|
||||
setActiveTab(value as string);
|
||||
form.resetFields()
|
||||
}
|
||||
|
||||
const handleEdit = (model?: Model) => {
|
||||
configModalRef?.current?.handleOpen(model)
|
||||
const handleEdit = (vo?: ModelListItem | ModelPlazaItem) => {
|
||||
switch(activeTab) {
|
||||
case 'group':
|
||||
configModalRef?.current?.handleOpen(vo as ModelListItem)
|
||||
break
|
||||
case 'square':
|
||||
customModelModalRef?.current?.handleOpen(vo as ModelPlazaItem)
|
||||
break
|
||||
}
|
||||
}
|
||||
const handleSearch = (value?: string) => {
|
||||
setQuery({ search: value })
|
||||
const handleRefresh = () => {
|
||||
switch (activeTab) {
|
||||
case 'group':
|
||||
groupRef.current?.getList()
|
||||
break
|
||||
case 'square':
|
||||
squareRef.current?.getList()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:w-full">
|
||||
<Row className='rb:mb-[16px] rb:w-full'>
|
||||
<Col span={6}>
|
||||
<SearchInput
|
||||
placeholder={t('model.searchPlaceholder')}
|
||||
onSearch={handleSearch}
|
||||
style={{width: '100%'}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={18} className="rb:text-right">
|
||||
<Button type="primary" onClick={() => handleEdit()}>{t('model.createModel')}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<>
|
||||
<Flex justify="space-between" align="center">
|
||||
<PageTabs
|
||||
value={activeTab}
|
||||
options={formatTabItems()}
|
||||
onChange={handleChangeTab}
|
||||
/>
|
||||
|
||||
<PageScrollList
|
||||
ref={scrollListRef}
|
||||
url={getModelListUrl}
|
||||
query={query}
|
||||
renderItem={(item: Model) => (
|
||||
<RbCard
|
||||
title={item.name}
|
||||
>
|
||||
{formatData(item)?.map((description: DescriptionItem) => (
|
||||
<div
|
||||
key={description.key}
|
||||
className="rb:flex rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-[20px] rb:mb-[12px]"
|
||||
>
|
||||
<span className="rb:whitespace-nowrap">{(description.label as string)}</span>
|
||||
<span className={clsx({
|
||||
"rb:text-[#212332]": description.key !== 'is_active',
|
||||
"rb:text-[#369F21] rb:font-medium": description.key === 'is_active' && item.is_active,
|
||||
})}>{(description.children as string)}</span>
|
||||
</div>
|
||||
))}
|
||||
<Button className="rb:mt-[8px]" type="primary" ghost block onClick={() => handleEdit(item)}>{t('model.configureBtn')}</Button>
|
||||
</RbCard>
|
||||
)}
|
||||
/>
|
||||
<Form form={form}>
|
||||
<Space size={12}>
|
||||
{activeTab === 'list' &&
|
||||
<Form.Item name="type" noStyle>
|
||||
<CustomSelect
|
||||
url={modelTypeUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
|
||||
className="rb:w-30"
|
||||
allowClear={true}
|
||||
placeholder={t('modelNew.type')}
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
{(activeTab === 'list' || activeTab === 'square') &&
|
||||
<Form.Item name="provider" noStyle>
|
||||
<CustomSelect
|
||||
url={modelProviderUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
|
||||
className="rb:w-30"
|
||||
allowClear={true}
|
||||
placeholder={t('modelNew.provider')}
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
{activeTab !== 'list' &&
|
||||
<Form.Item name="search" noStyle>
|
||||
<SearchInput
|
||||
placeholder={t(`modelNew.${activeTab}SearchPlaceholder`)}
|
||||
className="rb:w-70!"
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
{activeTab === 'group' && <Button type="primary" onClick={() => handleEdit()}>+ {t('modelNew.createGroupModel')}</Button>}
|
||||
{activeTab === 'square' && <Button type="primary" onClick={() => handleEdit()}>+ {t('modelNew.createCustomModel')}</Button>}
|
||||
</Space>
|
||||
</Form>
|
||||
</Flex>
|
||||
|
||||
<ConfigModal
|
||||
<div className="rb:w-full rb:h-[calc(100%-48px)] rb:my-4">
|
||||
{activeTab === 'group' && <GroupModel ref={groupRef} query={query} handleEdit={handleEdit} />}
|
||||
{activeTab === 'list' && <ModelList query={query} />}
|
||||
{activeTab === 'square' && <ModelSquare ref={squareRef} query={query} handleEdit={handleEdit} />}
|
||||
</div>
|
||||
<GroupModelModal
|
||||
ref={configModalRef}
|
||||
refresh={() => scrollListRef?.current?.refresh()}
|
||||
refresh={handleRefresh}
|
||||
/>
|
||||
</div>
|
||||
<CustomModelModal
|
||||
ref={customModelModalRef}
|
||||
refresh={handleRefresh}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,70 +1,139 @@
|
||||
// 模型表单数据类型
|
||||
export interface ModelFormData extends ApiKey {
|
||||
name: string;
|
||||
type: string;
|
||||
api_keys: ApiKey;
|
||||
}
|
||||
export interface Query {
|
||||
type?: string;
|
||||
provider?: string;
|
||||
is_active?: boolean;
|
||||
is_public?: boolean;
|
||||
is_composite?: boolean;
|
||||
search?: string;
|
||||
|
||||
pagesize?: number;
|
||||
page?: number;
|
||||
}
|
||||
export interface DescriptionItem {
|
||||
key: string;
|
||||
label: string;
|
||||
children: string;
|
||||
}
|
||||
export interface CompositeModelForm {
|
||||
logo?: any;
|
||||
name: string;
|
||||
type?: string;
|
||||
description: string;
|
||||
api_key_ids: ModelApiKey[] | string[];
|
||||
}
|
||||
export interface GroupModelModalRef {
|
||||
handleOpen: (model?: ModelListItem) => void;
|
||||
}
|
||||
export interface GroupModelModalProps {
|
||||
refresh?: () => void;
|
||||
}
|
||||
export interface ModelListDetailRef {
|
||||
handleOpen: (vo: ProviderModelItem) => void;
|
||||
}
|
||||
|
||||
// 模型类型定义
|
||||
export interface Model {
|
||||
|
||||
export interface ModelApiKey {
|
||||
model_name: string;
|
||||
description: string | null;
|
||||
provider: string;
|
||||
api_key: string;
|
||||
api_base: string;
|
||||
config: any;
|
||||
is_active: boolean;
|
||||
priority: string;
|
||||
id: string;
|
||||
usage_count: string;
|
||||
last_used_at: number;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
model_config_ids: string[];
|
||||
}
|
||||
export interface ModelListItem {
|
||||
model_name?: string;
|
||||
model_config_ids: string[];
|
||||
name: string;
|
||||
type: string;
|
||||
logo: string;
|
||||
description: string;
|
||||
provider: string;
|
||||
config: any;
|
||||
is_active: boolean;
|
||||
is_public: boolean;
|
||||
id: string;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
api_keys: ModelApiKey[]
|
||||
}
|
||||
export interface ProviderModelItem {
|
||||
provider: string;
|
||||
logo?: string;
|
||||
tags: string[];
|
||||
models: ModelListItem[];
|
||||
}
|
||||
export interface KeyConfigModalForm {
|
||||
provider: string;
|
||||
api_key: string;
|
||||
api_base: string;
|
||||
}
|
||||
export interface KeyConfigModalRef {
|
||||
handleOpen: (vo: ProviderModelItem) => void;
|
||||
}
|
||||
export interface KeyConfigModalProps {
|
||||
refresh?: () => void;
|
||||
}
|
||||
export interface MultiKeyForm {
|
||||
model_config_id?: string;
|
||||
model_name: string;
|
||||
provider: string;
|
||||
api_key: string;
|
||||
api_base: string;
|
||||
}
|
||||
|
||||
export interface MultiKeyConfigModalRef {
|
||||
handleOpen: (vo: ModelListItem, provider?: string) => void;
|
||||
}
|
||||
export interface MultiKeyConfigModalProps {
|
||||
refresh?: () => void;
|
||||
}
|
||||
|
||||
|
||||
export interface ModelPlaza {
|
||||
provider: string;
|
||||
models: ModelPlazaItem[];
|
||||
}
|
||||
export interface ModelPlazaItem {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
description?: string;
|
||||
config: Record<string, unknown>;
|
||||
is_active: boolean;
|
||||
is_public: boolean;
|
||||
created_at: string | number;
|
||||
updated_at: string | number;
|
||||
api_keys: ApiKey[];
|
||||
|
||||
// provider: string;
|
||||
// temperature: number,
|
||||
// topP: number,
|
||||
// status: string;
|
||||
// vectorDimension: number;
|
||||
// batchSize: number;
|
||||
// truncateStrategy: string;
|
||||
// created: string;
|
||||
// updatedAt: string;
|
||||
// descriptionItems?: Record<string, unknown>[];
|
||||
// basicParameters?: string;
|
||||
// normalization?: string;
|
||||
// maxInputLength?: number;
|
||||
// encodingFormat?: string;
|
||||
// enablePooling?: boolean;
|
||||
// poolingStrategy?: string;
|
||||
// apiKey?: string;
|
||||
// apiEndpoint?: string;
|
||||
// timeout?: number;
|
||||
// autoRetry?: boolean;
|
||||
// retryCount?: number;
|
||||
}
|
||||
interface ApiKey {
|
||||
model_name?: string;
|
||||
provider: string;
|
||||
api_key?: string;
|
||||
api_base?: string;
|
||||
config?: Record<string, unknown>;
|
||||
is_active?: boolean;
|
||||
priority?: string;
|
||||
id: string;
|
||||
model_config_id?: string;
|
||||
usage_count?: string;
|
||||
last_used_at?: string | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
logo: string;
|
||||
description: string;
|
||||
is_deprecated: boolean;
|
||||
is_official: boolean;
|
||||
tags: string[];
|
||||
add_count: number;
|
||||
is_added: boolean;
|
||||
}
|
||||
// 定义组件暴露的方法接口
|
||||
export interface ConfigModalRef {
|
||||
handleOpen: (model?: Model) => void;
|
||||
export interface ModelSquareDetailRef {
|
||||
handleOpen: (vo: ModelPlaza) => void;
|
||||
}
|
||||
export interface ConfigModalProps {
|
||||
export interface CustomModelForm {
|
||||
name: string;
|
||||
type?: string;
|
||||
provider?: string;
|
||||
logo?: any;
|
||||
description: string;
|
||||
is_official: boolean;
|
||||
tags: string[];
|
||||
}
|
||||
export interface CustomModelModalRef {
|
||||
handleOpen: (vo?: ModelPlazaItem) => void;
|
||||
}
|
||||
export interface CustomModelModalProps {
|
||||
refresh?: () => void;
|
||||
}
|
||||
|
||||
|
||||
export interface BaseRef {
|
||||
getList: () => void;
|
||||
}
|
||||
26
web/src/views/ModelManagement/utils.ts
Normal file
26
web/src/views/ModelManagement/utils.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import bedrockIcon from '@/assets/images/model/bedrock.svg'
|
||||
import dashscopeIcon from '@/assets/images/model/dashscope.png'
|
||||
import gpustackIcon from '@/assets/images/model/gpustack.png'
|
||||
import ollamaIcon from '@/assets/images/model/ollama.svg'
|
||||
import openaiIcon from '@/assets/images/model/openai.svg'
|
||||
import xinferenceIcon from '@/assets/images/model/xinference.svg'
|
||||
|
||||
export const ICONS = {
|
||||
bedrock: bedrockIcon,
|
||||
dashscope: dashscopeIcon,
|
||||
gpustack: gpustackIcon,
|
||||
ollama: ollamaIcon,
|
||||
openai: openaiIcon,
|
||||
xinference: xinferenceIcon
|
||||
}
|
||||
|
||||
export const getLogoUrl = (logo?: string) => {
|
||||
if (!logo) {
|
||||
return undefined
|
||||
}
|
||||
if (logo.startsWith('http')) {
|
||||
return logo
|
||||
}
|
||||
|
||||
return ICONS[logo as keyof typeof ICONS] || undefined
|
||||
}
|
||||
95
web/src/views/Prompt/History.tsx
Normal file
95
web/src/views/Prompt/History.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React, { useRef, type MouseEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tooltip, Space, App } from 'antd';
|
||||
import { EyeOutlined } from '@ant-design/icons';
|
||||
|
||||
import type { HistoryQuery, HistoryItem, PromptDetailRef } from './types';
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import { getPromptReleaseListUrl, deletePrompt } from '@/api/prompt'
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import PromptDetail from './components/PromptDetail'
|
||||
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
|
||||
|
||||
const History: React.FC<{ query: HistoryQuery; edit: (item: HistoryItem) => void; }> = ({ query, edit }) => {
|
||||
const { t } = useTranslation();
|
||||
const scrollListRef = useRef<PageScrollListRef>(null)
|
||||
const detailRef = useRef<PromptDetailRef>(null)
|
||||
const { message, modal } = App.useApp()
|
||||
|
||||
const handleView = (item: HistoryItem) => {
|
||||
detailRef.current?.handleOpen(item)
|
||||
}
|
||||
const handleDelete = (item: HistoryItem, e?: MouseEvent) => {
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
modal.confirm({
|
||||
title: t('common.confirmDeleteDesc', { name: item.title }),
|
||||
content: t('application.apiKeyDeleteContent'),
|
||||
okText: t('common.delete'),
|
||||
cancelText: t('common.cancel'),
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
deletePrompt(item.id).then(() => {
|
||||
message.success(t('common.deleteSuccess'))
|
||||
scrollListRef.current?.refresh()
|
||||
detailRef.current?.handleClose()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
const handleEdit = (item: HistoryItem) => {
|
||||
edit(item)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageScrollList
|
||||
ref={scrollListRef}
|
||||
url={getPromptReleaseListUrl}
|
||||
query={query}
|
||||
column={3}
|
||||
renderItem={(item) => {
|
||||
const historyItem = item as unknown as HistoryItem;
|
||||
return (
|
||||
<RbCard
|
||||
className="rb:cursor-pointer"
|
||||
headerType="borderless"
|
||||
bodyClassName="rb:p-4!"
|
||||
title={<Tooltip title={historyItem.title}>{historyItem.title}</Tooltip>}
|
||||
extra={<div className="rb:text-[12px] rb:text-[#5B6167]">{formatDateTime(historyItem.created_at, 'YYYY/MM/DD HH:mm')}</div>}
|
||||
onClick={() => handleView(historyItem)}
|
||||
>
|
||||
<div className="rb:text-[12px] rb:h-30 rb:overflow-hidden rb:px-3 rb:py-2.5 rb:bg-[#F6F8FC] rb:rounded-lg rb:border rb:border-[#DFE4ED] rb:shadow-[0px_4px_8px_0px_rgba(33,35,50,0.12)]">
|
||||
<Markdown content={historyItem.prompt} className="rb:h-full! rb:overflow-y-auto" />
|
||||
</div>
|
||||
|
||||
<div className="rb:mt-4 rb:text-[12px] rb:leading-4 rb:font-regular rb:text-[#5B6167] rb:flex rb:items-center rb:justify-end">
|
||||
<Space size={16}>
|
||||
<EyeOutlined className="rb:text-[16px]" onClick={() => handleView(historyItem)} />
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
|
||||
onClick={() => handleEdit(historyItem)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
|
||||
onClick={(e) => handleDelete(historyItem, e)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
</RbCard>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<PromptDetail
|
||||
ref={detailRef}
|
||||
handleEdit={handleEdit}
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default History;
|
||||
227
web/src/views/Prompt/Prompt.tsx
Normal file
227
web/src/views/Prompt/Prompt.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import { type FC, useState, useRef, useEffect } from 'react';
|
||||
import { Button, Form, Input, App, Row, Col } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx'
|
||||
import copy from 'copy-to-clipboard';
|
||||
|
||||
import { updatePromptMessages, createPromptSessions } from '@/api/prompt'
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
import type { PromptVariableModalRef, AiPromptForm, HistoryItem, PromptSaveModalRef } from './types'
|
||||
import ChatContent from '@/components/Chat/ChatContent'
|
||||
import Empty from '@/components/Empty'
|
||||
import ChatSendIcon from '@/assets/images/application/chatSend.svg'
|
||||
import ConversationEmptyIcon from '@/assets/images/conversation/conversationEmpty.svg'
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import PromptVariableModal from './components/PromptVariableModal'
|
||||
import { type SSEMessage } from '@/utils/stream'
|
||||
import Editor from '@/views/ApplicationConfig/components/Editor'
|
||||
import PromptSaveModal from './components/PromptSaveModal'
|
||||
|
||||
const Prompt: FC<{ editVo: HistoryItem | null; refresh: () => void; }> = ({ editVo, refresh }) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [form] = Form.useForm<AiPromptForm>()
|
||||
const [chatList, setChatList] = useState<ChatItem[]>([])
|
||||
const [variables, setVariables] = useState<string[]>([])
|
||||
const [promptSession, setPromptSession] = useState<string | null>(null)
|
||||
const aiPromptVariableModalRef = useRef<PromptVariableModalRef>(null)
|
||||
const promptSaveModalRef = useRef<PromptSaveModalRef>(null)
|
||||
const editorRef = useRef<any>(null)
|
||||
const currentPromptValueRef = useRef<string>(undefined)
|
||||
const values = Form.useWatch([], form)
|
||||
|
||||
useEffect(() => {
|
||||
if (editVo?.id) {
|
||||
form.setFieldValue('current_prompt', editVo.prompt)
|
||||
setChatList([])
|
||||
}
|
||||
updateSession()
|
||||
}, [editVo])
|
||||
|
||||
const updateSession = () => {
|
||||
console.log('updateSession')
|
||||
createPromptSessions().then(res => {
|
||||
const response = res as { id: string }
|
||||
setPromptSession(response.id)
|
||||
})
|
||||
}
|
||||
|
||||
const handleSend = () => {
|
||||
if (!promptSession) return
|
||||
if (!values.model_id) {
|
||||
message.warning(t('common.selectPlaceholder', { title: t('prompt.model') }))
|
||||
return
|
||||
}
|
||||
if (!values.message) {
|
||||
message.warning(t('prompt.promptChatPlaceholder'))
|
||||
return
|
||||
}
|
||||
const messageContent = values.message
|
||||
setLoading(true)
|
||||
setChatList(prev => {
|
||||
return [...prev, { role: 'user', content: messageContent}]
|
||||
})
|
||||
form.setFieldsValue({ message: undefined, current_prompt: undefined })
|
||||
|
||||
const handleStreamMessage = (data: SSEMessage[]) => {
|
||||
data.map(item => {
|
||||
const { content, desc, variables } = item.data as { content: string; desc: string; variables: string[] };
|
||||
|
||||
switch (item.event) {
|
||||
case 'start':
|
||||
currentPromptValueRef.current = ''
|
||||
if (editorRef.current?.clear) {
|
||||
editorRef.current.clear();
|
||||
}
|
||||
break;
|
||||
case 'message':
|
||||
if (typeof content === 'string') {
|
||||
currentPromptValueRef.current += content;
|
||||
if (editorRef.current?.appendText) {
|
||||
editorRef.current.appendText(content);
|
||||
editorRef.current.scrollToBottom();
|
||||
} else {
|
||||
form.setFieldsValue({ current_prompt: currentPromptValueRef.current })
|
||||
}
|
||||
}
|
||||
if (desc) {
|
||||
setChatList(prev => {
|
||||
return [...prev, { role: 'assistant', content: desc }]
|
||||
})
|
||||
}
|
||||
if (variables) {
|
||||
setVariables(variables)
|
||||
}
|
||||
break;
|
||||
case 'end':
|
||||
setLoading(false)
|
||||
// 流结束时同步表单值
|
||||
form.setFieldsValue({ current_prompt: currentPromptValueRef.current })
|
||||
break
|
||||
}
|
||||
})
|
||||
};
|
||||
updatePromptMessages((promptSession) as string, values, handleStreamMessage)
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
const handleCopy = () => {
|
||||
if (!values.current_prompt || values?.current_prompt?.trim() === '') return
|
||||
copy(values.current_prompt)
|
||||
message.success(t('common.copySuccess'))
|
||||
}
|
||||
const handleAdd = () => {
|
||||
aiPromptVariableModalRef.current?.handleOpen()
|
||||
}
|
||||
const handleVariableApply = (value: string) => {
|
||||
if (editorRef.current?.insertText) {
|
||||
editorRef.current.insertText(value)
|
||||
} else {
|
||||
form.setFieldValue('current_prompt', (values.current_prompt || '') + value)
|
||||
}
|
||||
}
|
||||
const handleSave = () => {
|
||||
if (!values.current_prompt || !promptSession) {
|
||||
return
|
||||
}
|
||||
promptSaveModalRef.current?.handleOpen({
|
||||
session_id: promptSession,
|
||||
prompt: values.current_prompt
|
||||
})
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
form.setFieldValue('current_prompt', undefined)
|
||||
currentPromptValueRef.current = undefined;
|
||||
setChatList([])
|
||||
refresh()
|
||||
}
|
||||
|
||||
console.log(values)
|
||||
return (
|
||||
<>
|
||||
<Form form={form}>
|
||||
<div className="rb:grid rb:grid-cols-2 rb:-my-4">
|
||||
<div className="rb:border-r rb:border-r-[#EBEBEB] rb:pr-6 rb:pt-3">
|
||||
<Form.Item
|
||||
label={t('prompt.model')}
|
||||
name="model_id"
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<ChatContent
|
||||
classNames="rb:h-[calc(100vh-260px)] rb:px-[16px] rb:py-[20px] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-[8px]"
|
||||
contentClassNames="rb:max-w-[260px]!"
|
||||
empty={<Empty url={ConversationEmptyIcon} title={t('prompt.promptChatEmpty')} isNeedSubTitle={false} size={[240, 200]} className="rb:h-full" />}
|
||||
data={chatList || []}
|
||||
streamLoading={false}
|
||||
labelPosition="top"
|
||||
labelFormat={(item) => item.role === 'user' ? t('prompt.you') : t('prompt.ai')}
|
||||
/>
|
||||
|
||||
<div className="rb:flex rb:items-center rb:gap-2.5 rb:py-4">
|
||||
<Form.Item name="message" className="rb:mb-0!" style={{ width: 'calc(100% - 54px)' }}>
|
||||
<Input
|
||||
className="rb:h-11 rb:shadow-[0px_2px_8px_0px_rgba(33,35,50,0.1)]"
|
||||
placeholder={t('prompt.promptChatPlaceholder')}
|
||||
onPressEnter={handleSend}
|
||||
/>
|
||||
</Form.Item>
|
||||
<img src={ChatSendIcon} className={clsx("rb:w-11 rb:h-11 rb:cursor-pointer", {
|
||||
'rb:opacity-50': loading,
|
||||
})} onClick={handleSend} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rb:pl-6 rb:pt-3">
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item label={t('prompt.conversationOptimizationPrompt')}></Form.Item>
|
||||
</Col>
|
||||
<Col span={12} className="rb:text-right">
|
||||
<Button onClick={handleAdd}>+ {t('prompt.addVariable')}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="current_prompt">
|
||||
<Editor
|
||||
ref={editorRef}
|
||||
placeholder={t('prompt.promptPlaceholder')}
|
||||
className="rb:h-[calc(100vh-260px)]"
|
||||
// onChange={(value) => form.setFieldValue('current_prompt', value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-4 rb:mt-6">
|
||||
<Button type="primary" block disabled={!values?.current_prompt} onClick={handleSave}>{t('common.save')}</Button>
|
||||
<Button block disabled={!values?.current_prompt} onClick={handleCopy}>{t('common.copy')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<PromptVariableModal
|
||||
ref={aiPromptVariableModalRef}
|
||||
variables={variables}
|
||||
refresh={handleVariableApply}
|
||||
/>
|
||||
|
||||
<PromptSaveModal
|
||||
ref={promptSaveModalRef}
|
||||
refresh={handleRefresh}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Prompt;
|
||||
82
web/src/views/Prompt/components/PromptDetail.tsx
Normal file
82
web/src/views/Prompt/components/PromptDetail.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Flex, Button, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
import type { HistoryItem, PromptDetailRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
|
||||
const PromptDetail = forwardRef<PromptDetailRef, { handleEdit: (item: HistoryItem) => void; handleDelete: (item: HistoryItem) => void; }>(({ handleEdit, handleDelete }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp()
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [data, setData] = useState<HistoryItem>({} as HistoryItem)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOpen = (vo: HistoryItem) => {
|
||||
setVisible(true);
|
||||
setData(vo)
|
||||
};
|
||||
const handleCopy = (text = '') => {
|
||||
copy(text)
|
||||
message.success(t('common.copySuccess'))
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
return (
|
||||
<RbModal
|
||||
title={<div>
|
||||
{data.title}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-normal rb:mt-1!">{formatDateTime(data.created_at)}</div>
|
||||
</div>}
|
||||
open={visible}
|
||||
footer={
|
||||
<Flex justify="end" gap={8}>
|
||||
<Button danger onClick={() => handleDelete(data)}>{t('common.delete')}</Button>
|
||||
<Button type="primary" onClick={() => {
|
||||
handleClose()
|
||||
handleEdit(data)
|
||||
}}>{t('common.edit')}</Button>
|
||||
</Flex>
|
||||
}
|
||||
onCancel={handleClose}
|
||||
width={1000}
|
||||
>
|
||||
<Flex justify="space-between">
|
||||
{t('prompt.initialInput')}
|
||||
<Button className="rb:group" size="small" disabled={!data.first_message || data.first_message.trim() === ''} onClick={() => handleCopy(data.first_message)}>
|
||||
<div
|
||||
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
|
||||
></div>
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<div className="rb:my-3 rb:bg-[#F6F8FC] rb:border-[#DFE4ED] rb:rounded-lg rb:p-3">
|
||||
<Markdown content={data.first_message} className="rb:min-h-5 rb:max-h-50 rb:overflow-y-auto" />
|
||||
</div>
|
||||
|
||||
<Flex justify="space-between">
|
||||
{t('prompt.conversationOptimizationPrompt')}
|
||||
<Button className="rb:group" size="small" onClick={() => handleCopy(data.prompt)}>
|
||||
<div
|
||||
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
|
||||
></div>
|
||||
</Button>
|
||||
</Flex>
|
||||
<div className="rb:relative rb:my-3 rb:overflow-hidden rb:bg-[#F6F8FC] rb:border-[#DFE4ED] rb:rounded-lg rb:p-3">
|
||||
<Markdown content={data.prompt} className="rb:min-h-5 rb:max-h-70 rb:overflow-y-auto" />
|
||||
</div>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default PromptDetail;
|
||||
90
web/src/views/Prompt/components/PromptSaveModal.tsx
Normal file
90
web/src/views/Prompt/components/PromptSaveModal.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { PromptSaveModalRef, PromptReleaseData } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { savePrompt } from '@/api/prompt'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface PromptSaveModalProps {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
const PromptSaveModal = forwardRef<PromptSaveModalRef, PromptSaveModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<{ title?: string; }>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [data, setData] = useState<PromptReleaseData | null>(null)
|
||||
const title = Form.useWatch(['title'], form)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setData(null)
|
||||
};
|
||||
|
||||
const handleOpen = (vo: PromptReleaseData) => {
|
||||
setData(vo)
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
if (!title || title.trim() === '') {
|
||||
message.warning(t('common.inputPlaceholder', { title: t('prompt.saveTitle') }))
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
savePrompt({
|
||||
...data,
|
||||
title
|
||||
} as PromptReleaseData)
|
||||
.then(() => {
|
||||
setLoading(false)
|
||||
refresh()
|
||||
handleClose()
|
||||
message.success(t('common.saveSuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('prompt.saveTitle')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<FormItem
|
||||
name="title"
|
||||
noStyle
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default PromptSaveModal;
|
||||
104
web/src/views/Prompt/components/PromptVariableModal.tsx
Normal file
104
web/src/views/Prompt/components/PromptVariableModal.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { Form, AutoComplete, type AutoCompleteProps } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { PromptVariableModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface PromptVariableModalProps {
|
||||
refresh: (value: string) => void;
|
||||
variables: string[];
|
||||
}
|
||||
|
||||
const PromptVariableModal = forwardRef<PromptVariableModalRef, PromptVariableModalProps>(({
|
||||
refresh,
|
||||
variables
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [options, setOptions] = useState<AutoCompleteProps['options']>([])
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(variables.map(key => ({
|
||||
value: key,
|
||||
label: `{{${key}}}`
|
||||
})))
|
||||
}, [variables])
|
||||
const handleSearch = (value: string) => {
|
||||
const filterKeys = variables?.filter(key => key.includes(value))
|
||||
|
||||
if (filterKeys.length) {
|
||||
setOptions(filterKeys.map(key => ({
|
||||
value: key,
|
||||
label: `{{${key}}}`
|
||||
})))
|
||||
} else {
|
||||
setOptions([{
|
||||
value: value,
|
||||
label: `{{${value}}}`
|
||||
}])
|
||||
}
|
||||
}
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
setVisible(true);
|
||||
form.resetFields();
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
const variableName = form.getFieldValue('variableName')
|
||||
|
||||
if (!variableName) return
|
||||
|
||||
refresh(`{{${variableName}}}`)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('application.addVariable')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
confirmLoading={loading}
|
||||
onOk={handleSave}
|
||||
okText={t('application.apply')}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
|
||||
>
|
||||
<FormItem
|
||||
name="variableName"
|
||||
label={t('application.defineVariableName')}
|
||||
extra={t('application.defineVariableNameExtra')}
|
||||
>
|
||||
<AutoComplete
|
||||
placeholder={t('application.defineVariableNamePlaceholder')}
|
||||
onSearch={handleSearch}
|
||||
options={options}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default PromptVariableModal;
|
||||
59
web/src/views/Prompt/index.tsx
Normal file
59
web/src/views/Prompt/index.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { type FC, useState } from 'react';
|
||||
import { type SegmentedProps, Flex } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import PageTabs from '@/components/PageTabs';
|
||||
import SearchInput from '@/components/SearchInput'
|
||||
import PromptEditor from './Prompt';
|
||||
import History from './History'
|
||||
import type { HistoryQuery, HistoryItem } from './types';
|
||||
|
||||
const tabs = ['editor', 'history']
|
||||
const Prompt: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<SegmentedProps['value']>(tabs[0])
|
||||
const [query, setQuery] = useState<HistoryQuery>({});
|
||||
const [editVo, setEditVo] = useState<HistoryItem | null>(null)
|
||||
|
||||
const handleChangeTab = (value: SegmentedProps['value']) => {
|
||||
setActiveTab(value)
|
||||
setEditVo(null)
|
||||
setQuery({})
|
||||
}
|
||||
const handleSearch = (value?: string) => {
|
||||
setQuery(prev => ({ ...prev, keyword: value }))
|
||||
}
|
||||
const handleEdit = (item: HistoryItem) => {
|
||||
console.log('edit', item)
|
||||
setEditVo(item)
|
||||
setActiveTab('editor')
|
||||
}
|
||||
const refresh = () => {
|
||||
setEditVo(null)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Flex justify="space-between" align="center" className="rb:mb-4">
|
||||
<PageTabs
|
||||
value={activeTab}
|
||||
options={tabs.map(key => ({ label: t(`prompt.${key}`), value: key }))}
|
||||
onChange={handleChangeTab}
|
||||
/>
|
||||
{activeTab === 'history' &&
|
||||
<SearchInput
|
||||
placeholder={t('prompt.historySearchPlaceholder')}
|
||||
onSearch={handleSearch}
|
||||
className="rb:w-70"
|
||||
/>
|
||||
}
|
||||
</Flex>
|
||||
|
||||
<div className="rb:mt-4 rb:h-[calc(100vh-128px)]">
|
||||
{activeTab === 'editor' && <PromptEditor editVo={editVo} refresh={refresh} />}
|
||||
{activeTab === 'history' && <History query={query} edit={handleEdit} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Prompt;
|
||||
35
web/src/views/Prompt/types.ts
Normal file
35
web/src/views/Prompt/types.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export interface PromptVariableModalRef {
|
||||
handleOpen: () => void;
|
||||
}
|
||||
|
||||
export interface AiPromptForm {
|
||||
model_id?: string;
|
||||
message?: string;
|
||||
current_prompt?: string;
|
||||
}
|
||||
|
||||
export interface PromptReleaseData {
|
||||
session_id: string;
|
||||
title?: string;
|
||||
prompt: string;
|
||||
}
|
||||
export interface HistoryQuery extends Record<string, unknown> {
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface HistoryItem {
|
||||
id: string;
|
||||
title: string;
|
||||
prompt: string;
|
||||
created_at: number;
|
||||
first_message: string;
|
||||
}
|
||||
|
||||
export interface PromptDetailRef {
|
||||
handleOpen: (vo: HistoryItem) => void;
|
||||
handleClose: () => void;
|
||||
}
|
||||
|
||||
export interface PromptSaveModalRef {
|
||||
handleOpen: (vo: PromptReleaseData) => void;
|
||||
}
|
||||
@@ -24,7 +24,7 @@ const configList = [
|
||||
key: 'reflection_model_id',
|
||||
type: 'customSelect',
|
||||
url: getModelListUrl,
|
||||
params: { type: 'chat,llm', page: 1, pagesize: 100 }, // chat,llm
|
||||
params: { type: 'chat,llm', page: 1, pagesize: 100, is_active: true }, // chat,llm
|
||||
},
|
||||
// 迭代周期
|
||||
{
|
||||
|
||||
@@ -66,7 +66,7 @@ const SpaceConfig: FC = () => {
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'llm', pagesize: 100 }}
|
||||
params={{ type: 'llm', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
@@ -80,7 +80,7 @@ const SpaceConfig: FC = () => {
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'embedding', pagesize: 100 }}
|
||||
params={{ type: 'embedding', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
@@ -94,7 +94,7 @@ const SpaceConfig: FC = () => {
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'rerank', pagesize: 100 }}
|
||||
params={{ type: 'rerank', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { createWorkspace } from '@/api/workspaces'
|
||||
import RadioGroupCard from '@/components/RadioGroupCard'
|
||||
import { getModelListUrl, getModelList } from '@/api/models'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import type { Model } from '@/views/ModelManagement/types'
|
||||
import type { ModelListItem } from '@/views/ModelManagement/types'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
@@ -29,7 +29,7 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
||||
const [form] = Form.useForm<SpaceModalData>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [editVo, setEditVo] = useState<Space | null>(null)
|
||||
const [modelList, setModelList] = useState<Model[]>([])
|
||||
const [modelList, setModelList] = useState<ModelListItem[]>([])
|
||||
|
||||
const values = Form.useWatch([], form);
|
||||
|
||||
@@ -80,9 +80,9 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
||||
}, [])
|
||||
|
||||
const getModels = () => {
|
||||
getModelList({ type: 'llm,chat', pagesize: 100, page: 1 })
|
||||
getModelList({ type: 'llm,chat', pagesize: 100, page: 1, is_active: true })
|
||||
.then(res => {
|
||||
const response = res as { items: Model[] }
|
||||
const response = res as { items: ModelListItem[] }
|
||||
setModelList(response.items)
|
||||
})
|
||||
}
|
||||
@@ -134,7 +134,7 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'embedding', pagesize: 100 }}
|
||||
params={{ type: 'embedding', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
@@ -148,7 +148,7 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'rerank', pagesize: 100 }}
|
||||
params={{ type: 'rerank', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
|
||||
@@ -59,6 +59,11 @@ const PerceptualLastInfo: FC<{ type: 'last_visual' | 'last_listen' | 'last_text'
|
||||
})
|
||||
}
|
||||
|
||||
const handleDownload = () => {
|
||||
if (!data.file_path) return
|
||||
window.open(data.file_path, '_blank')
|
||||
}
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t(`perceptualDetail.${type}`)}
|
||||
@@ -78,17 +83,17 @@ const PerceptualLastInfo: FC<{ type: 'last_visual' | 'last_listen' | 'last_text'
|
||||
<Image src={data.file_path} alt={data.file_name} />
|
||||
// <img src={data.file_path} alt={data.file_name} className="rb:max-w-full rb:max-h-full rb:object-contain" />
|
||||
) : (
|
||||
<div className="rb:text-gray-500">{data.file_name}</div>
|
||||
<div className="rb:text-[#5B6167]">{data.file_name}</div>
|
||||
)
|
||||
) : type === 'last_listen' && /\.(mp3|wav|ogg|m4a|aac)$/i.test(data.file_name) ? (
|
||||
<audio controls className="rb:w-full">
|
||||
<source src={data.file_path} />
|
||||
</audio>
|
||||
) : (
|
||||
<div className="rb:text-gray-500">{data.file_name}</div>
|
||||
<div className="rb:text-[#5B6167] rb:cursor-pointer" onClick={handleDownload}>{data.file_name}</div>
|
||||
)
|
||||
) : (
|
||||
<div className="rb:text-gray-400">No file</div>
|
||||
<div className="rb:text-[#5B6167]">{t('empty.tableEmpty')}</div>
|
||||
)}
|
||||
</div>
|
||||
<Space size={4} direction="vertical" className="rb:w-full rb:mt-3">
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { forwardRef, useImperativeHandle, useState, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import clsx from 'clsx'
|
||||
import { Input, Form, App } from 'antd'
|
||||
import { Space, Button } from 'antd'
|
||||
import { Input, Form, App, Space, Button, Collapse } from 'antd'
|
||||
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons'
|
||||
import CodeBlock from '@/components/Markdown/CodeBlock'
|
||||
|
||||
import ChatIcon from '@/assets/images/application/chat.png'
|
||||
import RbDrawer from '@/components/RbDrawer';
|
||||
@@ -13,8 +14,11 @@ import ChatContent from '@/components/Chat/ChatContent'
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
import ChatSendIcon from '@/assets/images/application/chatSend.svg'
|
||||
import dayjs from 'dayjs'
|
||||
import type { ChatRef, VariableConfigModalRef, StartVariableItem, GraphRef } from '../../types'
|
||||
import type { ChatRef, VariableConfigModalRef, GraphRef } from '../../types'
|
||||
import { type SSEMessage } from '@/utils/stream'
|
||||
import type { Variable } from '../Properties/VariableList/types'
|
||||
import styles from './chat.module.css'
|
||||
import Markdown from '@/components/Markdown'
|
||||
|
||||
const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId, graphRef }, ref) => {
|
||||
const { t } = useTranslation()
|
||||
@@ -24,7 +28,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
const [open, setOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [chatList, setChatList] = useState<ChatItem[]>([])
|
||||
const [variables, setVariables] = useState<StartVariableItem[]>([])
|
||||
const [variables, setVariables] = useState<Variable[]>([])
|
||||
const [streamLoading, setStreamLoading] = useState(false)
|
||||
const [conversationId, setConversationId] = useState<string | null>(null)
|
||||
|
||||
@@ -39,7 +43,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
if (startNodes.length) {
|
||||
const curVariables = startNodes[0].config.variables?.defaultValue
|
||||
|
||||
curVariables.forEach((vo: StartVariableItem) => {
|
||||
curVariables.forEach((vo: Variable) => {
|
||||
if (typeof vo.default !== 'undefined') {
|
||||
vo.value = vo.default
|
||||
}
|
||||
@@ -60,7 +64,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
const handleEditVariables = () => {
|
||||
variableConfigModalRef.current?.handleOpen(variables)
|
||||
}
|
||||
const handleSave = (values: StartVariableItem[]) => {
|
||||
const handleSave = (values: Variable[]) => {
|
||||
setVariables([...values])
|
||||
}
|
||||
const handleSend = () => {
|
||||
@@ -97,13 +101,28 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
created_at: Date.now(),
|
||||
subContent: [],
|
||||
}])
|
||||
|
||||
const handleStreamMessage = (data: SSEMessage[]) => {
|
||||
setStreamLoading(false)
|
||||
|
||||
data.forEach(item => {
|
||||
const { chunk, conversation_id } = item.data as { chunk: string; conversation_id: string | null; };
|
||||
const { chunk, conversation_id, node_id, input, output, error, elapsed_time, status } = item.data as {
|
||||
chunk: string;
|
||||
conversation_id: string | null;
|
||||
node_id: string;
|
||||
node_name?: string;
|
||||
input?: any;
|
||||
output?: any;
|
||||
elapsed_time?: string;
|
||||
error?: any;
|
||||
state: Record<string, any>;
|
||||
status?: 'completed' | 'failed'
|
||||
};
|
||||
|
||||
const node = graphRef.current?.getNodes().find(n => n.id === node_id);
|
||||
const { name, icon } = node?.getData() || {}
|
||||
|
||||
console.log('node', node?.getData())
|
||||
|
||||
switch(item.event) {
|
||||
case 'message':
|
||||
@@ -119,6 +138,66 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
return newList
|
||||
})
|
||||
break
|
||||
case 'node_start':
|
||||
setChatList(prev => {
|
||||
const newList = [...prev]
|
||||
const lastIndex = newList.length - 1
|
||||
if (lastIndex >= 0) {
|
||||
const newSubContent = newList[lastIndex].subContent || []
|
||||
const filterIndex = newSubContent.findIndex(vo => vo.id === node_id)
|
||||
if (filterIndex > -1) {
|
||||
newSubContent[filterIndex] = {
|
||||
...newSubContent[filterIndex],
|
||||
node_id: node_id,
|
||||
node_name: name,
|
||||
icon,
|
||||
content: {},
|
||||
}
|
||||
} else {
|
||||
newSubContent.push({
|
||||
id: node_id,
|
||||
node_id: node_id,
|
||||
node_name: name,
|
||||
icon,
|
||||
content: {},
|
||||
})
|
||||
}
|
||||
newList[lastIndex] = {
|
||||
...newList[lastIndex],
|
||||
subContent: newSubContent
|
||||
}
|
||||
}
|
||||
return newList
|
||||
})
|
||||
break
|
||||
case 'node_end':
|
||||
case 'node_error':
|
||||
setChatList(prev => {
|
||||
const newList = [...prev]
|
||||
const lastIndex = newList.length - 1
|
||||
if (lastIndex >= 0) {
|
||||
const newSubContent = newList[lastIndex].subContent || []
|
||||
const filterIndex = newSubContent.findIndex(vo => vo.node_id === node_id)
|
||||
if (filterIndex > -1 && newSubContent[filterIndex].content) {
|
||||
newSubContent[filterIndex] = {
|
||||
...newSubContent[filterIndex],
|
||||
content: {
|
||||
input,
|
||||
output,
|
||||
error,
|
||||
},
|
||||
status: status || 'completed',
|
||||
elapsed_time
|
||||
}
|
||||
}
|
||||
newList[lastIndex] = {
|
||||
...newList[lastIndex],
|
||||
subContent: newSubContent
|
||||
}
|
||||
}
|
||||
return newList
|
||||
})
|
||||
break
|
||||
case 'workflow_end':
|
||||
setChatList(prev => {
|
||||
const newList = [...prev]
|
||||
@@ -126,6 +205,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
if (lastIndex >= 0) {
|
||||
newList[lastIndex] = {
|
||||
...newList[lastIndex],
|
||||
status,
|
||||
content: newList[lastIndex].content === '' ? null : newList[lastIndex].content
|
||||
}
|
||||
}
|
||||
@@ -142,14 +222,31 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
}
|
||||
|
||||
form.setFieldValue('message', undefined)
|
||||
setStreamLoading(true)
|
||||
draftRun(appId, {
|
||||
message: message,
|
||||
variables: params,
|
||||
stream: true,
|
||||
conversation_id: conversationId
|
||||
}, handleStreamMessage)
|
||||
.catch((error) => {
|
||||
setChatList(prev => {
|
||||
const newList = [...prev]
|
||||
const lastIndex = newList.length - 1
|
||||
if (lastIndex >= 0) {
|
||||
newList[lastIndex] = {
|
||||
...newList[lastIndex],
|
||||
status: 'failed',
|
||||
content: null,
|
||||
subContent: error.error
|
||||
}
|
||||
}
|
||||
return newList
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
setStreamLoading(false)
|
||||
})
|
||||
}
|
||||
// 暴露给父组件的方法
|
||||
@@ -158,6 +255,11 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
handleClose
|
||||
}));
|
||||
|
||||
const getStatus = (status?: string) => {
|
||||
return status === 'completed' ? 'rb:text-[#369F21]' : status === 'failed' ? 'rb:text-[#FF5D34]' : 'rb:text-[#5B6167]'
|
||||
}
|
||||
|
||||
console.log('chatList', chatList)
|
||||
return (
|
||||
<RbDrawer
|
||||
title={<div className="rb:flex rb:items-center rb:gap-2.5">
|
||||
@@ -173,10 +275,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
onClose={handleClose}
|
||||
>
|
||||
<ChatContent
|
||||
classNames={{
|
||||
'rb:mx-[16px] rb:pt-[24px] rb:h-[calc(100%-76px)]': true,
|
||||
|
||||
}}
|
||||
classNames="rb:mx-[16px] rb:pt-[24px] rb:h-[calc(100%-76px)]"
|
||||
contentClassNames="rb:max-w-[400px]!'"
|
||||
empty={<Empty url={ChatIcon} title={t('application.chatEmpty')} isNeedSubTitle={false} size={[240, 200]} className="rb:h-full" />}
|
||||
data={chatList}
|
||||
@@ -184,6 +283,87 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
labelPosition="bottom"
|
||||
labelFormat={(item) => dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')}
|
||||
errorDesc={t('application.ReplyException')}
|
||||
renderRuntime={(item, index) => {
|
||||
return (
|
||||
<div key={index} className="rb:w-100 rb:mb-2">
|
||||
<Collapse
|
||||
className={styles[item.status || 'default']}
|
||||
items={[{
|
||||
key: 0,
|
||||
label: <div className={getStatus(item.status)}>
|
||||
{item.status === 'completed' ? <CheckCircleFilled className="rb:mr-1" /> : item.status === 'failed' ? <CloseCircleFilled className="rb:mr-1" /> : <LoadingOutlined className="rb:mr-1" />}
|
||||
{t('application.workflow')}
|
||||
</div>,
|
||||
className: styles.collapseItem,
|
||||
children: (
|
||||
Array.isArray(item.subContent)
|
||||
? <Space size={8} direction="vertical" className="rb:w-full!">
|
||||
{item.subContent?.map(vo => (
|
||||
<Collapse
|
||||
key={vo.node_id}
|
||||
items={[{
|
||||
key: vo.node_id,
|
||||
label: <div className={clsx("rb:flex rb:justify-between rb:items-center", getStatus(vo.status))}>
|
||||
<div className="rb:flex rb:items-center rb:gap-1 rb:flex-1">
|
||||
{vo.icon && <img src={vo.icon} className="rb:size-4" />}
|
||||
<div className="rb:wrap-break-word rb:line-clamp-1">{vo.node_name || vo.node_id}</div>
|
||||
</div>
|
||||
<span>
|
||||
{typeof vo.elapsed_time == 'number' && <>{vo.elapsed_time?.toFixed(3)}ms</>}
|
||||
{vo.status === 'completed' ? <CheckCircleFilled className="rb:ml-1" /> : vo.status === 'failed' ? <CloseCircleFilled className="rb:ml-1" /> : <LoadingOutlined className="rb:ml-1" />}
|
||||
</span>
|
||||
</div>,
|
||||
className: styles.collapseItem,
|
||||
children: (
|
||||
<Space size={8} direction="vertical" className="rb:w-full!">
|
||||
{vo.status === 'failed' &&
|
||||
<div className={clsx("rb:bg-[#F0F3F8] rb:rounded-md", getStatus(vo.status))}>
|
||||
<div className="rb:py-2 rb:px-3 rb:flex rb:justify-between rb:items-center rb:text-[12px]">
|
||||
{t(`workflow.error`)}
|
||||
<Button
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>{t('common.copy')}</Button>
|
||||
</div>
|
||||
<div className="rb:pb-2 rb:px-3 rb:max-h-40 rb:overflow-auto">
|
||||
<Markdown content={vo.content?.error || ''} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{['input', 'output'].map(key => (
|
||||
<div key={key} className="rb:bg-[#F0F3F8] rb:rounded-md">
|
||||
<div className="rb:py-2 rb:px-3 rb:flex rb:justify-between rb:items-center rb:text-[12px]">
|
||||
{t(`workflow.${key}`)}
|
||||
<Button
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>{t('common.copy')}</Button>
|
||||
</div>
|
||||
<div className="rb:max-h-40 rb:overflow-auto">
|
||||
<CodeBlock
|
||||
size="small"
|
||||
value={typeof vo.content === 'object' && vo.content?.[key] ? JSON.stringify(vo.content[key], null, 2) : '{}'}
|
||||
needCopy={false}
|
||||
showLineNumbers={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
)
|
||||
}]}
|
||||
/>
|
||||
))}
|
||||
</Space>
|
||||
: <div className={clsx("rb:bg-[#FBFDFF] rb:rounded-md rb:py-2 rb:px-3 ", getStatus('failed'))}>
|
||||
<Markdown content={item.subContent || ''} />
|
||||
</div>
|
||||
)
|
||||
}]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<div className="rb:flex rb:items-center rb:gap-2.5 rb:p-4">
|
||||
<Form form={form} style={{width: 'calc(100% - 54px)'}}>
|
||||
|
||||
45
web/src/views/Workflow/components/Chat/chat.module.css
Normal file
45
web/src/views/Workflow/components/Chat/chat.module.css
Normal file
@@ -0,0 +1,45 @@
|
||||
.completed {
|
||||
background-color: rgba(54, 159, 33, 0.06);
|
||||
border-color: rgba(54, 159, 33, 0.25);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.failed {
|
||||
background-color: rgba(255, 138, 76, 0.08);
|
||||
border-color: rgba(255, 138, 76, 0.20);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.default {
|
||||
background-color: rgba(91, 97, 103, 0.08);
|
||||
border-color: rgba(91, 97, 103, 0.30);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.collapse-item {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
.collapse-item:global(.ant-collapse-item>.ant-collapse-header) {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.collapse-item:global(.ant-collapse-item>.ant-collapse-header .ant-collapse-expand-icon) {
|
||||
height: 16px;
|
||||
}
|
||||
.completed:global(.ant-collapse .ant-collapse-content),
|
||||
.failed:global(.ant-collapse .ant-collapse-content) {
|
||||
background-color: transparent;
|
||||
border-top: none;
|
||||
}
|
||||
:global(.ant-collapse .ant-collapse-content>.ant-collapse-content-box) {
|
||||
padding-top: 0;
|
||||
}
|
||||
.collapse-item :global(.ant-collapse) {
|
||||
/* background-color: #F0F3F8; */
|
||||
background-color: #FBFDFF;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.collapse-item :global(.ant-collapse>.ant-collapse-item:last-child),
|
||||
.collapse-item :global(.ant-collapse>.ant-collapse-item:last-child>.ant-collapse-header) {
|
||||
border-radius: 0 0 6px 6px;
|
||||
}
|
||||
.collapse-item :global(.ant-collapse .ant-collapse-content>.ant-collapse-content-box) {
|
||||
padding: 0 4px 4px 4px;
|
||||
}
|
||||
@@ -15,22 +15,24 @@ import CharacterCountPlugin from './plugin/CharacterCountPlugin'
|
||||
import InitialValuePlugin from './plugin/InitialValuePlugin';
|
||||
import CommandPlugin from './plugin/CommandPlugin';
|
||||
import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin';
|
||||
import Python3HighlightPlugin from './plugin/Python3HighlightPlugin';
|
||||
import JavaScriptHighlightPlugin from './plugin/JavaScriptHighlightPlugin';
|
||||
import LineNumberPlugin from './plugin/LineNumberPlugin';
|
||||
import BlurPlugin from './plugin/BlurPlugin';
|
||||
import { VariableNode } from './nodes/VariableNode'
|
||||
|
||||
interface LexicalEditorProps {
|
||||
export interface LexicalEditorProps {
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
options: Suggestion[];
|
||||
options?: Suggestion[];
|
||||
variant?: 'outlined' | 'borderless';
|
||||
height?: number;
|
||||
fontSize?: number;
|
||||
lineHeight?: number;
|
||||
enableJinja2?: boolean;
|
||||
size?: 'default' | 'small';
|
||||
type?: 'input' | 'textarea'
|
||||
type?: 'input' | 'textarea',
|
||||
language?: 'string' | 'jinja2' | 'python3' | 'javascript'
|
||||
}
|
||||
|
||||
const theme = {
|
||||
@@ -54,20 +56,25 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
placeholder = "请输入内容...",
|
||||
value = "",
|
||||
onChange,
|
||||
options,
|
||||
options = [],
|
||||
variant = 'borderless',
|
||||
enableJinja2 = false,
|
||||
size = 'default',
|
||||
type = 'textarea'
|
||||
type = 'textarea',
|
||||
language = 'string'
|
||||
}) => {
|
||||
|
||||
const [_count, setCount] = useState(0);
|
||||
const [enableJinja2, setEnableJinja2] = useState(false)
|
||||
const [enableLineNumbers, setEnableLineNumbers] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (enableJinja2) {
|
||||
const styleId = 'jinja2-styles';
|
||||
const needsLineNumbers = language === 'jinja2' || language === 'python3' || language === 'javascript';
|
||||
setEnableJinja2(language === 'jinja2');
|
||||
setEnableLineNumbers(needsLineNumbers);
|
||||
|
||||
if (needsLineNumbers) {
|
||||
const styleId = 'code-editor-styles';
|
||||
let existingStyle = document.getElementById(styleId);
|
||||
|
||||
|
||||
if (!existingStyle) {
|
||||
const style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
@@ -119,6 +126,7 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
}
|
||||
.editor-content-with-numbers {
|
||||
white-space: pre-wrap;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
}
|
||||
.editor-content-with-numbers p {
|
||||
margin: 0;
|
||||
@@ -128,7 +136,8 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
}, [enableJinja2]);
|
||||
}, [language])
|
||||
|
||||
const initialConfig = {
|
||||
namespace: 'AutocompleteEditor',
|
||||
theme: enableJinja2 ? jinja2Theme : theme,
|
||||
@@ -168,7 +177,7 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
<div style={{ position: 'relative' }}>
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
enableJinja2 ? (
|
||||
enableLineNumbers ? (
|
||||
<div className="editor-with-line-numbers" style={{
|
||||
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
|
||||
borderRadius: '6px',
|
||||
@@ -212,8 +221,8 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
style={{
|
||||
minHeight: placeHolderMinheight,
|
||||
position: 'absolute',
|
||||
top: enableJinja2 ? '4px' : variant === 'borderless' ? '0' : '6px',
|
||||
left: enableJinja2 ? '16px' : (variant === 'borderless' ? '0' : '11px'),
|
||||
top: enableLineNumbers ? '4px' : variant === 'borderless' ? '0' : '6px',
|
||||
left: enableLineNumbers ? '16px' : (variant === 'borderless' ? '0' : '11px'),
|
||||
color: '#A8A9AA',
|
||||
fontSize: fontSize,
|
||||
lineHeight: placeHolderMinheight,
|
||||
@@ -227,12 +236,14 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
/>
|
||||
<HistoryPlugin />
|
||||
<CommandPlugin />
|
||||
{enableJinja2 && <Jinja2HighlightPlugin />}
|
||||
{enableJinja2 && <LineNumberPlugin />}
|
||||
{language === 'jinja2' && <Jinja2HighlightPlugin />}
|
||||
{language === 'python3' && <Python3HighlightPlugin />}
|
||||
{language === 'javascript' && <JavaScriptHighlightPlugin />}
|
||||
{enableLineNumbers && <LineNumberPlugin />}
|
||||
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
|
||||
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
||||
<InitialValuePlugin value={value} options={options} enableJinja2={enableJinja2} />
|
||||
{enableJinja2 && <BlurPlugin />}
|
||||
{enableLineNumbers && <BlurPlugin />}
|
||||
</div>
|
||||
</LexicalComposer>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { TextNode, $createTextNode, $getSelection, $isRangeSelection } from 'lexical';
|
||||
|
||||
const JS_KEYWORDS = new Set([
|
||||
'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default',
|
||||
'delete', 'do', 'else', 'export', 'extends', 'finally', 'for', 'function', 'if', 'import',
|
||||
'in', 'instanceof', 'let', 'new', 'return', 'super', 'switch', 'this', 'throw', 'try',
|
||||
'typeof', 'var', 'void', 'while', 'with', 'yield', 'true', 'false', 'null', 'undefined'
|
||||
]);
|
||||
|
||||
const JavaScriptHighlightPlugin = () => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
|
||||
const text = textNode.getTextContent();
|
||||
|
||||
if (textNode.hasFormat('code')) return;
|
||||
if (!needsHighlight(text)) return;
|
||||
|
||||
const parent = textNode.getParent();
|
||||
if (!parent) return;
|
||||
|
||||
const selection = $getSelection();
|
||||
let selectionOffset = null;
|
||||
if ($isRangeSelection(selection)) {
|
||||
const anchor = selection.anchor;
|
||||
if (anchor.getNode() === textNode) {
|
||||
selectionOffset = anchor.offset;
|
||||
}
|
||||
}
|
||||
|
||||
const tokens = tokenizeJavaScript(text);
|
||||
if (tokens.length <= 1) return;
|
||||
|
||||
const newNodes = tokens.map(token => {
|
||||
const newNode = $createTextNode(token.text);
|
||||
newNode.toggleFormat('code');
|
||||
|
||||
switch (token.type) {
|
||||
case 'keyword':
|
||||
newNode.setStyle('color: #d73a49; font-weight: 600;');
|
||||
break;
|
||||
case 'string':
|
||||
newNode.setStyle('color: #032f62;');
|
||||
break;
|
||||
case 'comment':
|
||||
newNode.setStyle('color: #6a737d; font-style: italic;');
|
||||
break;
|
||||
case 'number':
|
||||
newNode.setStyle('color: #005cc5; font-weight: 500;');
|
||||
break;
|
||||
case 'function':
|
||||
newNode.setStyle('color: #6f42c1; font-weight: 500;');
|
||||
break;
|
||||
}
|
||||
|
||||
return newNode;
|
||||
});
|
||||
|
||||
if (newNodes.length > 1) {
|
||||
textNode.replace(newNodes[0]);
|
||||
for (let i = 1; i < newNodes.length; i++) {
|
||||
newNodes[i - 1].insertAfter(newNodes[i]);
|
||||
}
|
||||
|
||||
if (selectionOffset !== null && $isRangeSelection(selection)) {
|
||||
let currentOffset = 0;
|
||||
for (const node of newNodes) {
|
||||
const nodeLength = node.getTextContent().length;
|
||||
if (currentOffset + nodeLength >= selectionOffset) {
|
||||
node.select(selectionOffset - currentOffset, selectionOffset - currentOffset);
|
||||
break;
|
||||
}
|
||||
currentOffset += nodeLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
function needsHighlight(text: string): boolean {
|
||||
return /[a-zA-Z0-9_/"'`]/.test(text);
|
||||
}
|
||||
|
||||
function tokenizeJavaScript(text: string): Array<{text: string, type: string}> {
|
||||
const tokens: Array<{text: string, type: string}> = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < text.length) {
|
||||
// Single-line comments
|
||||
if (text.slice(i, i + 2) === '//') {
|
||||
let start = i;
|
||||
while (i < text.length && text[i] !== '\n') i++;
|
||||
tokens.push({ text: text.slice(start, i), type: 'comment' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Multi-line comments
|
||||
if (text.slice(i, i + 2) === '/*') {
|
||||
let start = i;
|
||||
i += 2;
|
||||
while (i < text.length && text.slice(i, i + 2) !== '*/') i++;
|
||||
if (i < text.length) i += 2;
|
||||
tokens.push({ text: text.slice(start, i), type: 'comment' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strings
|
||||
if (text[i] === '"' || text[i] === "'" || text[i] === '`') {
|
||||
const quote = text[i];
|
||||
let start = i++;
|
||||
|
||||
while (i < text.length) {
|
||||
if (text[i] === quote && text[i - 1] !== '\\') {
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
tokens.push({ text: text.slice(start, i), type: 'string' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Numbers
|
||||
if (/\d/.test(text[i])) {
|
||||
let start = i;
|
||||
while (i < text.length && /[\d.]/.test(text[i])) i++;
|
||||
tokens.push({ text: text.slice(start, i), type: 'number' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keywords and identifiers
|
||||
if (/[a-zA-Z_$]/.test(text[i])) {
|
||||
let start = i;
|
||||
while (i < text.length && /[a-zA-Z0-9_$]/.test(text[i])) i++;
|
||||
const word = text.slice(start, i);
|
||||
|
||||
if (JS_KEYWORDS.has(word)) {
|
||||
tokens.push({ text: word, type: 'keyword' });
|
||||
} else if (i < text.length && text[i] === '(') {
|
||||
tokens.push({ text: word, type: 'function' });
|
||||
} else {
|
||||
tokens.push({ text: word, type: 'text' });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Other characters
|
||||
let start = i;
|
||||
while (i < text.length && !/[a-zA-Z0-9_$/"'`]/.test(text[i])) i++;
|
||||
if (start < i) {
|
||||
tokens.push({ text: text.slice(start, i), type: 'text' });
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export default JavaScriptHighlightPlugin;
|
||||
@@ -0,0 +1,159 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { TextNode, $createTextNode, $getSelection, $isRangeSelection } from 'lexical';
|
||||
|
||||
const PYTHON_KEYWORDS = new Set([
|
||||
'False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue',
|
||||
'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import',
|
||||
'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while',
|
||||
'with', 'yield'
|
||||
]);
|
||||
|
||||
const Python3HighlightPlugin = () => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
|
||||
const text = textNode.getTextContent();
|
||||
|
||||
if (textNode.hasFormat('code')) return;
|
||||
if (!needsHighlight(text)) return;
|
||||
|
||||
const parent = textNode.getParent();
|
||||
if (!parent) return;
|
||||
|
||||
const selection = $getSelection();
|
||||
let selectionOffset = null;
|
||||
if ($isRangeSelection(selection)) {
|
||||
const anchor = selection.anchor;
|
||||
if (anchor.getNode() === textNode) {
|
||||
selectionOffset = anchor.offset;
|
||||
}
|
||||
}
|
||||
|
||||
const tokens = tokenizePython(text);
|
||||
if (tokens.length <= 1) return;
|
||||
|
||||
const newNodes = tokens.map(token => {
|
||||
const newNode = $createTextNode(token.text);
|
||||
newNode.toggleFormat('code');
|
||||
|
||||
switch (token.type) {
|
||||
case 'keyword':
|
||||
newNode.setStyle('color: #d73a49; font-weight: 600;');
|
||||
break;
|
||||
case 'string':
|
||||
newNode.setStyle('color: #032f62;');
|
||||
break;
|
||||
case 'comment':
|
||||
newNode.setStyle('color: #6a737d; font-style: italic;');
|
||||
break;
|
||||
case 'number':
|
||||
newNode.setStyle('color: #005cc5; font-weight: 500;');
|
||||
break;
|
||||
case 'function':
|
||||
newNode.setStyle('color: #6f42c1; font-weight: 500;');
|
||||
break;
|
||||
}
|
||||
|
||||
return newNode;
|
||||
});
|
||||
|
||||
if (newNodes.length > 1) {
|
||||
textNode.replace(newNodes[0]);
|
||||
for (let i = 1; i < newNodes.length; i++) {
|
||||
newNodes[i - 1].insertAfter(newNodes[i]);
|
||||
}
|
||||
|
||||
if (selectionOffset !== null && $isRangeSelection(selection)) {
|
||||
let currentOffset = 0;
|
||||
for (const node of newNodes) {
|
||||
const nodeLength = node.getTextContent().length;
|
||||
if (currentOffset + nodeLength >= selectionOffset) {
|
||||
node.select(selectionOffset - currentOffset, selectionOffset - currentOffset);
|
||||
break;
|
||||
}
|
||||
currentOffset += nodeLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
function needsHighlight(text: string): boolean {
|
||||
return /[a-zA-Z0-9_#"']/.test(text);
|
||||
}
|
||||
|
||||
function tokenizePython(text: string): Array<{text: string, type: string}> {
|
||||
const tokens: Array<{text: string, type: string}> = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < text.length) {
|
||||
// Comments
|
||||
if (text[i] === '#') {
|
||||
let start = i;
|
||||
while (i < text.length && text[i] !== '\n') i++;
|
||||
tokens.push({ text: text.slice(start, i), type: 'comment' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strings
|
||||
if (text[i] === '"' || text[i] === "'") {
|
||||
const quote = text[i];
|
||||
let start = i++;
|
||||
const isTriple = text.slice(start, start + 3) === quote.repeat(3);
|
||||
if (isTriple) i += 2;
|
||||
|
||||
while (i < text.length) {
|
||||
if (isTriple && text.slice(i, i + 3) === quote.repeat(3)) {
|
||||
i += 3;
|
||||
break;
|
||||
} else if (!isTriple && text[i] === quote && text[i - 1] !== '\\') {
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
tokens.push({ text: text.slice(start, i), type: 'string' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Numbers
|
||||
if (/\d/.test(text[i])) {
|
||||
let start = i;
|
||||
while (i < text.length && /[\d.]/.test(text[i])) i++;
|
||||
tokens.push({ text: text.slice(start, i), type: 'number' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keywords and identifiers
|
||||
if (/[a-zA-Z_]/.test(text[i])) {
|
||||
let start = i;
|
||||
while (i < text.length && /[a-zA-Z0-9_]/.test(text[i])) i++;
|
||||
const word = text.slice(start, i);
|
||||
|
||||
if (PYTHON_KEYWORDS.has(word)) {
|
||||
tokens.push({ text: word, type: 'keyword' });
|
||||
} else if (i < text.length && text[i] === '(') {
|
||||
tokens.push({ text: word, type: 'function' });
|
||||
} else {
|
||||
tokens.push({ text: word, type: 'text' });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Other characters
|
||||
let start = i;
|
||||
while (i < text.length && !/[a-zA-Z0-9_#"']/.test(text[i])) i++;
|
||||
if (start < i) {
|
||||
tokens.push({ text: text.slice(start, i), type: 'text' });
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export default Python3HighlightPlugin;
|
||||
@@ -0,0 +1,86 @@
|
||||
import { type FC, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Form, Input, Divider, Space, Select } from 'antd';
|
||||
|
||||
interface OutputListProps {
|
||||
label: string;
|
||||
name: string;
|
||||
extra?: ReactNode;
|
||||
}
|
||||
|
||||
const types = [
|
||||
'string',
|
||||
'number',
|
||||
'boolean',
|
||||
'array[string]',
|
||||
'array[number]',
|
||||
'array[boolean]',
|
||||
'array[object]',
|
||||
'object'
|
||||
]
|
||||
const OutputList: FC<OutputListProps> = ({ label, name, extra }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<Form.List name={name}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
|
||||
{label}
|
||||
</div>
|
||||
|
||||
<Space size={8}>
|
||||
{extra}
|
||||
<Button
|
||||
onClick={() => add({ type: 'string' })}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addVariable')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<div key={key} className="rb:flex rb:items-center rb:gap-1 rb:mb-2">
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'name']}
|
||||
noStyle
|
||||
>
|
||||
<Input
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
size="small"
|
||||
className="rb:w-45!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'type']}
|
||||
noStyle
|
||||
>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={types.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.parameter-extractor.${key}`),
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
className="rb:w-22!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
export default OutputList;
|
||||
@@ -0,0 +1,128 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Form, Select, Space, Row, Col, Divider, Button, Tooltip } from 'antd'
|
||||
import { Node } from '@antv/x6'
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import MappingList from '../MappingList'
|
||||
import Editor from '../../Editor'
|
||||
import OutputList from './OutputList'
|
||||
|
||||
interface MappingItem {
|
||||
name?: string
|
||||
value?: string
|
||||
}
|
||||
|
||||
interface CodeExecutionProps {
|
||||
options: Suggestion[]
|
||||
selectedNode: Node
|
||||
}
|
||||
|
||||
const codeTemplate = {
|
||||
python3: `def main(arg1: str, arg2: str):
|
||||
return {
|
||||
"result": arg1 + arg2,
|
||||
}`,
|
||||
javascript: `function main({arg1, arg2}) {
|
||||
return {
|
||||
result: arg1 + arg2
|
||||
}
|
||||
}`
|
||||
}
|
||||
|
||||
const CodeExecution: FC<CodeExecutionProps> = ({ options }) => {
|
||||
const { t } = useTranslation()
|
||||
const form = Form.useFormInstance()
|
||||
const values = Form.useWatch([], form) || {}
|
||||
|
||||
const handleRefresh = () => {
|
||||
const code = form.getFieldValue('code') || ''
|
||||
const language = form.getFieldValue('language') || 'javascript'
|
||||
const currentInput = form.getFieldValue('input_variables') || []
|
||||
|
||||
// Get input_variables names to replace in code
|
||||
const inputNames = currentInput.map((item: MappingItem) => item.name).filter(Boolean).join(', ')
|
||||
|
||||
let newTemplate = code
|
||||
|
||||
if (language === 'javascript') {
|
||||
// Replace function parameters: function name({arg1, arg2}) or function name(arg1, arg2)
|
||||
newTemplate = code.replace(
|
||||
/function(\s+\w+\s*\(\s*)(\{?)([^})]*)\}?(\s*\))/,
|
||||
(_match: string, prefix: string, brace: string, _params: string, suffix: string) => {
|
||||
return `function${prefix}${brace}${inputNames}${brace ? '}' : ''}${suffix}`
|
||||
}
|
||||
)
|
||||
} else if (language === 'python3') {
|
||||
// Replace Python function parameters: def name(arg1, arg2):
|
||||
newTemplate = code.replace(
|
||||
/def(\s+\w+\s*\()([^)]*)(\))/,
|
||||
(_match: string, prefix: string, _params: string, suffix: string) => {
|
||||
return `def${prefix}${inputNames}${suffix}`
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
form.setFieldValue('code', newTemplate)
|
||||
}
|
||||
const handleChangeLanguage = (value: string) => {
|
||||
form.setFieldValue('code', codeTemplate[value as keyof typeof codeTemplate])
|
||||
form.setFieldsValue({
|
||||
input_variables: [{ name: 'arg1' }, { name: 'arg2' }],
|
||||
code: codeTemplate[value as keyof typeof codeTemplate]
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item name="input_variables" noStyle>
|
||||
<MappingList
|
||||
label={t('workflow.config.code.input_variables')}
|
||||
name="input_variables"
|
||||
options={options}
|
||||
valueKey="variable"
|
||||
extra={<Tooltip title={t('workflow.config.code.refreshTip')}>
|
||||
<Button
|
||||
onClick={handleRefresh}
|
||||
className="rb:py-0! rb:px-1.5! rb:text-[12px]! rb:group"
|
||||
size="small"
|
||||
>
|
||||
<div onClick={handleRefresh} className="rb:size-3 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/refresh.svg')] rb:group-hover:bg-[url('@/assets/images/refresh_hover.svg')]"></div>
|
||||
</Button>
|
||||
</Tooltip>}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Space size={8} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5">
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item name="language" noStyle>
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'PYTHON3', value: 'python3' },
|
||||
{ label: 'JAVASCRIPT', value: 'javascript' }
|
||||
]}
|
||||
popupMatchSelectWidth={false}
|
||||
className="rb:font-medium!"
|
||||
onChange={handleChangeLanguage}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="code" noStyle>
|
||||
<Editor size="small" language={values.language} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Divider />
|
||||
<Form.Item name="output_variables" noStyle>
|
||||
<OutputList
|
||||
label={t('workflow.config.code.output_variables')}
|
||||
name="output_variables"
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CodeExecution
|
||||
@@ -144,6 +144,7 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
icon={block ? undefined : <PlusOutlined />}
|
||||
onClick={() => add(createNewRow())}
|
||||
size="small"
|
||||
block={block}
|
||||
className={block ? "rb:mt-1 rb:text-[12px]! rb:bg-transparent!" : "rb:text-[12px]!"}
|
||||
>
|
||||
{block && `+${t('common.add')}`}
|
||||
@@ -155,7 +156,7 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
{title && (
|
||||
<div className="rb:flex rb:items-center rb:mb-2 rb:justify-between">
|
||||
<div className="rb:font-medium rb:text-[12px] rb:leading-4.5">{title}</div>
|
||||
<AddButton block={true} />
|
||||
<AddButton block={false} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -196,6 +196,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options.filter(vo => vo.dataType.includes('file'))}
|
||||
filterBooleanType={true}
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ const JinjaRender: FC<JinjaRenderProps> = ({ selectedNode, options, templateOpti
|
||||
return (
|
||||
<>
|
||||
<Form.Item name="mapping" noStyle>
|
||||
<MappingList name="mapping" options={options} />
|
||||
<MappingList label={t('workflow.config.jinja-render.mapping')} name="mapping" options={options} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="template">
|
||||
@@ -184,7 +184,7 @@ const JinjaRender: FC<JinjaRenderProps> = ({ selectedNode, options, templateOpti
|
||||
title={t('workflow.config.jinja-render.template')}
|
||||
isArray={false}
|
||||
parentName="template"
|
||||
enableJinja2={true}
|
||||
language="jinja2"
|
||||
options={templateOptions}
|
||||
titleVariant="borderless"
|
||||
size="small"
|
||||
|
||||
@@ -66,7 +66,7 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
|
||||
useEffect(() => {
|
||||
if (values?.retrieve_type) {
|
||||
const fieldsToReset = Object.keys(values).filter(key =>
|
||||
key !== 'kb_id' && key !== 'retrieve_type'
|
||||
key !== 'kb_id' && key !== 'retrieve_type' && key !== 'top_k'
|
||||
) as (keyof KnowledgeConfigForm)[];
|
||||
form.resetFields(fieldsToReset);
|
||||
}
|
||||
@@ -108,6 +108,7 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
|
||||
label: t(`application.${key}`),
|
||||
value: key,
|
||||
}))}
|
||||
// onChange={handleChange}
|
||||
/>
|
||||
</FormItem>
|
||||
{/* Top K */}
|
||||
@@ -116,13 +117,12 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
|
||||
label={t('application.top_k')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
extra={t('application.top_k_desc')}
|
||||
initialValue={5}
|
||||
>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
max={20}
|
||||
onChange={(value) => form.setFieldValue('top_k', value)}
|
||||
// onChange={(value) => form.setFieldValue('top_k', value)}
|
||||
/>
|
||||
</FormItem>
|
||||
{/* 语义相似度阈值 similarity_threshold */}
|
||||
|
||||
@@ -98,7 +98,7 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'rerank', pagesize: 100 }}
|
||||
params={{ type: 'rerank', pagesize: 100, is_active: true }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import React from 'react';
|
||||
import { type FC, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Form, Input, Divider } from 'antd';
|
||||
import { Button, Form, Input, Divider, Space } from 'antd';
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
|
||||
interface MappingListProps {
|
||||
label: string;
|
||||
name: string;
|
||||
options: Suggestion[];
|
||||
extra?: ReactNode;
|
||||
valueKey?: string;
|
||||
}
|
||||
const MappingList: React.FC<MappingListProps> = ({ name, options }) => {
|
||||
const MappingList: FC<MappingListProps> = ({ label, name, options, extra, valueKey = 'value' }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
@@ -17,16 +20,19 @@ const MappingList: React.FC<MappingListProps> = ({ name, options }) => {
|
||||
<>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
|
||||
{t('workflow.config.jinja-render.mapping')}
|
||||
{label}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => add()}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addVariable')}
|
||||
</Button>
|
||||
<Space size={8}>
|
||||
{extra}
|
||||
<Button
|
||||
onClick={() => add()}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addVariable')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<div key={key} className="rb:flex rb:items-center rb:gap-1 rb:mb-2">
|
||||
@@ -43,7 +49,7 @@ const MappingList: React.FC<MappingListProps> = ({ name, options }) => {
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
name={[name, valueKey]}
|
||||
noStyle
|
||||
>
|
||||
<VariableSelect
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { type FC, useMemo } from 'react';
|
||||
import { type FC, type ReactNode, useMemo } from 'react';
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Input, Form, Space, Button, Row, Col, Select, type FormListOperation } from 'antd';
|
||||
import Editor from '../Editor'
|
||||
import Editor, { type LexicalEditorProps } from '../Editor'
|
||||
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
||||
|
||||
interface MessageEditor {
|
||||
options: Suggestion[];
|
||||
title?: string;
|
||||
options?: Suggestion[];
|
||||
title?: string | ReactNode;
|
||||
titleVariant?: 'outlined' | 'borderless';
|
||||
isArray?: boolean;
|
||||
parentName?: string | string[];
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
enableJinja2?: boolean;
|
||||
language?: LexicalEditorProps['language'];
|
||||
onChange?: (value?: string) => void;
|
||||
size?: 'small' | 'default'
|
||||
}
|
||||
@@ -29,8 +29,8 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
isArray = true,
|
||||
parentName = 'messages',
|
||||
placeholder,
|
||||
options,
|
||||
enableJinja2 = false,
|
||||
options = [],
|
||||
language,
|
||||
size = 'default'
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
@@ -81,13 +81,15 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
<Space size={8} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5" data-editor-type={parentName === 'template' ? 'template' : undefined}>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<div className={clsx("rb:text-[12px] rb:font-medium rb:py-1 rb:leading-2", {
|
||||
{typeof title === 'string'
|
||||
? <div className={clsx("rb:text-[12px] rb:font-medium rb:py-1 rb:leading-2", {
|
||||
'rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-sm rb:px-2': titleVariant === 'outlined'
|
||||
})}>{title ?? t('workflow.answerDesc')}</div>
|
||||
: title}
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name={parentName} noStyle>
|
||||
<Editor size={size} enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
||||
<Editor size={size} language={language} placeholder={placeholder} options={processedOptions} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
);
|
||||
@@ -132,7 +134,7 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
)}
|
||||
</Row>
|
||||
<Form.Item {...restField} name={[name, 'content']} noStyle>
|
||||
<Editor size={size} enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
||||
<Editor size={size} language={language} placeholder={placeholder} options={processedOptions} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
);
|
||||
|
||||
@@ -68,7 +68,7 @@ const processNodeVariables = (
|
||||
if (p?.name) addVariable(variableList, addedKeys, `${dataNodeId}_${p.name}`, p.name, p.type || 'string', `${dataNodeId}.${p.name}`, nodeData);
|
||||
});
|
||||
break;
|
||||
|
||||
|
||||
case 'var-aggregator':
|
||||
if (config.group.defaultValue) {
|
||||
(config.group_variables.defaultValue || []).forEach((gv: any) => {
|
||||
@@ -106,6 +106,11 @@ const processNodeVariables = (
|
||||
if (cv.name?.trim()) addVariable(variableList, addedKeys, `${dataNodeId}_cycle_${cv.name}`, cv.name, cv.type || 'string', `${dataNodeId}.${cv.name}`, nodeData);
|
||||
});
|
||||
break;
|
||||
case 'code':
|
||||
(config.output_variables.defaultValue || []).forEach((cv: any) => {
|
||||
if (cv.name?.trim()) addVariable(variableList, addedKeys, `${dataNodeId}_cycle_${cv.name}`, cv.name, cv.type || 'string', `${dataNodeId}.${cv.name}`, nodeData);
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -26,9 +26,10 @@ import MemoryConfig from './MemoryConfig'
|
||||
import VariableList from './VariableList'
|
||||
import { useVariableList, getCurrentNodeVariables, getChildNodeVariables } from './hooks/useVariableList'
|
||||
import styles from './properties.module.css'
|
||||
import Editor from "../Editor";
|
||||
import Editor, { type LexicalEditorProps } from "../Editor";
|
||||
import RbSlider from './RbSlider'
|
||||
import JinjaRender from './JinjaRender'
|
||||
import CodeExecution from './CodeExecution'
|
||||
|
||||
interface PropertiesProps {
|
||||
selectedNode?: Node | null;
|
||||
@@ -364,6 +365,11 @@ const Properties: FC<PropertiesProps> = ({
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, 'mapping')}
|
||||
templateOptions={getFilteredVariableList(selectedNode?.data?.type, 'template')}
|
||||
/>
|
||||
: selectedNode?.data?.type === 'code'
|
||||
? <CodeExecution
|
||||
selectedNode={selectedNode}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, 'mapping')}
|
||||
/>
|
||||
: configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => {
|
||||
const config = configs[key] || {}
|
||||
|
||||
@@ -438,7 +444,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
title={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
||||
isArray={!!config.isArray}
|
||||
parentName={key}
|
||||
enableJinja2={config.enableJinja2 as boolean}
|
||||
language={config.language as LexicalEditorProps['language']}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
titleVariant={config.titleVariant}
|
||||
size="small"
|
||||
|
||||
@@ -87,4 +87,7 @@
|
||||
.properties :global(.ant-select .ant-select-arrow) {
|
||||
font-size: 10px;
|
||||
inset-inline-end: 6px;
|
||||
}
|
||||
.properties :global(.ant-input-sm) {
|
||||
padding: 3.6px 7px;
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
model_id: {
|
||||
type: 'customSelect',
|
||||
url: getModelListUrl,
|
||||
params: { type: 'llm,chat' }, // llm/chat
|
||||
params: { type: 'llm,chat', pagesize: 100, is_active: true }, // llm/chat
|
||||
valueKey: 'id',
|
||||
labelKey: 'name',
|
||||
},
|
||||
@@ -166,7 +166,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
model_id: {
|
||||
type: 'customSelect',
|
||||
url: getModelListUrl,
|
||||
params: { type: 'llm,chat' }, // llm/chat
|
||||
params: { type: 'llm,chat', pagesize: 100, is_active: true }, // llm/chat
|
||||
valueKey: 'id',
|
||||
labelKey: 'name',
|
||||
},
|
||||
@@ -259,7 +259,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
model_id: {
|
||||
type: 'customSelect',
|
||||
url: getModelListUrl,
|
||||
params: { type: 'llm,chat' }, // llm/chat
|
||||
params: { type: 'llm,chat', pagesize: 100, is_active: true }, // llm/chat
|
||||
valueKey: 'id',
|
||||
labelKey: 'name',
|
||||
},
|
||||
@@ -284,7 +284,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
config: {
|
||||
input: {
|
||||
type: 'variableList',
|
||||
filterNodeTypes: ['knowledge-retrieval', 'iteration', 'loop'],
|
||||
filterNodeTypes: ['knowledge-retrieval', 'iteration', 'loop', 'parameter-extractor', 'code'],
|
||||
filterVariableNames: ['message']
|
||||
},
|
||||
parallel: {
|
||||
@@ -431,7 +431,32 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
}
|
||||
}
|
||||
},
|
||||
// { type: "code_execution", icon: codeExecutionIcon },
|
||||
{ type: "code", icon: codeExecutionIcon,
|
||||
config: {
|
||||
input_variables: {
|
||||
type: 'inputList',
|
||||
defaultValue: [{ name: 'arg1' }, { name: 'arg2' }]
|
||||
},
|
||||
language: {
|
||||
type: 'select',
|
||||
defaultValue: 'python3'
|
||||
},
|
||||
code: {
|
||||
type: 'messageEditor',
|
||||
isArray: false,
|
||||
language: ['python3', 'javascript'],
|
||||
titleVariant: 'borderless',
|
||||
defaultValue: `def main(arg1: str, arg2: str):
|
||||
return {
|
||||
"result": arg1 + arg2,
|
||||
}`
|
||||
},
|
||||
output_variables: {
|
||||
type: 'outputList',
|
||||
defaultValue: [{name: 'result', type: 'string'}]
|
||||
},
|
||||
}
|
||||
},
|
||||
{ type: "jinja-render", icon: templateRenderingIcon,
|
||||
config: {
|
||||
mapping: {
|
||||
@@ -441,12 +466,12 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
template: {
|
||||
type: 'messageEditor',
|
||||
isArray: false,
|
||||
enableJinja2: true,
|
||||
language: 'jinja2',
|
||||
titleVariant: 'borderless',
|
||||
defaultValue: "{{arg1}}"
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
// {
|
||||
|
||||
@@ -109,6 +109,12 @@ export const useWorkflowGraph = ({
|
||||
: group_variables
|
||||
} else if (type === 'http-request' && (key === 'headers' || key === 'params') && config[key] && typeof config[key] === 'object' && !Array.isArray(config[key]) && nodeLibraryConfig.config && nodeLibraryConfig.config[key]) {
|
||||
nodeLibraryConfig.config[key].defaultValue = Object.entries(config[key]).map(([name, value]) => ({ name, value }))
|
||||
} else if (type === 'code' && key === 'code' && config[key] && nodeLibraryConfig.config && nodeLibraryConfig.config[key]) {
|
||||
try {
|
||||
nodeLibraryConfig.config[key].defaultValue = atob(config[key] as string)
|
||||
} catch {
|
||||
nodeLibraryConfig.config[key].defaultValue = config[key]
|
||||
}
|
||||
} else if (nodeLibraryConfig.config && nodeLibraryConfig.config[key] && config[key]) {
|
||||
nodeLibraryConfig.config[key].defaultValue = config[key]
|
||||
}
|
||||
@@ -588,77 +594,6 @@ export const useWorkflowGraph = ({
|
||||
graphRef.current.resize(containerRef.current.offsetWidth, containerRef.current.offsetHeight);
|
||||
}
|
||||
};
|
||||
|
||||
const nodeChangePosition = ({ node, options }: { node: Node; options: { skipParentHandler?: boolean } }) => {
|
||||
const embedPadding = 50; // Define the embed padding constant
|
||||
if (options.skipParentHandler) {
|
||||
return
|
||||
}
|
||||
|
||||
const children = node.getChildren()
|
||||
if (children && children.length) {
|
||||
node.prop('originPosition', node.getPosition())
|
||||
}
|
||||
|
||||
const parent = node.getParent()
|
||||
if (parent && parent.isNode()) {
|
||||
let originSize = parent.prop('originSize')
|
||||
if (originSize == null) {
|
||||
originSize = parent.getSize()
|
||||
parent.prop('originSize', originSize)
|
||||
}
|
||||
|
||||
let originPosition = parent.prop('originPosition')
|
||||
if (originPosition == null) {
|
||||
originPosition = parent.getPosition()
|
||||
parent.prop('originPosition', originPosition)
|
||||
}
|
||||
|
||||
let x = originPosition.x
|
||||
let y = originPosition.y
|
||||
let cornerX = originPosition.x + originSize.width
|
||||
let cornerY = originPosition.y + originSize.height
|
||||
let hasChange = false
|
||||
|
||||
const children = parent.getChildren()
|
||||
if (children) {
|
||||
children.forEach((child) => {
|
||||
const bbox = child.getBBox().inflate(embedPadding)
|
||||
const corner = bbox.getCorner()
|
||||
|
||||
if (bbox.x < x) {
|
||||
x = bbox.x
|
||||
hasChange = true
|
||||
}
|
||||
|
||||
if (bbox.y < y) {
|
||||
y = bbox.y
|
||||
hasChange = true
|
||||
}
|
||||
|
||||
if (corner.x > cornerX) {
|
||||
cornerX = corner.x
|
||||
hasChange = true
|
||||
}
|
||||
|
||||
if (corner.y > cornerY) {
|
||||
cornerY = corner.y
|
||||
hasChange = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (hasChange) {
|
||||
parent.prop(
|
||||
{
|
||||
position: { x, y },
|
||||
size: { width: cornerX - x, height: cornerY - y },
|
||||
},
|
||||
{ skipParentHandler: true },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
const init = () => {
|
||||
@@ -912,7 +847,13 @@ export const useWorkflowGraph = ({
|
||||
|
||||
if (data.config) {
|
||||
Object.keys(data.config).forEach(key => {
|
||||
if (key === 'memory' && data.config[key] && 'defaultValue' in data.config[key]) {
|
||||
if (data.type === 'code' && key === 'code' && data.config[key] && 'defaultValue' in data.config[key]) {
|
||||
const code = data.config[key].defaultValue || ''
|
||||
itemConfig = {
|
||||
...itemConfig,
|
||||
code: btoa(code || '')
|
||||
}
|
||||
} else if (key === 'memory' && data.config[key] && 'defaultValue' in data.config[key]) {
|
||||
const { messages, ...rest } = data.config[key].defaultValue
|
||||
let memoryMessage = { role: 'USER', content: data.config[key].defaultValue.messages }
|
||||
itemConfig = {
|
||||
|
||||
Reference in New Issue
Block a user