This commit is contained in:
WenC 2024-04-24 18:03:44 +08:00
parent 5099637414
commit 4058877acd
4 changed files with 396 additions and 229 deletions

View File

@ -1,10 +1,12 @@
import {useContext, useEffect, useRef, useState} from "react";
import {GlobalContext} from "../../context/GlobalContext.ts";
import {put, WebSocketBaseUrl} from "../../util/AjaxUtils.ts";
import {Button, Col, Divider, Empty, Row, Space, Spin, Table, TableProps} from "antd";
import {CheckOutlined, EditOutlined, ReloadOutlined} from "@ant-design/icons";
import {get, post, put, WebSocketBaseUrl} from "../../util/AjaxUtils.ts";
import {Col, Divider, Row, Spin} from "antd";
import useMessage from "antd/es/message/useMessage";
import TextArea from "antd/es/input/TextArea";
import AiHelperText from "./text/AiHelperText.tsx";
import AiHelperMod from "./mod/AiHelperMod.tsx";
import {IProjMod} from "../../interfaces/proj/IProj.ts";
import {MAX_MOD_SIZE} from "../../route/proj/edit/ProjConfigModList.tsx";
type PropsType = {
projId: string;
@ -14,6 +16,7 @@ type PropsType = {
}
type ProjModType = {
id: string,
name: string,
desc: string
}
@ -25,20 +28,17 @@ export default function AiHelper(props: PropsType) {
const [messageApi, messageApiHolder] = useMessage();
const [projIntroduction, setProjIntroduction] = useState<string>(props.projIntroduction ? props.projIntroduction : '');
const [newProjIntroduction, setNewProjIntroduction] = useState<string>('');
const [isProjIntroductionEdit, setIsProjIntroductionEdit] = useState<boolean>(false);
const [isNewProjIntroductionEdit, setIsNewProjIntroductionEdit] = useState<boolean>(false);
const [isProjIntroductionLoading, setIsProjIntroductionLoading] = useState(false);
const [projDesc, setProjDesc] = useState<string>(props.projDesc ? props.projDesc : '');
const [newProjDesc, setNewProjDesc] = useState<string>('');
const [isProjDescEdit, setIsProjDescEdit] = useState<boolean>(false);
const [isNewProjDescEdit, setIsNewProjDescEdit] = useState<boolean>(false);
const [isProjDescLoading, setIsProjDescLoading] = useState(false);
const [projModArray, setProjModArray] = useState<ProjModType[]>(props.projMods ? props.projMods : []);
const [projModArray, setProjModArray] = useState<IProjMod[]>([]);
const [newProjModArray, setNewProjModArray] = useState<ProjModType[]>([]);
const [isProjModArrayLoading, setIsProjModArrayLoading] = useState(false);
const ping = () => {
clearTimeout(pingTimeout.current);
pingTimeout.current = setTimeout(() => {
@ -74,6 +74,8 @@ export default function AiHelper(props: PropsType) {
setIsProjModArrayLoading(false);
const projMods = JSON.parse(data.content) as ProjModType[];
setNewProjModArray(projMods);
} else if (data.type == 'REFRESH_PROJ_MOD_FIELDS') {
listMods();
}
}
ws.current.onerror = (event) => {
@ -85,20 +87,21 @@ export default function AiHelper(props: PropsType) {
}
}
const projModColumnArray: TableProps<ProjModType>['columns'] = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 60,
align: 'center',
render: (_value, _record, index) => {
return index + 1
const listMods = () => {
get<IProjMod[]>({
messageApi,
url: `/api/proj-mod/list/proj-id/${props.projId}`,
onBefore() {
setIsProjModArrayLoading(true);
},
onSuccess({data}) {
setProjModArray(data);
},
onFinally() {
setIsProjModArrayLoading(false);
}
},
{title: '模块名称', dataIndex: 'name', key: 'name', width: 200, align: 'center'},
{title: '模块描述', dataIndex: 'desc', key: 'desc', align: 'center'},
];
})
}
const generateProjIntroduction = () => {
ws.current?.send(JSON.stringify({
@ -174,239 +177,106 @@ export default function AiHelper(props: PropsType) {
}
})
}
/**
*
*/
const updateProjModArray = () => {
put<any>({
messageApi,
url: `/api/proj-mod/update-mods/${props.projId}`,
body: {
list: newProjModArray
},
onBefore() {
setIsProjModArrayLoading(true);
},
onSuccess() {
messageApi.success('保存成功').then();
setProjModArray(newProjModArray);
setNewProjModArray([]);
},
onFinally() {
setIsProjModArrayLoading(false)
}
})
}
useEffect(() => {
if (!props.projId) {
return;
}
listMods();
websocket();
}, [globalContext.user.userId, props.projId]);
/**
*
*/
const renderNewProjIntroductionBtn = () => {
if(!newProjIntroduction) {
if(!isProjIntroductionEdit) {
return (
<Space>
<Button type="link" style={{cursor: 'pointer'}} onClick={generateProjIntroduction}>AI生成</Button>
{
projIntroduction ? (
<Button type="link" style={{cursor: 'pointer'}} onClick={() => {
setIsProjIntroductionEdit(true);
}}><EditOutlined /> </Button>
) : <></>
}
</Space>
)
}
return (
<Space>
<Button type="link" style={{cursor: 'pointer'}} onClick={() => {
setIsProjIntroductionEdit(false);
updateProjIntroduction(projIntroduction);
}}><CheckOutlined/> </Button>
</Space>
)
}
if(!isNewProjIntroductionEdit) {
return (
<Space>
<Button type="link" style={{cursor: 'pointer'}} onClick={() => {
updateProjIntroduction(newProjIntroduction);
}}><CheckOutlined/> </Button>
<Button type="link" style={{cursor: 'pointer'}} onClick={generateProjIntroduction}><ReloadOutlined/> </Button>
<Button type="link" style={{cursor: 'pointer'}} onClick={() => {
setIsNewProjIntroductionEdit(true);
}}><EditOutlined /> </Button>
</Space>
)
}
return (
<Space>
<Button type="link" style={{cursor: 'pointer'}} onClick={() => {
setIsNewProjIntroductionEdit(false);
}}><CheckOutlined/> </Button>
</Space>
)
}
/**
*
*/
const renderNewProjDescBtn = () => {
if(!newProjDesc) {
if(!isProjDescEdit) {
return (
<Space>
<Button type="link" style={{cursor: 'pointer'}} onClick={generateProjDesc}>AI生成</Button>
{
projIntroduction ? (
<Button type="link" style={{cursor: 'pointer'}} onClick={() => {
setIsProjDescEdit(true);
}}><EditOutlined /> </Button>
) : <></>
}
</Space>
)
}
return (
<Space>
<Button type="link" style={{cursor: 'pointer'}} onClick={() => {
setIsProjDescEdit(false);
updateProjDesc(projDesc);
}}><CheckOutlined/> </Button>
</Space>
)
}
if(!isNewProjDescEdit) {
return (
<Space>
<Button type="link" style={{cursor: 'pointer'}} onClick={() => {
updateProjDesc(newProjDesc);
}}><CheckOutlined/> </Button>
<Button type="link" style={{cursor: 'pointer'}} onClick={generateProjDesc}><ReloadOutlined/> </Button>
<Button type="link" style={{cursor: 'pointer'}} onClick={() => {
setIsNewProjDescEdit(true);
}}><EditOutlined /> </Button>
</Space>
)
}
return (
<Space>
<Button type="link" style={{cursor: 'pointer'}} onClick={() => {
setIsNewProjDescEdit(false);
}}><CheckOutlined/> </Button>
</Space>
)
}
return (
<>
{messageApiHolder}
<Row>
<Col span={24}>
<div style={{padding: '5px 0 0 0', fontWeight: 'bold'}}></div>
<Spin tip="正在处理,请稍后..." size="small" spinning={isProjIntroductionLoading}>
<div style={{padding: '5px 0 0 0'}}>
{newProjIntroduction ? <Divider orientation="right" plain></Divider> : <></>}
{projIntroduction ? (
isProjIntroductionEdit ? <TextArea rows={10} value={projIntroduction} placeholder="请编辑简介" maxLength={1500} onChange={(e) => {
setProjIntroduction(e.currentTarget.value);
}}/>
: <div>{projIntroduction}</div>
) : <Empty description="暂无内容"/>}
{
newProjIntroduction ? (
<>
<Divider orientation="right" plain></Divider>
{
isNewProjIntroductionEdit ? (
<TextArea rows={10} value={newProjIntroduction} placeholder="请编辑简介" maxLength={1500} onChange={(e) => {
setNewProjIntroduction(e.currentTarget.value);
}}/>
) : <div>{newProjIntroduction}</div>
}
</>
) : <></>
}
</div>
<div style={{padding: '5px 0 0 0', textAlign: 'center'}}>
{renderNewProjIntroductionBtn()}
</div>
<AiHelperText title="项目简介"
text={projIntroduction}
newText={newProjIntroduction}
maxLength={1500}
handleGenerate={() => {
generateProjIntroduction();
}}
handleSave={(text) => {
updateProjIntroduction(text);
}}
/>
</Spin>
</Col>
<Col span={24}>
<Divider dashed/>
<div style={{padding: '5px 0 0 0', fontWeight: 'bold'}}></div>
<Spin tip="正在处理,请稍后..." size="small" spinning={isProjDescLoading}>
<div style={{padding: '5px 0 0 0'}}>
{newProjDesc ? <Divider orientation="right" plain></Divider> : <></>}
{projDesc ? (
isProjDescEdit ?
<TextArea rows={10} value={projDesc} placeholder="请编辑详情"
maxLength={1500} onChange={(e) => {
setProjDesc(e.currentTarget.value);
}}/>
: <div>{projDesc}</div>
) : <Empty description="暂无内容"/>}
{
newProjDesc ? (
<>
<Divider orientation="right" plain></Divider>
{
isNewProjDescEdit ? (
<TextArea rows={10} value={newProjDesc} placeholder="请编辑简介"
maxLength={1500} onChange={(e) => {
setNewProjDesc(e.currentTarget.value);
}}/>
) : <div>{newProjDesc}</div>
}
</>
) : <></>
}
</div>
<div style={{padding: '5px 0 0 0', textAlign: 'center'}}>
{renderNewProjDescBtn()}
</div>
<AiHelperText title="项目详情"
text={projDesc}
newText={newProjDesc}
maxLength={1500}
handleGenerate={() => {
generateProjDesc();
}}
handleSave={(text) => {
updateProjDesc(text);
}}
/>
</Spin>
</Col>
<Col span={24}>
<Divider dashed/>
<div style={{padding: '5px 0 0 0', fontWeight: 'bold'}}></div>
<Spin tip="正在处理,请稍后..." size="small" spinning={isProjModArrayLoading}>
<div style={{padding: '5px 0 0 0'}}>
{newProjModArray.length > 0 ? <Divider orientation="right" plain></Divider> : <></>}
<Table columns={projModColumnArray} dataSource={projModArray} size="small" bordered={true}
scroll={{y: 240}} pagination={{pageSize: 20}}/>
{
newProjModArray.length > 0 ? (
<>
<Divider orientation="right" plain></Divider>
<Table columns={projModColumnArray} dataSource={newProjModArray} size="small"
bordered={true} scroll={{y: 240}} pagination={{pageSize: 20}}/>
</>
) : <></>
}
</div>
<div style={{padding: '5px 0 0 0', textAlign: 'center'}}>
{
newProjModArray.length > 0 ? (
<Space>
<Button type="link" style={{cursor: 'pointer'}}
onClick={updateProjModArray}><CheckOutlined/> </Button>
<Button type="link" style={{cursor: 'pointer'}}
onClick={generateProjModArray}><ReloadOutlined/> </Button>
</Space>
) : <Button type="link" style={{cursor: 'pointer'}}
onClick={generateProjModArray}>AI生成</Button>
}
</div>
<AiHelperMod mods={projModArray}
newMods={newProjModArray}
handleGenerate={() => {
generateProjModArray();
}}
handleSave={(index, mod) => {
if(projModArray.length > MAX_MOD_SIZE) {
messageApi.error(`模块最大数量为${MAX_MOD_SIZE}`);
return;
}
post<any>({
messageApi,
url: `/api/proj-mod/save-ai/${props.projId}`,
body: {
name: mod.name,
desc: mod.desc,
},
onBefore() {
setIsProjModArrayLoading(true);
},
onSuccess() {
messageApi.success('提交成功')
listMods();
newProjModArray.splice(index, 1);
setNewProjModArray([
...newProjModArray
]);
},
onFinally() {
setIsProjModArrayLoading(false);
}
})
}}
handleResaveField={(_index, projModId) => {
post<any>({
messageApi,
url: `/api/proj-mod/resave-ai-field/${props.projId}/${projModId}`,
onBefore() {
setIsProjModArrayLoading(true);
},
onSuccess() {
messageApi.success('提交成功')
listMods();
},
onFinally() {
setIsProjModArrayLoading(false);
}
})
}}
/>
</Spin>
</Col>
</Row>
</>
)

View File

@ -0,0 +1,155 @@
import {Button, Divider, Dropdown, Space, Table, TableProps} from "antd";
import {CheckOutlined, LoadingOutlined, ReloadOutlined, RedoOutlined} from "@ant-design/icons";
import {useEffect, useState} from "react";
import {IProjMod} from "../../../interfaces/proj/IProj.ts";
type PropsType = {
mods: IProjMod[];
newMods: ProjModType[],
handleGenerate: () => void;
handleSave: (index: number, mod: ProjModType) => void;
handleResaveField: (index: number, projModId: string) => void;
}
type ProjModType = {
name: string,
desc: string,
}
const savedItems = [
{
key: 'edit',
label: '编辑',
},{
key: 'remove',
label: '删除',
},
]
export default function AiHelperMod(props: PropsType) {
const [modArray, setModArray] = useState<IProjMod[]>([]);
const [newModArray, setNewModArray] = useState<ProjModType[]>([]);
useEffect(() => {
setModArray(props.mods);
setNewModArray(props.newMods);
}, [props.mods, props.newMods]);
const modColumnArray: TableProps<IProjMod>['columns'] = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 60,
align: 'center',
render: (_value, _record, index) => {
return index + 1
}
},
{title: '模块名称', dataIndex: 'modName', key: 'name', width: 200, align: 'center'},
{title: '模块描述', dataIndex: 'modDesc', key: 'desc', align: 'center'},
{
title: 'AI状态',
dataIndex: 'aiFieldStatus',
key: 'desc',
width: 100,
align: 'center',
render: (value) => {
if(value == 'GENERATING') {
return '处理中'
}
if(value == 'FIELD') {
return '失败'
}
if(value == 'SUCCESS') {
return '成功'
}
}
},
{
title: '操作',
dataIndex: 'option',
key: 'option',
width: 80,
align: 'center',
render: (_value, record, index) => {
if(record.aiFieldStatus == 'GENERATING') {
return <LoadingOutlined />
}
if(record.aiFieldStatus == 'FAILED') {
return <Button onClick={() => {
props.handleResaveField(index, record.projModId);
}}><RedoOutlined /></Button>
}
return (
<Dropdown menu={{ items: savedItems }} placement="bottomCenter" arrow>
<Button></Button>
</Dropdown>
)
}
},
];
const newModColumnArray: TableProps<ProjModType>['columns'] = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 60,
align: 'center',
render: (_value, _record, index) => {
return index + 1
}
},
{title: '模块名称', dataIndex: 'name', key: 'name', width: 200, align: 'center'},
{title: '模块描述', dataIndex: 'desc', key: 'desc', align: 'center'},
{
title: '确认',
dataIndex: 'option',
key: 'option',
width: 80,
align: 'center',
render: (_value, record, index) => {
return (
<Button onClick={() => {
props.handleSave(index, record);
}}>
<CheckOutlined />
</Button>
)
}
},
];
return (
<>
<div style={{padding: '5px 0 0 0'}}>
{newModArray.length > 0 ? <Divider orientation="right" plain></Divider> : <></>}
<Table columns={modColumnArray} dataSource={modArray} size="small" bordered={true} scroll={{y: 240}} pagination={false}/>
{
newModArray.length > 0 ? (
<>
<Divider orientation="right" plain></Divider>
<Table columns={newModColumnArray} dataSource={newModArray} size="small" bordered={true} scroll={{y: 240}} pagination={false}/>
</>
) : <></>
}
</div>
<div style={{padding: '5px 0 0 0', textAlign: 'center'}}>
{
newModArray.length > 0 ? (
<Space>
<Button type="link" style={{cursor: 'pointer'}}
onClick={() => {
props.handleGenerate();
}}><ReloadOutlined/> </Button>
</Space>
) : <Button type="link" style={{cursor: 'pointer'}}
onClick={() => {
props.handleGenerate();
}}>AI生成</Button>
}
</div>
</>
)
}

