2024-10-14 00:46:54 +08:00

391 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useShallow } from 'zustand/react/shallow';
import { useAiStore } from './store/ai-store';
import { CloseOutlined, HistoryOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Dropdown, Form, Input, message, Modal, Tooltip } from 'antd';
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { TextArea } from '../container/components/TextArea';
import clsx from 'clsx';
import { query, useNewNavigate } from '@/modules';
import { nanoid } from 'nanoid';
import { ChatMessage } from './module/ChatMessage';
import { isObjectNull } from '@/utils/is-null';
import { useLocation } from 'react-router';
const testId = '60aca66b-4be9-4266-9568-6001032c7e13';
const NormalMessage = ({ onSend }: { onSend: any }) => {
const [message, setMessage] = useState('');
const onClick = () => {
onSend(message);
setMessage('');
};
return (
<div>
<TextArea value={message} onChange={(v) => setMessage(v)} className='scrollbar' style={{ minHeight: 180 }} placeholder='message' />
<div className='px-4'>
<Button type='primary' className='mt-4' onClick={onClick}>
</Button>
</div>
</div>
);
};
type FormModalProps = {
send?: any;
};
const FormModal = (props: FormModalProps) => {
const [form] = Form.useForm();
const aiStore = useAiStore(
useShallow((state) => {
return {
showEdit: state.showEdit,
setShowEdit: state.setShowEdit,
formData: state.formData,
setFormData: state.setFormData,
setMessage: state.setMessage,
};
}),
);
useEffect(() => {
const open = aiStore.showEdit;
if (open) {
const isNull = isObjectNull(aiStore.formData);
if (isNull) {
form.setFieldsValue({});
} else form.setFieldsValue(aiStore.formData);
}
}, [aiStore.showEdit]);
const onFinish = async (values: any) => {
props.send({
type: 'changeSession',
data: {
id: testId,
},
});
aiStore.setMessage([]);
};
const onClose = () => {
aiStore.setShowEdit(false);
aiStore.setFormData({});
form.resetFields();
};
const isEdit = aiStore.formData.id;
return (
<Modal
title={isEdit ? 'Edit' : 'Add'}
open={aiStore.showEdit}
onClose={() => aiStore.setShowEdit(false)}
destroyOnClose
footer={false}
width={800}
onCancel={onClose}>
<Form
form={form}
onFinish={onFinish}
labelCol={{
span: 4,
}}
wrapperCol={{
span: 20,
}}>
<Form.Item name='id' hidden>
<Input />
</Form.Item>
<Form.Item name='sessionId' label='sessionId'>
<Input />
</Form.Item>
<Form.Item label=' ' colon={false}>
<Button type='primary' htmlType='submit'>
Submit
</Button>
<Button className='ml-2' htmlType='reset' onClick={onClose}>
Cancel
</Button>
</Form.Item>
</Form>
</Modal>
);
};
export const useListenQuery = () => {
const aiStore = useAiStore(
useShallow((state) => {
return {
open: state.open,
key: state.key,
onMessage: state.onMessage,
};
}),
);
// TODO: Implement useEffect
useEffect(() => {
if (!aiStore.open) {
return;
}
const open = aiStore.open;
send({
type: 'subscribe',
token: query.getToken(),
data: {
key: aiStore.key,
},
});
const close = query.qws.onMessage(onMessage, {
selector: (data) => {
const requestId = data.requestId;
return {
requestId,
...data.data,
};
},
});
return () => {
send({ type: 'unsubscribe' });
close();
};
}, [aiStore.open, query]);
const send = async (data: any) => {
query.qws.send(data, {
wrapper: (data) => ({
requestId: nanoid(16),
type: 'chat',
data: data,
}),
});
};
const onMessage = (data: any) => {
type MessageData = {
chatId: string;
chatPromptId: string;
role: string;
root: boolean;
show: boolean;
data: {
message: string;
};
};
if (data.code === 200 && data.type === 'messages') {
const messageData = data.data as MessageData;
aiStore.onMessage(messageData);
}
};
return {
send,
};
};
export const AiMoudle = () => {
const [form] = Form.useForm();
const navigate = useNewNavigate();
const aiStore = useAiStore(
useShallow((state) => {
return {
open: state.open,
setOpen: state.setOpen,
formData: state.formData,
setFormData: state.setFormData,
messages: state.messages,
setMessages: state.setMessage,
key: state.key,
root: state.root,
setRoot: state.setRoot,
title: state.title,
setShowEdit: state.setShowEdit,
list: state.list,
getList: state.getList,
};
}),
);
const [noPrompt, setNoPrompt] = useState(false);
const [currentPage, setCurrentPage] = useState('');
const location = useLocation();
useEffect(() => {
if (!aiStore.open) {
return;
}
const isNull = JSON.stringify(aiStore.formData) === '{}';
if (!isNull) {
form.setFieldsValue(aiStore.formData);
} else {
form.setFieldsValue({ inputs: [] });
}
}, [aiStore.open, aiStore.formData]);
useEffect(() => {
if (!aiStore.open) {
aiStore.setMessages([]);
} else {
aiStore.getList();
}
}, [aiStore.open]);
const { send } = useListenQuery();
useLayoutEffect(() => {
if (aiStore.open) {
aiStore.setOpen(false);
}
}, [location.pathname]);
const onSend = () => {
const data = form.getFieldsValue();
send({ type: 'messages', data: { ...data, root: true } });
};
const onSendNoPrompt = (value) => {
send({ type: 'messages', data: { message: value, root: true } });
};
const inputs = useMemo(() => {
if (!aiStore.open) return [];
const inputs = aiStore.formData?.inputs || [];
return inputs;
}, [aiStore.open, aiStore.formData]);
const isNotShow = inputs?.length === 0 || !inputs;
const OnlyNormalMessage = (
<Tooltip title='不需要任何预设prompt'>
<div
className='text-blue-500 text-xs mt-2 italic cursor-pointer'
onClick={() => {
setNoPrompt(true);
}}>
</div>
</Tooltip>
);
const NeedPromptMessage = (
<Tooltip title='需要预设prompt'>
<div
className='text-blue-500 text-xs mt-2 italic cursor-pointer'
onClick={() => {
setNoPrompt(false);
}}>
prompt
</div>
</Tooltip>
);
const RootForm = (
<div className='flex flex-col relative'>
<div className={clsx('flex flex-col absolute top-3 w-full', isNotShow && 'invisible', noPrompt && 'invisible')}>
<div className='mt-1 ml-2'>? {OnlyNormalMessage}</div>
<div className='mt-4'>
<Form form={form}>
<Form.List name={'inputs'} initialValue={[]}>
{(fields, { add, remove }) => {
return (
<>
{fields.map((field, index) => {
const key = form.getFieldValue(['inputs', index, 'key']);
const isTitle = key === 'title';
return (
<div key={field.name + '-' + index} className=''>
<Form.Item name={[field.name, 'key']} rules={[{ required: true, message: 'Missing name' }]} hidden noStyle>
<Input placeholder='name' />
</Form.Item>
<Form.Item className='!mb-0' name={[field.name, 'value']} rules={[{ required: true, message: 'Missing value' }]}>
<TextArea className='scrollbar' style={{ minHeight: isTitle ? 18 : 180 }} placeholder={key} />
</Form.Item>
</div>
);
})}
</>
);
}}
</Form.List>
</Form>
<div className='px-4'>
<Button type='primary' className='mt-4' onClick={onSend}>
</Button>
</div>
</div>
</div>
{isNotShow && !noPrompt && (
<div className='flex flex-col absolute top-3'>
<div className='mt-2 text-gray-400 flex '>
<div
className='text-blue-500 cursor-pointer'
onClick={(e) => {
navigate('/chat/chat-prompt/list');
}}>
</div>
</div>
<div>{OnlyNormalMessage}</div>
</div>
)}
{noPrompt && (
<div className=''>
{NeedPromptMessage}
<NormalMessage onSend={onSendNoPrompt} />
</div>
)}
</div>
);
const ChatForm = (
<ChatMessage
messages={aiStore.messages}
onSend={(value) => {
send({ type: 'messages', data: { message: value } });
}}
/>
);
const items = aiStore.list.map((item) => {
return {
key: item.id,
label: item.title || item.id,
onClick: () => {
// aiStore.setKey(item.id);
// 选中具体的
send({
type: 'changeSession',
data: {
id: item.id,
},
});
aiStore.setMessages([]);
},
};
});
return (
<div className={clsx('w-full h-full flex-shrink-0 bg-gray-100 border-l-2 shadow-lg flex flex-col', !aiStore?.open && 'hidden')}>
<div className='flex justify-between p-2 border bg-white'>
<div className='flex gap-4'>
<Button className='position ml-4 ' onClick={() => aiStore.setOpen(false)} icon={<CloseOutlined />}></Button>
<h1 className=''>{aiStore.title || 'Ai Module'}</h1>
</div>
<div className='mr-4 flex'>
{!aiStore.root && (
<Button
className='mr-2'
onClick={() => {
aiStore.setMessages([]);
aiStore.setRoot(true);
send({
type: 'changeSession',
data: {
id: null,
},
});
}}
icon={<PlusOutlined />}></Button>
)}
<Dropdown
className='mr-2'
placement='bottomRight'
menu={{
items,
}}>
<Button className='' icon={<HistoryOutlined />}></Button>
</Dropdown>
{/* <Tooltip title='历史会话'>
<Button
className=''
onClick={() => {
aiStore.setShowEdit(true);
}}
icon={<HistoryOutlined />}></Button>
</Tooltip> */}
</div>
</div>
<div className='flex-grow p-2 overflow-hidden h-full flex flex-col'>{aiStore.root ? RootForm : ChatForm}</div>
<FormModal send={send} />
</div>
);
};