feat: Add base project structure with API and web components

This commit is contained in:
Ke Sun
2025-12-02 20:28:01 +08:00
parent f3de6d6cc9
commit c1adc62ec6
817 changed files with 111226 additions and 106 deletions

View File

@@ -0,0 +1,61 @@
import { type FC } from 'react'
import { useTranslation } from 'react-i18next';
import { Progress } from 'antd'
import type { Data } from '../types'
import Tag from '@/components/Tag'
const bgList = [
'linear-gradient( 180deg, #F1F6FE 0%, #FBFDFF 100%)',
'linear-gradient( 180deg, #F1F9FE 0%, #FBFDFF 100%)',
'linear-gradient( 180deg, #FEFBF7 0%, #FBFDFF 100%)',
'linear-gradient( 180deg, #F1F9FE 0%, #FBFDFF 100%)',
]
interface CardListProps {
data: Data[]
handleViewDetail: (id: string | number) => void
}
const CardList: FC<CardListProps> = ({
data,
handleViewDetail
}) => {
const { t } = useTranslation();
return (
<div className="rb:grid rb:grid-cols-3 rb:gap-[16px]">
{data.map((item, index) => {
return (
<div
key={item.id}
className="rb:p-[20px] rb:rounded-[12px] rb:border-[1px] rb:border-[#DFE4ED] rb:cursor-pointer"
style={{
background: bgList[index % bgList.length],
}}
onClick={() => handleViewDetail(item.id)}
>
<div className="rb:flex rb:items-center">
<div className="rb:w-[48px] rb:h-[48px] rb:text-center rb:font-semibold rb:text-[28px] rb:leading-[48px] rb:rounded-[8px] rb:text-[#FBFDFF] rb:bg-[#155EEF]">{item.username[0]}</div>
<div className="rb:text-base rb:font-medium rb:leading-[24px] rb:ml-[12px]">
{item.username}<br/>
<Tag color={item.role === 'administrator' ? 'processing' : 'error'}>{item.role}</Tag>
</div>
</div>
<div className="rb:grid rb:grid-cols-3 rb:gap-[12px] rb:mt-[28px] rb:mb-[28px]">
{['knowledgeEntryCount', 'interactionCount', 'averageTimeConsumption'].map(key => (
<div key={key} className="rb:text-center">
<div className="rb:text-[24px] rb:leading-[30px] rb:font-extrabold">{item[key] || 0}</div>
<div className="rb:break-words">{t(`memory.${key}`)}</div>
</div>
))}
</div>
<div className="rb:flex rb:items-center rb:justify-between rb:w-full rb:text-[#5B6167] rb:text-[12px]">{t('memory.dataCompletionDegree')}<span>{item.dataCompletionDegree || 0}%</span></div>
<Progress percent={item.dataCompletionDegree || 0} showInfo={false} size={{height: 8}} />
</div>
)
})}
</div>
)
}
export default CardList

View File

