Merge branch 'develop' into feature/ontology_zy

This commit is contained in:
yingzhao
2026-01-30 14:26:27 +08:00
committed by GitHub
340 changed files with 13482 additions and 11967 deletions

View File

@@ -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 || '',

View File

@@ -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}

View 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;

View File

@@ -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}

View File

@@ -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,

View File

@@ -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);
}

View File

@@ -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}

View 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

View File

@@ -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} />}
</>
);
};

View File

@@ -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;
}

View File

@@ -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',

View File

@@ -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>

View File

@@ -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 => (

View File

@@ -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": []
}
}
}
]
}
}

View File

@@ -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)
})
}

View File

@@ -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;

View 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

View 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

View 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 })})&gt;</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

View File

@@ -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;

View 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;

View 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;

View 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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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[]>
}

View 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;

View 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;

View 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;

View File

@@ -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}
/>
</>
)
}

View File

@@ -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;
}

View 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
}

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;
}

View File

@@ -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
},
// 迭代周期
{

View File

@@ -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}

View File

@@ -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}

View File

@@ -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">

View File

@@ -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)'}}>

View 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;
}

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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>
)}

View File

@@ -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>
}

View File

@@ -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"

View File

@@ -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 */}

View File

@@ -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}

View File

@@ -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

View File

@@ -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>
);

View File

@@ -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;
}
};

View File

@@ -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"

View File

@@ -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;
}

View File

@@ -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}}"
},
}
}
},
]
},
// {

View File

@@ -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 = {