2025-12-22 19:53:31 +08:00
|
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
|
|
import { Button, Modal, Form, Input, Select, message, Row, Col, Empty, Tabs, Card, Tag, Space, Divider, Typography, InputNumber } from 'antd';
|
|
|
|
|
|
import { ThunderboltOutlined, PlusOutlined, EditOutlined, DeleteOutlined, TrophyOutlined } from '@ant-design/icons';
|
|
|
|
|
|
import { useParams } from 'react-router-dom';
|
2025-12-23 09:28:24 +08:00
|
|
|
|
import api from '../services/api';
|
2025-12-22 19:53:31 +08:00
|
|
|
|
import SSEProgressModal from '../components/SSEProgressModal';
|
|
|
|
|
|
|
|
|
|
|
|
const { TextArea } = Input;
|
|
|
|
|
|
const { Title, Text, Paragraph } = Typography;
|
|
|
|
|
|
|
|
|
|
|
|
interface CareerStage {
|
|
|
|
|
|
level: number;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
description?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface Career {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
project_id: string;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
type: 'main' | 'sub';
|
|
|
|
|
|
description?: string;
|
|
|
|
|
|
category?: string;
|
|
|
|
|
|
stages: CareerStage[];
|
|
|
|
|
|
max_stage: number;
|
|
|
|
|
|
requirements?: string;
|
|
|
|
|
|
special_abilities?: string;
|
|
|
|
|
|
worldview_rules?: string;
|
|
|
|
|
|
source: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function Careers() {
|
|
|
|
|
|
const { projectId } = useParams<{ projectId: string }>();
|
|
|
|
|
|
const [mainCareers, setMainCareers] = useState<Career[]>([]);
|
|
|
|
|
|
const [subCareers, setSubCareers] = useState<Career[]>([]);
|
|
|
|
|
|
const [, setLoading] = useState(true);
|
|
|
|
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
|
|
|
|
const [isAIModalOpen, setIsAIModalOpen] = useState(false);
|
|
|
|
|
|
const [editingCareer, setEditingCareer] = useState<Career | null>(null);
|
|
|
|
|
|
const [form] = Form.useForm();
|
|
|
|
|
|
const [aiForm] = Form.useForm();
|
|
|
|
|
|
|
|
|
|
|
|
// AI生成状态
|
|
|
|
|
|
const [aiGenerating, setAiGenerating] = useState(false);
|
|
|
|
|
|
const [aiProgress, setAiProgress] = useState(0);
|
|
|
|
|
|
const [aiMessage, setAiMessage] = useState('');
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (projectId) {
|
|
|
|
|
|
fetchCareers();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [projectId]);
|
|
|
|
|
|
|
|
|
|
|
|
const fetchCareers = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setLoading(true);
|
2025-12-23 09:28:24 +08:00
|
|
|
|
const response: any = await api.get('/careers', {
|
|
|
|
|
|
params: { project_id: projectId }
|
2025-12-22 19:53:31 +08:00
|
|
|
|
});
|
2025-12-23 09:28:24 +08:00
|
|
|
|
setMainCareers(response.main_careers || []);
|
|
|
|
|
|
setSubCareers(response.sub_careers || []);
|
2025-12-22 19:53:31 +08:00
|
|
|
|
} catch (error: any) {
|
2025-12-23 09:28:24 +08:00
|
|
|
|
console.error('获取职业列表失败:', error);
|
2025-12-22 19:53:31 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleOpenModal = (career?: Career) => {
|
|
|
|
|
|
if (career) {
|
|
|
|
|
|
setEditingCareer(career);
|
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
|
...career,
|
|
|
|
|
|
stages: career.stages.map(s => `${s.level}. ${s.name}${s.description ? ` - ${s.description}` : ''}`).join('\n')
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setEditingCareer(null);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
}
|
|
|
|
|
|
setIsModalOpen(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async (values: any) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 解析阶段数据
|
|
|
|
|
|
const stagesText = values.stages || '';
|
|
|
|
|
|
const stages: CareerStage[] = stagesText.split('\n')
|
|
|
|
|
|
.filter((line: string) => line.trim())
|
|
|
|
|
|
.map((line: string, index: number) => {
|
|
|
|
|
|
const match = line.match(/^(\d+)\.\s*([^-]+)(?:\s*-\s*(.*))?$/);
|
|
|
|
|
|
if (match) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
level: parseInt(match[1]),
|
|
|
|
|
|
name: match[2].trim(),
|
|
|
|
|
|
description: match[3]?.trim() || ''
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
level: index + 1,
|
|
|
|
|
|
name: line.trim(),
|
|
|
|
|
|
description: ''
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = {
|
|
|
|
|
|
...values,
|
|
|
|
|
|
stages,
|
|
|
|
|
|
max_stage: stages.length
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (editingCareer) {
|
2025-12-23 09:28:24 +08:00
|
|
|
|
await api.put(`/careers/${editingCareer.id}`, data);
|
2025-12-22 19:53:31 +08:00
|
|
|
|
message.success('职业更新成功');
|
|
|
|
|
|
} else {
|
2025-12-23 09:28:24 +08:00
|
|
|
|
await api.post('/careers', {
|
2025-12-22 19:53:31 +08:00
|
|
|
|
...data,
|
|
|
|
|
|
project_id: projectId,
|
|
|
|
|
|
source: 'manual'
|
|
|
|
|
|
});
|
|
|
|
|
|
message.success('职业创建成功');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setIsModalOpen(false);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
fetchCareers();
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
message.error(error.response?.data?.detail || '操作失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleDelete = async (id: string) => {
|
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: '确认删除',
|
|
|
|
|
|
content: '确定要删除这个职业吗?如果有角色使用了该职业,将无法删除。',
|
|
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
try {
|
2025-12-23 09:28:24 +08:00
|
|
|
|
await api.delete(`/careers/${id}`);
|
2025-12-22 19:53:31 +08:00
|
|
|
|
message.success('职业删除成功');
|
|
|
|
|
|
fetchCareers();
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
message.error(error.response?.data?.detail || '删除失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleAIGenerate = async (values: any) => {
|
|
|
|
|
|
setIsAIModalOpen(false);
|
|
|
|
|
|
setAiGenerating(true);
|
|
|
|
|
|
setAiProgress(0);
|
|
|
|
|
|
setAiMessage('开始生成新职业...');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const eventSource = new EventSource(
|
2025-12-23 09:28:24 +08:00
|
|
|
|
`/api/careers/generate-system?` +
|
2025-12-22 19:53:31 +08:00
|
|
|
|
new URLSearchParams({
|
|
|
|
|
|
project_id: projectId || '',
|
|
|
|
|
|
main_career_count: values.main_career_count.toString(),
|
|
|
|
|
|
sub_career_count: values.sub_career_count.toString(),
|
|
|
|
|
|
enable_mcp: 'false'
|
2025-12-23 09:28:24 +08:00
|
|
|
|
}).toString(),
|
|
|
|
|
|
{ withCredentials: true }
|
2025-12-22 19:53:31 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
eventSource.onmessage = (event) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = JSON.parse(event.data);
|
|
|
|
|
|
|
|
|
|
|
|
if (data.type === 'progress') {
|
|
|
|
|
|
setAiProgress(data.progress || 0);
|
|
|
|
|
|
setAiMessage(data.message || '');
|
|
|
|
|
|
} else if (data.type === 'done') {
|
|
|
|
|
|
eventSource.close();
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
setAiGenerating(false);
|
|
|
|
|
|
message.success('AI新职业生成完成!');
|
|
|
|
|
|
fetchCareers();
|
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
} else if (data.type === 'error') {
|
|
|
|
|
|
eventSource.close();
|
|
|
|
|
|
setAiGenerating(false);
|
|
|
|
|
|
message.error(data.message || '生成失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('解析SSE数据失败:', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
eventSource.onerror = () => {
|
|
|
|
|
|
eventSource.close();
|
|
|
|
|
|
setAiGenerating(false);
|
|
|
|
|
|
message.error('连接中断,生成失败');
|
|
|
|
|
|
};
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
setAiGenerating(false);
|
|
|
|
|
|
message.error(err.message || '启动生成失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const renderCareerCard = (career: Career) => (
|
|
|
|
|
|
<Card
|
|
|
|
|
|
key={career.id}
|
|
|
|
|
|
title={
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<TrophyOutlined />
|
|
|
|
|
|
{career.name}
|
|
|
|
|
|
<Tag color={career.source === 'ai' ? 'blue' : 'default'}>
|
|
|
|
|
|
{career.source === 'ai' ? 'AI生成' : '手动创建'}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
{career.category && <Tag>{career.category}</Tag>}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
|
|
|
|
|
extra={
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<Button size="small" icon={<EditOutlined />} onClick={() => handleOpenModal(career)} />
|
|
|
|
|
|
<Button size="small" danger icon={<DeleteOutlined />} onClick={() => handleDelete(career.id)} />
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
|
|
|
|
|
style={{ marginBottom: 16 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Paragraph ellipsis={{ rows: 2 }}>{career.description || '暂无描述'}</Paragraph>
|
|
|
|
|
|
<Divider style={{ margin: '12px 0' }} />
|
|
|
|
|
|
<Text strong>阶段体系(共{career.max_stage}个):</Text>
|
|
|
|
|
|
<div style={{ maxHeight: 120, overflowY: 'auto', marginTop: 8 }}>
|
|
|
|
|
|
{career.stages.slice(0, 5).map(stage => (
|
|
|
|
|
|
<div key={stage.level} style={{ marginLeft: 16, marginBottom: 4 }}>
|
|
|
|
|
|
<Text type="secondary">{stage.level}. {stage.name}</Text>
|
|
|
|
|
|
{stage.description && <Text type="secondary" style={{ fontSize: 12 }}> - {stage.description}</Text>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{career.stages.length > 5 && (
|
|
|
|
|
|
<Text type="secondary" style={{ marginLeft: 16 }}>...还有{career.stages.length - 5}个阶段</Text>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{career.special_abilities && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Divider style={{ margin: '12px 0' }} />
|
|
|
|
|
|
<Text strong>特殊能力:</Text>
|
|
|
|
|
|
<Paragraph ellipsis={{ rows: 2 }} style={{ marginTop: 4 }}>{career.special_abilities}</Paragraph>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const tabItems = [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'main',
|
|
|
|
|
|
label: `主职业 (${mainCareers.length})`,
|
|
|
|
|
|
children: mainCareers.length > 0 ? (
|
|
|
|
|
|
<div>{mainCareers.map(renderCareerCard)}</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Empty description="还没有主职业" />
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'sub',
|
|
|
|
|
|
label: `副职业 (${subCareers.length})`,
|
|
|
|
|
|
children: subCareers.length > 0 ? (
|
|
|
|
|
|
<div>{subCareers.map(renderCareerCard)}</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Empty description="还没有副职业" />
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
height: '100%',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
flexDirection: 'column',
|
|
|
|
|
|
overflow: 'hidden'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
{/* 固定头部 */}
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
padding: '16px 16px 0 16px',
|
|
|
|
|
|
flexShrink: 0
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
marginBottom: 16,
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
|
gap: '12px'
|
|
|
|
|
|
}}>
|
2025-12-29 12:08:01 +08:00
|
|
|
|
<Title level={3} style={{ margin: 0 }}>
|
|
|
|
|
|
<TrophyOutlined style={{ marginRight: 8 }} />
|
|
|
|
|
|
职业管理
|
|
|
|
|
|
</Title>
|
2025-12-22 19:53:31 +08:00
|
|
|
|
<Space wrap>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="dashed"
|
|
|
|
|
|
icon={<ThunderboltOutlined />}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
aiForm.resetFields();
|
|
|
|
|
|
setIsAIModalOpen(true);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
AI生成新职业
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
icon={<PlusOutlined />}
|
|
|
|
|
|
onClick={() => handleOpenModal()}
|
|
|
|
|
|
>
|
|
|
|
|
|
新增职业
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 可滚动的内容区域 */}
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
overflow: 'auto',
|
|
|
|
|
|
padding: '0 16px 16px 16px'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<Tabs items={tabItems} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 创建/编辑对话框 */}
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title={editingCareer ? '编辑职业' : '新增职业'}
|
|
|
|
|
|
open={isModalOpen}
|
|
|
|
|
|
onCancel={() => {
|
|
|
|
|
|
setIsModalOpen(false);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
}}
|
|
|
|
|
|
footer={null}
|
|
|
|
|
|
width={700}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col span={16}>
|
|
|
|
|
|
<Form.Item label="职业名称" name="name" rules={[{ required: true }]}>
|
|
|
|
|
|
<Input placeholder="如:剑修、炼丹师" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={8}>
|
|
|
|
|
|
<Form.Item label="类型" name="type" rules={[{ required: true }]} initialValue="main">
|
|
|
|
|
|
<Select>
|
|
|
|
|
|
<Select.Option value="main">主职业</Select.Option>
|
|
|
|
|
|
<Select.Option value="sub">副职业</Select.Option>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item label="职业描述" name="description">
|
|
|
|
|
|
<TextArea rows={2} placeholder="描述这个职业..." />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item label="职业分类" name="category">
|
|
|
|
|
|
<Input placeholder="如:战斗系、生产系、辅助系" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item label="职业阶段" name="stages" tooltip="每行一个阶段,格式:1. 阶段名 - 描述">
|
|
|
|
|
|
<TextArea
|
|
|
|
|
|
rows={8}
|
|
|
|
|
|
placeholder="示例: 1. 炼气期 - 初窥门径 2. 筑基期 - 根基稳固 3. 金丹期 - 凝结金丹"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item label="职业要求" name="requirements">
|
|
|
|
|
|
<TextArea rows={2} placeholder="需要什么条件才能修炼..." />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item label="特殊能力" name="special_abilities">
|
|
|
|
|
|
<TextArea rows={2} placeholder="这个职业的特殊能力..." />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item label="世界观规则" name="worldview_rules">
|
|
|
|
|
|
<TextArea rows={2} placeholder="如何融入世界观..." />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item>
|
|
|
|
|
|
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
|
|
|
|
|
<Button onClick={() => setIsModalOpen(false)}>取消</Button>
|
|
|
|
|
|
<Button type="primary" htmlType="submit">
|
|
|
|
|
|
{editingCareer ? '更新' : '创建'}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
|
|
{/* AI生成对话框 */}
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title="AI生成新职业(增量式)"
|
|
|
|
|
|
open={isAIModalOpen}
|
|
|
|
|
|
onCancel={() => setIsAIModalOpen(false)}
|
|
|
|
|
|
footer={null}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Form form={aiForm} layout="vertical" onFinish={handleAIGenerate}>
|
|
|
|
|
|
<Paragraph type="secondary">
|
|
|
|
|
|
AI将分析当前世界观和已有职业,智能生成新的补充职业。
|
|
|
|
|
|
<br />
|
|
|
|
|
|
💡 可以多次生成,逐步完善职业体系,不会替换已有职业。
|
|
|
|
|
|
</Paragraph>
|
|
|
|
|
|
<Divider style={{ margin: '12px 0' }} />
|
|
|
|
|
|
<Form.Item label="本次新增主职业数量" name="main_career_count" initialValue={3}>
|
|
|
|
|
|
<InputNumber min={1} max={10} style={{ width: '100%' }} />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
<Form.Item label="本次新增副职业数量" name="sub_career_count" initialValue={5}>
|
|
|
|
|
|
<InputNumber min={0} max={15} style={{ width: '100%' }} />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
<Form.Item>
|
|
|
|
|
|
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
|
|
|
|
|
<Button onClick={() => setIsAIModalOpen(false)}>取消</Button>
|
|
|
|
|
|
<Button type="primary" icon={<ThunderboltOutlined />} htmlType="submit">
|
|
|
|
|
|
开始生成
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
|
|
{/* AI生成进度 */}
|
|
|
|
|
|
<SSEProgressModal
|
|
|
|
|
|
visible={aiGenerating}
|
|
|
|
|
|
progress={aiProgress}
|
|
|
|
|
|
message={aiMessage}
|
|
|
|
|
|
title="AI生成新职业中..."
|
|
|
|
|
|
onCancel={() => setAiGenerating(false)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|