@@ -0,0 +1,127 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, App } from 'antd';
import { useTranslation } from 'react-i18next';
import type { ConfigModalData, ConfigModalRef } from '../types'
import { getWorkspaceModels, updateWorkspaceModels } from '@/api/workspaces'
import { getModelListUrl } from '@/api/models'
import CustomSelect from '@/components/CustomSelect'
import RbModal from '@/components/RbModal'
const ConfigModal = forwardRef<ConfigModalRef>((_props, ref) => {
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm<ConfigModalData>();
const [loading, setLoading] = useState(false)
const values = Form.useWatch([], form);
// 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => {
setVisible(false);
form.resetFields();
setLoading(false)
};
const handleOpen = () => {
getWorkspaceModels().then((res) => {
const { llm, embedding, rerank } = res as ConfigModalData
form.setFieldsValue({
llm,
embedding,
rerank
})
})
setVisible(true);
};
// 封装保存方法,添加提交逻辑
const handleSave = () => {
form
.validateFields()
.then(() => {
setLoading(true)
// updateWorkspaceModels(values as ConfigModalData)
// .then(() => {
// setLoading(false)
// handleClose()
// message.success(t('common.createSuccess'))
// })
// .catch(() => {
// setLoading(false)
// });
handleClose()
})
.catch((err) => {
console.log('err', err)
});
}
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
return (
<RbModal
title={t(`userMemory.editConfig`)}
open={visible}
onCancel={handleClose}
okText={t('common.save')}
onOk={handleSave}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
>
<Form.Item
label={t('space.llmModel')}
name="llm"
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'llm', pagesize: 100 }}
valueKey="id"
labelKey="name"
hasAll={false}
style={{width: '100%'}}
/>
</Form.Item>
<Form.Item
label={t('space.embeddingModel')}
name="embedding"
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'embedding', pagesize: 100 }}
valueKey="id"
labelKey="name"
hasAll={false}
style={{width: '100%'}}
/>
</Form.Item>
<Form.Item
label={t('space.rerankModel')}
name="rerank"
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'rerank', pagesize: 100 }}
valueKey="id"
labelKey="name"
hasAll={false}
style={{width: '100%'}}
/>
</Form.Item>
</Form>
</RbModal>
);
});
export default ConfigModal;

View File

View File

