feat: 暂存添加ai chat and prompt generate
This commit is contained in:
38
src/pages/ai-chat/AiModule.tsx
Normal file
38
src/pages/ai-chat/AiModule.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useAiStore } from './store/ai-store';
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import { Button } from 'antd';
|
||||
|
||||
export const AiMoudle = () => {
|
||||
const aiStore = useAiStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
open: state.open,
|
||||
setOpen: state.setOpen,
|
||||
runAi: state.runAi,
|
||||
};
|
||||
}),
|
||||
);
|
||||
if (!aiStore.open) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className='w-96 flex-shrink-0 bg-gray-100 border-l-2 shadow-lg flex flex-col'>
|
||||
<div className='flex gap-4 bg-slate-400 p-2'>
|
||||
<Button className='position ml-4 !bg-slate-400 !border-black' onClick={() => aiStore.setOpen(false)} icon={<CloseOutlined />}></Button>
|
||||
<h1 className='ml-10'>Ai Moudle</h1>
|
||||
</div>
|
||||
<div className='flex-grow p-2'>
|
||||
<div> chat message</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
aiStore.runAi();
|
||||
}}>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
7
src/pages/ai-chat/index.tsx
Normal file
7
src/pages/ai-chat/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { AiMoudle } from './AiModule';
|
||||
import { useAiStore } from './store/ai-store';
|
||||
export { AiMoudle, useAiStore };
|
||||
|
||||
export const App = () => {
|
||||
return <div>AI Chat</div>;
|
||||
};
|
||||
58
src/pages/ai-chat/store/ai-store.ts
Normal file
58
src/pages/ai-chat/store/ai-store.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { query } from '@/modules';
|
||||
import { message } from 'antd';
|
||||
import { create } from 'zustand';
|
||||
|
||||
export type AiStore = {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
type?: string;
|
||||
key: string;
|
||||
setKey: (key: string) => void;
|
||||
setType?: (type: string) => void;
|
||||
sendMsg: (msg: string) => void;
|
||||
formData: any;
|
||||
setFormData: (data: any) => void;
|
||||
runAi: () => any;
|
||||
title: string;
|
||||
setTitle: (title: string) => void;
|
||||
};
|
||||
|
||||
export const useAiStore = create<AiStore>((set, get) => {
|
||||
return {
|
||||
open: false,
|
||||
setOpen: (open) => set({ open }),
|
||||
key: '',
|
||||
setKey: (key) => set({ key }),
|
||||
sendMsg: (msg) => {
|
||||
console.log(msg);
|
||||
},
|
||||
formData: {},
|
||||
setFormData: (data) => set({ formData: data }),
|
||||
runAi: async () => {
|
||||
const { formData } = get();
|
||||
const res = await query.post({
|
||||
path: 'ai',
|
||||
key: 'run',
|
||||
data: {
|
||||
key: formData.key,
|
||||
inputs: [
|
||||
{
|
||||
key: 'title',
|
||||
value: '根据描述生成代码',
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
value: '我想获取一个card, 包含标题和内容,标题是evision,内容是这是一个测试',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
if (res.code === 200) {
|
||||
console.log(res.data);
|
||||
message.success('Success');
|
||||
}
|
||||
},
|
||||
title: '',
|
||||
setTitle: (title) => set({ title }),
|
||||
};
|
||||
});
|
||||
@@ -11,6 +11,8 @@ type Props = {
|
||||
style?: React.CSSProperties;
|
||||
language?: string;
|
||||
listen?: boolean;
|
||||
placeholder?: string;
|
||||
onBlur?: () => void;
|
||||
};
|
||||
export const TextArea = (props: Props) => {
|
||||
const [code, setCode] = useState<string>('');
|
||||
@@ -27,12 +29,13 @@ export const TextArea = (props: Props) => {
|
||||
<div className={clsx('min-h-16 max-h-52 overflow-scroll scrollbar p-1 ', props.className)}>
|
||||
<CodeEditor
|
||||
value={code}
|
||||
language='js'
|
||||
language={props.language || 'js'}
|
||||
className='border rounded-sm '
|
||||
readOnly={props.readonly}
|
||||
placeholder='Please enter JS code.'
|
||||
placeholder={props.placeholder || 'Please enter JS code.'}
|
||||
onChange={(evn) => _onChange(evn.target.value)}
|
||||
padding={10}
|
||||
onBlur={props.onBlur}
|
||||
style={{
|
||||
// backgroundColor: '#f5f5f5',
|
||||
fontFamily: 'ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace',
|
||||
|
||||
@@ -59,12 +59,7 @@ const FormModal = () => {
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name='code' label='code'>
|
||||
<TextArea
|
||||
value={containerStore.formData.code}
|
||||
style={{
|
||||
height: '200px',
|
||||
}}
|
||||
/>
|
||||
<TextArea />
|
||||
</Form.Item>
|
||||
<Form.Item label=' ' colon={false}>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
|
||||
41
src/pages/prompt/D3.tsx
Normal file
41
src/pages/prompt/D3.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { query } from '@/modules';
|
||||
import { useEffect } from 'react';
|
||||
import { drawGraph } from './graph/d3';
|
||||
export const D3Grahp = () => {
|
||||
const get = async () => {
|
||||
const res = await query.post({
|
||||
path: 'prompt',
|
||||
key: 'list',
|
||||
});
|
||||
console.log(res);
|
||||
const grahpData = {
|
||||
nodes: res.data.map((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
label: item.title,
|
||||
type: 'prompt',
|
||||
};
|
||||
}),
|
||||
links: [],
|
||||
};
|
||||
drawGraph(grahpData);
|
||||
};
|
||||
const getD3 = async () => {
|
||||
const res = await query.post({
|
||||
path: 'prompt',
|
||||
key: 'getD3',
|
||||
});
|
||||
drawGraph(res.data);
|
||||
};
|
||||
useEffect(() => {
|
||||
getD3();
|
||||
return () => {
|
||||
document.querySelector('.ai-graph')!.innerHTML = '';
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div className='w-full h-full'>
|
||||
<svg className='ai-graph border shadow-sm p-2 mx-auto mt-10 ' width='960' height='600' ></svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
57
src/pages/prompt/edit/Edit.tsx
Normal file
57
src/pages/prompt/edit/Edit.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Button, Form, Input } from 'antd';
|
||||
import { TextArea } from '../../container/components/TextArea';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export const Edit = () => {
|
||||
const [form] = Form.useForm();
|
||||
const onFinish = (values: any) => {
|
||||
console.log('Success:', values);
|
||||
};
|
||||
const onSave = () => {
|
||||
//
|
||||
};
|
||||
const isEdit = form.getFieldValue('id');
|
||||
return (
|
||||
<div className='w-full h-full felx flex-col bg-gray-200'>
|
||||
<h1 className='text-center py-4'>Prompt JS Code Generate</h1>
|
||||
<div className='py-2 px-4 w-3/4 min-w-[600px] mx-auto border shadow rounded bg-white'>
|
||||
<Form form={form} onFinish={onFinish} className='mt-4' labelCol={{ span: 4 }}>
|
||||
<Form.Item name='id' hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name='title' label='Title'>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name='description' label='Description'>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item name='code' label='Code'>
|
||||
<TextArea className='max-h-full' style={{ minHeight: 300 }} />
|
||||
</Form.Item>
|
||||
<Form.Item label=' ' colon={false}>
|
||||
<div className='flex gap-2'>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
Generate
|
||||
</Button>
|
||||
<Button htmlType='reset'>Reset</Button>
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={() => {
|
||||
//
|
||||
}}>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
className={clsx(isEdit ? 'block' : 'hidden')}
|
||||
onClick={() => {
|
||||
//
|
||||
}}>
|
||||
Preview
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
342
src/pages/prompt/edit/List.tsx
Normal file
342
src/pages/prompt/edit/List.tsx
Normal file
@@ -0,0 +1,342 @@
|
||||
import { Button, Input, message, Modal, Table } from 'antd';
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { usePromptStore } from '../store/prompt';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { Form } from 'antd';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { EditOutlined, SettingOutlined, LinkOutlined, SaveOutlined, DeleteOutlined, LeftOutlined, CaretRightOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import clsx from 'clsx';
|
||||
import { TextArea } from '@/pages/container/components/TextArea';
|
||||
|
||||
import { marked } from 'marked';
|
||||
import { extractKeysFromBraces } from '@/utils/extra';
|
||||
import { useAiStore } from '@/pages/ai-chat';
|
||||
|
||||
const FormModal = () => {
|
||||
const [form] = Form.useForm();
|
||||
const promptStore = usePromptStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
showEdit: state.showEdit,
|
||||
setShowEdit: state.setShowEdit,
|
||||
formData: state.formData,
|
||||
updateData: state.updateData,
|
||||
runAi: state.runAi,
|
||||
};
|
||||
}),
|
||||
);
|
||||
useEffect(() => {
|
||||
const open = promptStore.showEdit;
|
||||
if (open) {
|
||||
const isNull = JSON.stringify(promptStore.formData) === '{}';
|
||||
if (isNull) {
|
||||
form.resetFields();
|
||||
} else {
|
||||
form.setFieldsValue(promptStore.formData || {});
|
||||
}
|
||||
}
|
||||
}, [promptStore.showEdit]);
|
||||
const onFinish = async (values: any) => {
|
||||
let other = {};
|
||||
if (!values.id) {
|
||||
other = {
|
||||
presetData: {
|
||||
validator: {},
|
||||
data: {
|
||||
prompt: '',
|
||||
inputs: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
promptStore.updateData({
|
||||
...values,
|
||||
...other,
|
||||
});
|
||||
};
|
||||
const onClose = () => {
|
||||
promptStore.setShowEdit(false);
|
||||
form.resetFields();
|
||||
};
|
||||
const isEdit = promptStore.formData.id;
|
||||
return (
|
||||
<Modal
|
||||
title={isEdit ? 'Edit' : 'Add'}
|
||||
open={promptStore.showEdit}
|
||||
onClose={() => promptStore.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='title' label='title'>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name='description' label='description'>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item name='key' label='key'>
|
||||
<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 List = () => {
|
||||
const navicate = useNavigate();
|
||||
const aiStore = useAiStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
open: state.open,
|
||||
setOpen: state.setOpen,
|
||||
key: state.key,
|
||||
setKey: state.setKey,
|
||||
sendMsg: state.sendMsg,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const promptStore = usePromptStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
setFormData: state.setFormData,
|
||||
setShowEdit: state.setShowEdit,
|
||||
list: state.list,
|
||||
deleteData: state.deleteData,
|
||||
getList: state.getList,
|
||||
loading: state.loading,
|
||||
updateData: state.updateData,
|
||||
formData: state.formData,
|
||||
runAi: state.runAi,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const [codeEdit, setCodeEdit] = useState(false);
|
||||
const [code, setCode] = useState('');
|
||||
const [form] = Form.useForm<{ inputs: any[] }>();
|
||||
useEffect(() => {
|
||||
promptStore.getList();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (!codeEdit) {
|
||||
form.setFieldsValue({ inputs: [] });
|
||||
}
|
||||
}, [codeEdit]);
|
||||
const onAdd = () => {
|
||||
promptStore.setFormData({});
|
||||
promptStore.setShowEdit(true);
|
||||
setCodeEdit(false);
|
||||
};
|
||||
const getFormInputs = () => {
|
||||
if (!codeEdit) return;
|
||||
console.log('blur getFormInputs');
|
||||
|
||||
const keys = extractKeysFromBraces(code);
|
||||
const inputs = form.getFieldValue('inputs') || [];
|
||||
const newInputs = keys
|
||||
.map((key) => {
|
||||
const has = inputs.some((item: any) => item.key === key);
|
||||
if (!has) {
|
||||
return {
|
||||
key,
|
||||
value: '',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (newInputs.length > 0) {
|
||||
form.setFieldsValue({ inputs: [...inputs, ...newInputs] });
|
||||
}
|
||||
};
|
||||
const len = form.getFieldValue('inputs')?.length || 0;
|
||||
return (
|
||||
<div className='w-full h-full flex flex-col'>
|
||||
<div className='flex flex-grow overflow-hidden h-full'>
|
||||
<Button onClick={onAdd} type='primary' className='m-4 w-64 flex-shrink-0' icon={<PlusOutlined />}></Button>
|
||||
<div className='flex-grow overflow-auto scrollbar bg-gray-100'>
|
||||
<div className='flex flex-wrap gap-x-10 gap-y-4 rounded pt-10 justify-center'>
|
||||
{promptStore.list.length > 0 &&
|
||||
promptStore.list.map((item) => {
|
||||
const { presetData } = item;
|
||||
const md = presetData?.data?.prompt || '';
|
||||
const inputs = presetData?.data?.inputs || [];
|
||||
const html = marked.parse(md);
|
||||
return (
|
||||
<Fragment key={item.id}>
|
||||
<div className='flex text-sm gap flex-col w-[600px] max-h-[400px] bg-white p-4 rounded-lg' key={item.id} onClick={() => {}}>
|
||||
<div
|
||||
className='px-4 cursor-pointer'
|
||||
onClick={() => {
|
||||
setCode(md);
|
||||
promptStore.setFormData(item);
|
||||
form.setFieldsValue({
|
||||
inputs: inputs.map((item) => {
|
||||
return { key: item.key, value: item.value };
|
||||
}),
|
||||
});
|
||||
setCodeEdit(true);
|
||||
}}>
|
||||
<div
|
||||
className='font-bold flex'
|
||||
onClick={(e) => {
|
||||
// copy(item.code);
|
||||
// e.stopPropagation();
|
||||
// message.success('copy code success');
|
||||
}}>
|
||||
{item.title || '-'}
|
||||
<div
|
||||
className=' ml-3 text-xs text-gray-400'
|
||||
style={{
|
||||
fontFamily: 'D-DIN',
|
||||
}}>
|
||||
{item?.key ? item.key : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className='font-light mt-2'>{item.description ? item.description : '-'}</div>
|
||||
</div>
|
||||
{/* <div className='w-full text-xs'>
|
||||
<TextArea className='max-h-[240px] scrollbar' value={item.code} readonly />
|
||||
</div> */}
|
||||
<div className='px-4 mt-2'>{md ? 'Prompt' : ''}</div>
|
||||
<div className='px-4'>
|
||||
<div className='max-h-52 overflow-scroll scrollbar p-4 border shadow-sm mt-1'>
|
||||
<div dangerouslySetInnerHTML={{ __html: html }}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex mt-4 ml-4'>
|
||||
<Button.Group>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
promptStore.setFormData(item);
|
||||
promptStore.setShowEdit(true);
|
||||
setCodeEdit(false);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
icon={<EditOutlined />}></Button>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
promptStore.deleteData(item.id);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
icon={<DeleteOutlined />}></Button>
|
||||
<Button
|
||||
icon={<CaretRightOutlined />}
|
||||
onClick={() => {
|
||||
// navicate(`/prompt/${item.id}`);
|
||||
promptStore.setFormData(item);
|
||||
// promptStore.runAi();
|
||||
aiStore.setOpen(true);
|
||||
}}
|
||||
/>
|
||||
</Button.Group>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
{new Array(4).fill(0).map((_, index) => {
|
||||
return <div key={index} className='w-[600px]'></div>;
|
||||
})}
|
||||
{promptStore.list.length == 0 && (
|
||||
<div className='text-center' key={'no-data'}>
|
||||
No Data
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx('bg-gray-100 border-l flex flex-col border-bg-slate-300 w-[600px] flex-shrink-0', !codeEdit && 'hidden')}>
|
||||
<div className='bg-white p-2'>
|
||||
<div className='mt-2 ml-2 flex gap-2'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setCodeEdit(false);
|
||||
promptStore.setFormData({});
|
||||
}}
|
||||
icon={<LeftOutlined />}></Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
// console.log('save', promptStore.formData);
|
||||
const { presetData } = promptStore.formData;
|
||||
const inputs = form.getFieldValue('inputs') || [];
|
||||
promptStore.updateData({ ...promptStore.formData, presetData: { ...presetData, data: { ...presetData.data, prompt: code, inputs } } });
|
||||
}}
|
||||
icon={<SaveOutlined />}></Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-grow p-2 rounded-2 shadow-sm overflow-hidden'>
|
||||
<TextArea
|
||||
value={code}
|
||||
language='markdown'
|
||||
placeholder='Please enter markdown code.'
|
||||
onChange={(value) => {
|
||||
setCode(value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
console.log('blur');
|
||||
setTimeout(() => {
|
||||
getFormInputs();
|
||||
}, 400);
|
||||
}}
|
||||
className='h-full max-h-full scrollbar'
|
||||
style={{
|
||||
overflow: 'auto',
|
||||
minHeight: '200px',
|
||||
// height: '100%', // height: '100%' 有bug
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex-shrink-0 px-4 mt-4'>
|
||||
<h1 className={clsx('mb-2', len === 0 && 'hidden')}>Match Keys</h1>
|
||||
<Form form={form}>
|
||||
<Form.List name='inputs'>
|
||||
{(fields, { add, remove }) => {
|
||||
return (
|
||||
<>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<div key={field.name + '-' + index} className='flex gap-2'>
|
||||
<Form.Item name={[field.name, 'key']} rules={[{ required: true, message: 'Missing name' }]}>
|
||||
<Input placeholder='name' />
|
||||
</Form.Item>
|
||||
<Form.Item name={[field.name, 'value']} rules={[{ required: true, message: 'Missing value' }]}>
|
||||
<Input placeholder='value' />
|
||||
</Form.Item>
|
||||
{/* <Button onClick={() => add()} className='flex items-center'>
|
||||
+
|
||||
</Button> */}
|
||||
<Button onClick={() => remove(field.name)}>-</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FormModal />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
18
src/pages/prompt/graph/d3.css
Normal file
18
src/pages/prompt/graph/d3.css
Normal file
@@ -0,0 +1,18 @@
|
||||
.node circle {
|
||||
fill: #69b3a2;
|
||||
stroke: #333;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.link {
|
||||
fill: none;
|
||||
stroke: #999;
|
||||
stroke-opacity: 0.6;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
text {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
122
src/pages/prompt/graph/d3.ts
Normal file
122
src/pages/prompt/graph/d3.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
// @ts-nocheck
|
||||
import * as d3 from 'd3';
|
||||
import './d3.css';
|
||||
|
||||
export const drawGraph = (graphData) => {
|
||||
// 初始配置:宽度和高度通过容器自适应
|
||||
const svg = d3.select('.ai-graph');
|
||||
|
||||
const margin = { top: 20, right: 20, bottom: 20, left: 20 };
|
||||
|
||||
const updateChartSize = () => {
|
||||
const width = svg.node().clientWidth - margin.left - margin.right;
|
||||
const height = svg.node().clientHeight - margin.top - margin.bottom;
|
||||
|
||||
svg.attr('viewBox', `0 0 ${width} ${height}`);
|
||||
|
||||
return { width, height };
|
||||
};
|
||||
|
||||
let { width, height } = updateChartSize();
|
||||
|
||||
// 使用力导向布局
|
||||
const simulation = d3
|
||||
.forceSimulation(graphData.nodes)
|
||||
.force(
|
||||
'link',
|
||||
d3
|
||||
.forceLink(graphData.links)
|
||||
.id((d) => d.id)
|
||||
.distance(100)
|
||||
)
|
||||
.force('charge', d3.forceManyBody().strength(-300))
|
||||
.force('center', d3.forceCenter(width / 2, height / 2))
|
||||
.force('collide', d3.forceCollide(20)); // 防止节点重叠,半径设置为20
|
||||
|
||||
// 绘制连线
|
||||
const link = svg.append('g')
|
||||
.selectAll('line')
|
||||
.data(graphData.links)
|
||||
.enter()
|
||||
.append('line')
|
||||
.attr('class', 'link');
|
||||
|
||||
// 绘制节点
|
||||
const node = svg.append('g')
|
||||
.selectAll('g')
|
||||
.data(graphData.nodes)
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', 'node');
|
||||
|
||||
node.append('circle').attr('r', 10);
|
||||
|
||||
// 添加节点标签
|
||||
node
|
||||
.append('text')
|
||||
.attr('dx', 12)
|
||||
.attr('dy', '.35em')
|
||||
.text((d) => d.label);
|
||||
|
||||
// 限制节点在SVG范围内
|
||||
const clampPosition = (d, width, height) => {
|
||||
d.x = Math.max(10, Math.min(width - 10, d.x)); // 10为节点半径
|
||||
d.y = Math.max(10, Math.min(height - 10, d.y));
|
||||
};
|
||||
|
||||
// 更新节点和连线位置
|
||||
simulation.on('tick', () => {
|
||||
link
|
||||
.attr('x1', (d) => d.source.x)
|
||||
.attr('y1', (d) => d.source.y)
|
||||
.attr('x2', (d) => d.target.x)
|
||||
.attr('y2', (d) => d.target.y);
|
||||
|
||||
node.attr('transform', (d) => {
|
||||
// 限制节点在SVG内部
|
||||
clampPosition(d, width, height);
|
||||
return `translate(${d.x},${d.y})`;
|
||||
});
|
||||
});
|
||||
|
||||
// 添加拖拽事件
|
||||
node.call(
|
||||
d3
|
||||
.drag()
|
||||
.on('start', (event, d) => {
|
||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
||||
d.fx = d.x;
|
||||
d.fy = d.y;
|
||||
})
|
||||
.on('drag', (event, d) => {
|
||||
d.fx = event.x;
|
||||
d.fy = event.y;
|
||||
})
|
||||
.on('end', (event, d) => {
|
||||
if (!event.active) simulation.alphaTarget(0);
|
||||
d.fx = null;
|
||||
d.fy = null;
|
||||
})
|
||||
);
|
||||
|
||||
// 添加双击事件
|
||||
node.on('dblclick', (event, d) => {
|
||||
d.fx = null;
|
||||
d.fy = null;
|
||||
console.log('dblclick', d);
|
||||
});
|
||||
|
||||
// 监听窗口大小变化时更新图表
|
||||
const resize = () => {
|
||||
const { width, height } = updateChartSize();
|
||||
simulation.force('center', d3.forceCenter(width / 2, height / 2));
|
||||
simulation.alpha(1).restart(); // 重启仿真以更新布局
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
// 在需要的时候手动调用,销毁图表时可以移除监听
|
||||
return () => {
|
||||
window.removeEventListener('resize', resize);
|
||||
};
|
||||
};
|
||||
@@ -1,57 +1,18 @@
|
||||
import { Button, Form, Input } from 'antd';
|
||||
import { TextArea } from '../container/components/TextArea';
|
||||
import clsx from 'clsx';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { Edit } from './edit/Edit';
|
||||
import { List } from './edit/List';
|
||||
import { D3Grahp } from './D3';
|
||||
import { Main } from './layout/Main';
|
||||
|
||||
export const App = () => {
|
||||
const [form] = Form.useForm();
|
||||
const onFinish = (values: any) => {
|
||||
console.log('Success:', values);
|
||||
};
|
||||
const onSave = () => {
|
||||
//
|
||||
};
|
||||
const isEdit = form.getFieldValue('id');
|
||||
return (
|
||||
<div className='w-full h-full felx flex-col bg-gray-200'>
|
||||
<h1 className='text-center py-4'>Prompt JS Code Generate</h1>
|
||||
<div className='py-2 px-4 w-3/4 min-w-[600px] mx-auto border shadow rounded bg-white'>
|
||||
<Form form={form} onFinish={onFinish} className='mt-4' labelCol={{ span: 4 }}>
|
||||
<Form.Item name='id' hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name='title' label='Title'>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name='description' label='Description'>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item name='code' label='Code'>
|
||||
<TextArea className='max-h-full' style={{ minHeight: 300 }} />
|
||||
</Form.Item>
|
||||
<Form.Item label=' ' colon={false}>
|
||||
<div className='flex gap-2'>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
Generate
|
||||
</Button>
|
||||
<Button htmlType='reset'>Reset</Button>
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={() => {
|
||||
//
|
||||
}}>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
className={clsx(isEdit ? 'block' : 'hidden')}
|
||||
onClick={() => {
|
||||
//
|
||||
}}>
|
||||
Preview
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
<Routes>
|
||||
<Route element={<Main />}>
|
||||
<Route path='/' element={<Navigate to='/prompt/list' />} />
|
||||
<Route path='/graph' element={<D3Grahp />} />
|
||||
<Route path='/edit' element={<Edit />} />
|
||||
<Route path='/list' element={<List />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
13
src/pages/prompt/layout/Main.tsx
Normal file
13
src/pages/prompt/layout/Main.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { AiMoudle } from '@/pages/ai-chat';
|
||||
import { Outlet } from 'react-router';
|
||||
|
||||
export const Main = () => {
|
||||
return (
|
||||
<div className='w-full h-full flex'>
|
||||
<div className='flex-grow h-full'>
|
||||
<Outlet />
|
||||
</div>
|
||||
<AiMoudle />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
97
src/pages/prompt/store/prompt.ts
Normal file
97
src/pages/prompt/store/prompt.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { create } from 'zustand';
|
||||
import { query } from '@/modules';
|
||||
import { message } from 'antd';
|
||||
type PromptStore = {
|
||||
showEdit: boolean;
|
||||
setShowEdit: (showEdit: boolean) => void;
|
||||
formData: any;
|
||||
setFormData: (formData: any) => void;
|
||||
loading: boolean;
|
||||
setLoading: (loading: boolean) => void;
|
||||
list: any[];
|
||||
getList: () => Promise<void>;
|
||||
updateData: (data: any) => Promise<void>;
|
||||
deleteData: (id: string) => Promise<void>;
|
||||
runAi: () => any;
|
||||
};
|
||||
export const usePromptStore = create<PromptStore>((set, get) => {
|
||||
return {
|
||||
showEdit: false,
|
||||
setShowEdit: (showEdit) => set({ showEdit }),
|
||||
formData: {},
|
||||
setFormData: (formData) => set({ formData }),
|
||||
loading: false,
|
||||
setLoading: (loading) => set({ loading }),
|
||||
list: [],
|
||||
getList: async () => {
|
||||
set({ loading: true });
|
||||
const res = await query.post({
|
||||
path: 'prompt',
|
||||
key: 'list',
|
||||
});
|
||||
set({ loading: false });
|
||||
if (res.code === 200) {
|
||||
set({ list: res.data });
|
||||
} else {
|
||||
message.error(res.msg || 'Request failed');
|
||||
}
|
||||
},
|
||||
updateData: async (data) => {
|
||||
const { getList } = get();
|
||||
const res = await query.post({
|
||||
path: 'prompt',
|
||||
key: 'update',
|
||||
data,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
message.success('Success');
|
||||
set({ showEdit: false, formData: [] });
|
||||
getList();
|
||||
} else {
|
||||
message.error(res.msg || 'Request failed');
|
||||
}
|
||||
},
|
||||
deleteData: async (id) => {
|
||||
const { getList } = get();
|
||||
const res = await query.post({
|
||||
path: 'prompt',
|
||||
key: 'delete',
|
||||
id,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
getList();
|
||||
message.success('Success');
|
||||
} else {
|
||||
message.error(res.msg || 'Request failed');
|
||||
}
|
||||
},
|
||||
runAi: async () => {
|
||||
const { formData } = get();
|
||||
const res = await query.post({
|
||||
path: 'ai',
|
||||
key: 'run',
|
||||
data: {
|
||||
key: formData.key,
|
||||
inputs: [
|
||||
{
|
||||
key: 'title',
|
||||
value: '根据描述生成代码',
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
value: '我想获取一个card, 包含标题和内容,标题是evision,内容是这是一个测试',
|
||||
},
|
||||
],
|
||||
data: {
|
||||
title: formData.title,
|
||||
description: formData.description,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (res.code === 200) {
|
||||
console.log(res.data);
|
||||
message.success('Success');
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
11
src/utils/extra.ts
Normal file
11
src/utils/extra.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export function extractKeysFromBraces(text: string) {
|
||||
const regex = /\{\{\s*(.*?)\s*\}\}/g;
|
||||
const keys: string[] = [];
|
||||
let matches: RegExpExecArray | null;
|
||||
|
||||
while ((matches = regex.exec(text)) !== null) {
|
||||
keys.push(matches[1]); // 获取{{}}中间的key
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
Reference in New Issue
Block a user