import React, { useEffect, useState, useRef } from 'react';
import { isEmpty, isFunction, isNotEmptyArr } from 'cn-lib';
import { Button, Form, Input, Modal, Row } from 'antd';
import { marked } from 'marked';
import { ReactComponent as Robot } from '@/assets/robot.svg';
const AIBoss = ({
visible,
setVisible,
cmdForm,
question,
getRequestBody,
apiKey,
currentStage,
savePlanCommand,
}) => {
const [form] = Form.useForm();
const answerRef = useRef(null);
const [isAnswering, setAnswering] = useState(false);
const [currentAnswer, setAnswer] = useState('');
const decoder = new TextDecoder();
const aiStreamTalk = () => {
if (isEmpty(apiKey)) {
return;
}
setAnswering(true);
let buffer = '';
const body = isFunction(getRequestBody) ? getRequestBody(question) : { question };
answerRef.current = '';
fetch(`${origin}/agent/v1/chat-messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
response_mode: 'streaming',
conversation_id: '',
files: [],
user: 'abc-123',
query: question,
...(body || {}),
}),
})
.then(response => {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return response.body.getReader();
})
.then(reader => {
const readChunk = async () => {
try {
const { done, value } = await reader.read();
if (done) {
setAnswering(false);
return;
}
buffer += decoder.decode(value, { stream: true });
let boundary;
while ((boundary = buffer.indexOf('\n')) !== -1) {
const chunk = buffer.slice(0, boundary);
buffer = buffer.slice(boundary + 1);
if (chunk.startsWith('data: ')) {
const dataStr = chunk.slice(6);
try {
const { answer } = JSON.parse(dataStr);
if (typeof answer === 'string') {
answerRef.current += answer;
setAnswer(answerRef.current);
}
} catch (e) {
console.error('解析JSON失败:', e, '数据:', dataStr);
}
}
}
readChunk();
} catch (e) {
console.error('流读取错误:', e);
setAnswering(false);
}
};
readChunk();
})
.catch(() => {
setAnswering(false);
});
};
useEffect(() => {
if (question) {
setAnswer('');
aiStreamTalk();
}
}, [question]);
useEffect(() => {
if (currentAnswer) {
const node = document.getElementById('cnt-ai-footer');
if (node) {
node.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });
}
}
}, [currentAnswer]);
const extractJsonPart = str => {
const regex = /```json([\s\S]*?)```/;
const match = regex.exec(str);
if (match) {
const jsonContent = match[1].trim().replace(/},\s*\]$/, '}]');
try {
return JSON.parse(jsonContent);
} catch (e) {
return [];
}
} else {
return [];
}
};
useEffect(() => {
if (!isAnswering && currentAnswer) {
const aa = extractJsonPart(currentAnswer);
let ss = '';
if (isNotEmptyArr(aa)) {
aa.forEach(({ cmd }, index) => {
ss += `${cmd}${index !== aa.length - 1 ? `\n` : ''}`;
});
}
if (ss) {
form.setFieldsValue({ content: ss });
}
}
}, [isAnswering]);
return (
<Modal
destroyOnClose
maskClosable={false}
title={
<div style={{ display: 'flex', alignContent: 'center' }}>
<Robot />
<span style={{ marginLeft: 8, marginTop: 10 }}>{`AI${
isAnswering ? '深度思考中' : currentAnswer ? '已完成深度思考' : ''
}`}</span>
</div>
}
open={visible}
footer={
<Row justify="end">
<Button
onClick={() => {
setVisible(false);
}}
>
取消
</Button>
<Button
type="primary"
disabled={isAnswering}
onClick={() => {
if (currentStage === 1) {
const content = form.getFieldValue('content');
if (content) {
cmdForm.setFieldsValue({ planContent: content });
savePlanCommand({
planContent: content,
planDesc: cmdForm.getFieldValue('planDesc') || '',
});
}
}
setVisible(false);
}}
>
{currentStage === 1 ? '确认使用' : '确认'}
</Button>
</Row>
}
onCancel={() => {
setVisible(false);
}}
width={720}
>
{currentStage === 1 ? (
<>
<div
className="cnt-ai cnt-default-border"
style={{ height: 300, overflow: 'auto', padding: '4px 11px' }}
>
<div
dangerouslySetInnerHTML={{ __html: marked.parse(currentAnswer) }}
className="markdown-body"
/>
<div id="cnt-ai-footer" style={{ height: 1 }} />
</div>
<Form form={form} layout="vertical" style={{ marginTop: 24 }}>
<Form.Item name="content" label="AI辅助生成配置" style={{ marginBottom: 0 }}>
<Input.TextArea style={{ height: 200 }} />
</Form.Item>
</Form>
</>
) : (
<div
className="cnt-ai cnt-default-border"
style={{ height: 600, overflow: 'auto', padding: '4px 11px' }}
>
<div
dangerouslySetInnerHTML={{ __html: marked.parse(currentAnswer) }}
className="markdown-body"
/>
<div id="cnt-ai-footer" style={{ height: 1 }} />
</div>
)}
</Modal>
);
};
export default AIBoss;
console