@@ -0,0 +1,172 @@
import { useEffect, useState, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'
import { Row, Col, Radio, Button, List, Skeleton, Space } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import type { RadioChangeEvent } from 'antd';
import { AppstoreOutlined, MenuOutlined } from '@ant-design/icons';
import Empty from '@/components/Empty'
import type { Data, ConfigModalRef } from './types'
import totalNum from '@/assets/images/memory/totalNum.svg'
import onlineNum from '@/assets/images/memory/onlineNum.svg'
import Table from '@/components/Table'
import { getTotalEndUsers, userMemoryListUrl, getUserMemoryList } from '@/api/memory';
import ConfigModal from './components/ConfigModal';
const bgList = [
'linear-gradient( 180deg, #F1F6FE 0%, #FBFDFF 100%)',
'linear-gradient( 180deg, #F1F9FE 0%, #FBFDFF 100%)',
'linear-gradient( 180deg, #FEFBF7 0%, #FBFDFF 100%)',
'linear-gradient( 180deg, #F1F9FE 0%, #FBFDFF 100%)',
]
const countList = [
'total_num', 'online_num',
]
const IconList: Record<string, string> = {
total_num: totalNum,
online_num: onlineNum,
}
export default function UserMemory() {
const { t } = useTranslation();
const navigate = useNavigate()
const configModalRef = useRef<ConfigModalRef>(null)
const [loading, setLoading] = useState<boolean>(false);
const [data, setData] = useState<Data[]>([]);
const [countData, setCountData] = useState<Record<string, number>>({});
const [layout, setLayout] = useState<'card' | 'list'>('card');
// 获取数据
useEffect(() => {
getCountData()
getData()
}, []);
// 用户记忆统计
const getCountData = () => {
getTotalEndUsers().then((res) => {
setCountData(res as Record<string, number> || {})
})
}
const getData = () => {
setLoading(true)
getUserMemoryList().then((res) => {
setData(res as Data[] || [])
})
.finally(() => {
setLoading(false)
})
}
const handleViewDetail = (id: string | number) => {
navigate(`/user-memory/${id}`)
}
const handleChangeLayout = (e: RadioChangeEvent) => {
const type = e.target.value
setLayout(type)
}
// 表格列配置
const columns: ColumnsType = [
{
title: t('userMemory.user'),
dataIndex: 'end_user',
key: 'end_user',
render: (value) => value?.other_name && value?.other_name !== '' ? value?.other_name : value?.id || '-'
},
{
title: t('userMemory.knowledgeEntryCount'),
dataIndex: 'memory_num',
key: 'memory_num',
render: (value) => value?.total || 0
},
{
title: t('common.operation'),
key: 'action',
render: (_, record) => (
<Button
type="link"
onClick={() => handleViewDetail(record.end_user?.id)}
>
{t('common.viewDetail')}
</Button>
),
},
];
return (
<div>
<Row gutter={16} className="rb:mb-[16px]">
{countList.map(key => (
<Col key={key} span={6}>
<div className="rb:bg-[#FBFDFF] rb:border-[1px] rb:border-[#DFE4ED] rb:rounded-[12px] rb:p-[18px_20px_20px_20px]">
<div className="rb:text-[28px] rb:font-extrabold rb:leading-[35px] rb:flex rb:items-center rb:justify-between rb:mb-[12px]">
{countData[key] || 0}{key === 'avgInteractionTime' ? 's' : ''}
<img className="rb:w-[24px] rb:h-[24px]" src={IconList[key]} />
</div>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[16px]">{t(`userMemory.${key}`)}</div>
</div>
</Col>
))}
<Col span={12} className="rb:text-right">
<Space>
<Button type="primary" onClick={() => configModalRef?.current?.handleOpen()}>{t('userMemory.chooseModel')}</Button>
<Radio.Group value={layout} onChange={handleChangeLayout}>
<Radio.Button value="card" disabled={layout === 'card'}><AppstoreOutlined /></Radio.Button>
<Radio.Button value="list" disabled={layout === 'list'}><MenuOutlined /></Radio.Button>
</Radio.Group>
</Space>
</Col>
</Row>
{layout === 'card' &&
<>
{loading ?
<Skeleton active />
: data.length > 0 ? (
<List
grid={{ gutter: 16, column: 4 }}
dataSource={data}
renderItem={(item, index) => {
const { end_user, memory_num } = item as Data;
const name = end_user?.other_name && end_user?.other_name !== '' ? end_user?.other_name : end_user?.id
return (
<List.Item key={index}>
<div
className="rb:p-[20px] rb:rounded-[12px] rb:border-[1px] rb:border-[#DFE4ED] rb:cursor-pointer"
style={{
background: bgList[index % bgList.length],
}}
onClick={() => handleViewDetail(end_user.id)}
>
<div className="rb:flex rb:items-center">
<div className="rb:w-[48px] rb:h-[48px] rb:text-center rb:font-semibold rb:text-[28px] rb:leading-[48px] rb:rounded-[8px] rb:text-[#FBFDFF] rb:bg-[#155EEF]">{name[0]}</div>
<div className="rb:max-w-[calc(100%-60px)] rb:text-base rb:font-medium rb:leading-[24px] rb:ml-[12px] rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">
{name || '-'}<br/>
</div>
</div>
<div className="rb:grid rb:grid-cols-1 rb:gap-[12px] rb:mt-[28px] rb:mb-[28px]">
<div className="rb:text-center">
<div className="rb:text-[24px] rb:leading-[30px] rb:font-extrabold">{memory_num.total || 0}</div>
<div className="rb:break-words">{t(`userMemory.knowledgeEntryCount`)}</div>
</div>
</div>
</div>
</List.Item>
)
}}
/>
) : <Empty />}
</>
}
{layout === 'list' &&
<Table
apiUrl={userMemoryListUrl}
columns={columns}
rowKey="end_user.id"
pagination={false}
/>
}
<ConfigModal ref={configModalRef} />
</div>
);
}

View File

@@ -0,0 +1,28 @@
export interface Data {
end_user: {
id: string;
app_id: string;
other_id: string;
other_name: string;
other_address: string;
created_at: string;
updated_at: string;
},
memory_num: {
total: number;
counts: {
dialogue: number;
chunk: number;
statement: number;
entity: number;
}
}
}
export interface ConfigModalData {
llm: string;
embedding: string;
rerank: string;
}
export interface ConfigModalRef {
handleOpen: () => void;
}