View File

@ -0,0 +1,129 @@
import {Button, Divider, Empty, Space} from "antd";
import TextArea from "antd/es/input/TextArea";
import {useEffect, useState} from "react";
import {CheckOutlined, EditOutlined, ReloadOutlined} from "@ant-design/icons";
type PropsType = {
title: string;
maxLength: number;
text: string;
newText: string;
handleGenerate: () => void;
handleSave: (newText: string) => void;
}
export default function AiHelperText(props: PropsType) {
const [text, setText] = useState('');
const [newText, setNewText] = useState('');
const [isTextEdit, setIsTextEdit] = useState(false);
const [isNewTextEdit, setIsNewTextEdit] = useState(false);
useEffect(() => {
setText(props.text);
setNewText(props.newText);
}, [props.text, props.newText])
const renderTextBtn = () => {
if (!newText) {
if (!isTextEdit) {
return (
<Space>
<Button type="link" style={{cursor: 'pointer'}} onClick={() => {
props.handleGenerate();
}}>AI生成</Button>
{
text ? (
<Button type="link" style={{cursor: 'pointer'}} onClick={() => {
setIsTextEdit(true);
}}><EditOutlined/> </Button>
) : <></>
}
</Space>
)
}
return (
<Space>
<Button type="link" style={{cursor: 'pointer'}} onClick={() => {
props.handleSave(text);
setIsTextEdit(false);
}}><CheckOutlined/> </Button>
</Space>
)
}
if (!isNewTextEdit) {
return (
<Space>
<Button type="link" style={{cursor: 'pointer'}} onClick={() => {
props.handleSave(newText);
setIsNewTextEdit(false);
}}><CheckOutlined/> </Button>
<Button type="link" style={{cursor: 'pointer'}} onClick={() => {
props.handleGenerate();
}}><ReloadOutlined/> </Button>
<Button type="link" style={{cursor: 'pointer'}} onClick={() => {
setIsNewTextEdit(true);
}}><EditOutlined/> </Button>
</Space>
)
}
return (
<Space>
<Button type="link" style={{cursor: 'pointer'}} onClick={() => {
setIsNewTextEdit(false);
}}><CheckOutlined/> </Button>
</Space>
)
}
const renderTextDom = () => {
if(!text) {
return <Empty description="暂无内容"/>
}
if(!isTextEdit) {
return <div>{text}</div>
}
return (
<TextArea autoSize={true} value={text} placeholder={`请编辑${props.title}`} maxLength={props.maxLength} onChange={(e) => {
setText(e.currentTarget.value);
}}/>
)
}
const renderNewTextDom = () => {
if(!newText) {
return <></>
}
if(!isNewTextEdit) {
return (
<>
<Divider orientation="right" plain>{props.title}</Divider>
<div>{newText}</div>
</>
)
}
return (
<>
<Divider orientation="right" plain>{props.title}</Divider>
<TextArea autoSize={true} value={newText} placeholder={`请编辑${props.title}`} maxLength={props.maxLength} onChange={(e) => {
setNewText(e.currentTarget.value);
}}/>
</>
)
}
return (
<>
<div style={{padding: '5px 0 0 0', fontWeight: 'bold'}}>{props.title}</div>
<div style={{padding: '5px 0 0 0'}}>
{newText ? <Divider orientation="right" plain>{props.title}</Divider> : <></>}
{renderTextDom()}
{renderNewTextDom()}
</div>
<div style={{padding: '5px 0 0 0', textAlign: 'center'}}>
{renderTextBtn()}
</div>
</>
)
}

View File

@ -59,6 +59,19 @@ export interface IProjPay {
chargeAdditionals: string;
}
/**
*
*/
export interface IProjMod {
projModId: string;
projId: string;
projContext: string;
modName: string;
modDesc: string;
modIcon: string;
modContext: string;
aiFieldStatus: string;
}
export interface IProj {