|
|
@@ -357,11 +357,15 @@
|
|
|
<h5>生成的图片:</h5>
|
|
|
<img :src="state.generatedContent.imageUrl" class="extra-preview-image" alt="AI生成图片">
|
|
|
</div>
|
|
|
+ <div v-if="state.generatedContent.videoUrl" class="extra-image-preview">
|
|
|
+ <h5>生成的视频:</h5>
|
|
|
+ <video :src="state.generatedContent.videoUrl" controls class="preview-video" alt="AI生成视频"></video>
|
|
|
+ </div>
|
|
|
|
|
|
<!-- 添加台灯显示区域 -->
|
|
|
<div class="lamp-preview-container">
|
|
|
<h5>AI智能台灯</h5>
|
|
|
- <div class="lamp-display" :style="{ filter: `brightness(${state.lamp.brightness / 50})` }">
|
|
|
+ <div class="lamp-display" :style="{ filter: `brightness(${state.lamp.brightness / 100})` }">
|
|
|
<div class="lamp-image" :style="{ boxShadow: `0 0 40px 20px ${state.lamp.color}80` }"></div>
|
|
|
</div>
|
|
|
<div class="lamp-info">
|
|
|
@@ -404,8 +408,15 @@ import * as hans from 'blockly/msg/zh-hans'
|
|
|
import { ElDialog, ElButton, ElMessage } from 'element-plus';
|
|
|
|
|
|
//【文生图】文生图
|
|
|
-import {AiImageStatusEnum, CreatePainting, PaintingGetMys} from '@/api/questions.js'
|
|
|
-import { getModelIdByType } from '@/api/teachers.js'
|
|
|
+import {
|
|
|
+ AiImageStatusEnum,
|
|
|
+ CreatePainting,
|
|
|
+ PaintingGetMys,
|
|
|
+ CreateVideo,
|
|
|
+ VideoGetMys,
|
|
|
+ sendChatMessageStream, CreateDialogue
|
|
|
+} from '@/api/questions.js'
|
|
|
+import {getModelIdByType, ModelPlatformEnum} from '@/api/teachers.js'
|
|
|
import { ModelTypeEnum } from '@/api/teachers.js'
|
|
|
import { globalState } from '@/utils/globalState.js'
|
|
|
|
|
|
@@ -424,21 +435,33 @@ const state = reactive({
|
|
|
previewContent: '',
|
|
|
isProcessing: false,
|
|
|
|
|
|
- //【文生图】文生图
|
|
|
+ //年级
|
|
|
gradeId: '',
|
|
|
+
|
|
|
+ //【文生图】文生图
|
|
|
inProgressImageMap: {},
|
|
|
|
|
|
+ //【文生视频】文生视频
|
|
|
+ inProgressVideoMap: {},
|
|
|
|
|
|
// 台灯状态
|
|
|
lamp: {
|
|
|
- brightness: 50, // 默认亮度50%
|
|
|
+ brightness: 0, // 默认亮度50%
|
|
|
color: '#ffffff' // 默认颜色白色
|
|
|
},
|
|
|
+
|
|
|
+ // 【文本文】对话相关状态
|
|
|
+ activeConversationId: null,
|
|
|
+ conversationInProgress: false,
|
|
|
+ conversationInAbortController: null
|
|
|
});
|
|
|
|
|
|
// 【文生图】自动刷新image列表的定时器
|
|
|
const inProgressTimer = ref();
|
|
|
|
|
|
+// 【文生视频】自动刷新video列表的定时器
|
|
|
+const inProgressVideoTimer = ref();
|
|
|
+
|
|
|
// 初始化Blockly工作区和自定义积木
|
|
|
onMounted(async () => {
|
|
|
// 从全局状态初始化年级ID
|
|
|
@@ -556,7 +579,7 @@ onMounted(async () => {
|
|
|
this.appendDummyInput()
|
|
|
.appendField('智能台灯控制');
|
|
|
this.appendValueInput('BRIGHTNESS')
|
|
|
- .setCheck('Number')
|
|
|
+ .setCheck(['Number', 'String'])
|
|
|
.appendField('亮度 (0-100):');
|
|
|
this.appendValueInput('COLOR')
|
|
|
.setCheck('String')
|
|
|
@@ -576,7 +599,7 @@ onMounted(async () => {
|
|
|
this.appendDummyInput()
|
|
|
.appendField('设置台灯亮度');
|
|
|
this.appendValueInput('BRIGHTNESS')
|
|
|
- .setCheck('Number')
|
|
|
+ .setCheck(['Number', 'String'])
|
|
|
.appendField('亮度 (0-100):');
|
|
|
this.setInputsInline(false);
|
|
|
this.setPreviousStatement(true, null);
|
|
|
@@ -627,6 +650,10 @@ onUnmounted(() => {
|
|
|
if (inProgressTimer.value) {
|
|
|
clearInterval(inProgressTimer.value);
|
|
|
}
|
|
|
+
|
|
|
+ if (inProgressVideoTimer.value) {
|
|
|
+ clearInterval(inProgressVideoTimer.value);
|
|
|
+ }
|
|
|
});
|
|
|
|
|
|
// AI服务配置
|
|
|
@@ -723,7 +750,7 @@ const aiService = {
|
|
|
try {
|
|
|
|
|
|
//获取文生图-模型id
|
|
|
- const modelRes = await getModelIdByType({ type: ModelTypeEnum.TEXT_TO_IMAGE, platform: "DouBao" })
|
|
|
+ const modelRes = await getModelIdByType({ type: ModelTypeEnum.TEXT_TO_IMAGE, platform: ModelPlatformEnum.DOUBAO })
|
|
|
if (!modelRes.data) {
|
|
|
ElMessage.error('获取模型ID失败');
|
|
|
return null;
|
|
|
@@ -825,10 +852,11 @@ const aiService = {
|
|
|
const list = await PaintingGetMys([imageId]);
|
|
|
if (list.data && list.data.length > 0) {
|
|
|
const image = list.data[0];
|
|
|
- if (image.status === AiImageStatusEnum.COMPLETED) {
|
|
|
+ console.log('图片状态:', image.status, image.picUrl, AiImageStatusEnum);
|
|
|
+ if (image.status === AiImageStatusEnum.SUCCESS) {
|
|
|
clearInterval(checkInterval);
|
|
|
resolve(image.picUrl);
|
|
|
- } else if (image.status === AiImageStatusEnum.FAILED) {
|
|
|
+ } else if (image.status === AiImageStatusEnum.FAIL) {
|
|
|
clearInterval(checkInterval);
|
|
|
reject(new Error(image.error || '图片生成失败'));
|
|
|
}
|
|
|
@@ -847,78 +875,219 @@ const aiService = {
|
|
|
|
|
|
state.isProcessing = true;
|
|
|
try {
|
|
|
- // 视频生成通常耗时较长,这里使用轮询或WebSocket会更好
|
|
|
- // 简单实现:先获取任务ID,再轮询结果
|
|
|
- const initResponse = await fetch(`${aiServiceConfig.baseUrl}${aiServiceConfig.endpoints.textToVideo}`, {
|
|
|
- method: 'POST',
|
|
|
- headers: { 'Content-Type': 'application/json' },
|
|
|
- body: JSON.stringify({ prompt })
|
|
|
+ //获取视频生成模型id
|
|
|
+ const modelRes = await getModelIdByType({ type: ModelTypeEnum.IMAGE_TO_VIDEO, platform: ModelPlatformEnum.DOUBAO })
|
|
|
+ if (!modelRes.data) {
|
|
|
+ ElMessage.error('获取模型ID失败');
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用CreateVideo API创建视频任务
|
|
|
+ const createRes = await CreateVideo({
|
|
|
+ "modelId": modelRes.data,
|
|
|
+ "prompt": prompt,
|
|
|
+ "duration": 4,
|
|
|
+ "resolution": "1080P"
|
|
|
});
|
|
|
|
|
|
- if (!initResponse.ok) throw new Error('初始化视频生成失败');
|
|
|
+ // 记录任务ID到视频专用映射中
|
|
|
+ state.inProgressVideoMap[createRes.data] = {id: createRes.data, status: AiImageStatusEnum.IN_PROGRESS};
|
|
|
+
|
|
|
+ // 开始视频专用的轮询任务状态
|
|
|
+ if (!inProgressVideoTimer.value) {
|
|
|
+ this.startVideoPolling();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果需要等待完成,等待视频生成完成
|
|
|
+ if (waitForCompletion) {
|
|
|
+ return await this.waitForVideoCompletion(createRes.data);
|
|
|
+ }
|
|
|
+
|
|
|
+ return createRes.data; // 返回任务ID
|
|
|
+ } catch (error) {
|
|
|
+ console.error('生成视频失败:', error);
|
|
|
+ ElMessage.error('生成视频失败: ' + error.message);
|
|
|
+ return null;
|
|
|
+ } finally {
|
|
|
+ state.isProcessing = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- const initData = await initResponse.json();
|
|
|
- const taskId = initData.taskId;
|
|
|
+ // 【视频生成】开始轮询视频生成状态
|
|
|
+ startVideoPolling() {
|
|
|
+ if (inProgressVideoTimer.value) {
|
|
|
+ clearInterval(inProgressVideoTimer.value);
|
|
|
+ }
|
|
|
|
|
|
- // 轮询结果
|
|
|
- const checkResult = async () => {
|
|
|
- const resultResponse = await fetch(`${aiServiceConfig.baseUrl}${aiServiceConfig.endpoints.textToVideo}/${taskId}`);
|
|
|
- const resultData = await resultResponse.json();
|
|
|
+ inProgressVideoTimer.value = setInterval(async () => {
|
|
|
+ await this.refreshWatchVideos();
|
|
|
+ }, 3000); // 每3秒轮询一次
|
|
|
+ },
|
|
|
|
|
|
- if (resultData.status === 'completed') {
|
|
|
- state.generatedContent.videoUrl = resultData.videoUrl;
|
|
|
+// 【视频生成】轮询生成中的视频列表
|
|
|
+ async refreshWatchVideos() {
|
|
|
+ const videoIds = Object.keys(state.inProgressVideoMap).map(Number);
|
|
|
+ if (videoIds.length === 0) {
|
|
|
+ if (inProgressVideoTimer.value) {
|
|
|
+ clearInterval(inProgressVideoTimer.value);
|
|
|
+ inProgressVideoTimer.value = null;
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- // 显示预览
|
|
|
+ try {
|
|
|
+ const list = await VideoGetMys(videoIds);
|
|
|
+ const newWatchVideos = {};
|
|
|
+
|
|
|
+ list.data.forEach((video) => {
|
|
|
+ if (video.status === AiImageStatusEnum.IN_PROGRESS) {
|
|
|
+ newWatchVideos[video.id] = video;
|
|
|
+ } else if (video.status === AiImageStatusEnum.SUCCESS) {
|
|
|
+ // 视频生成完成,更新状态并显示预览
|
|
|
+ state.generatedContent.videoUrl = video.videoUrl;
|
|
|
state.previewType = 'video';
|
|
|
- state.previewContent = resultData.videoUrl;
|
|
|
+ state.previewContent = video.videoUrl;
|
|
|
state.previewVisible = true;
|
|
|
|
|
|
- return resultData.videoUrl;
|
|
|
- } else if (resultData.status === 'failed') {
|
|
|
- throw new Error(resultData.error || '视频生成失败');
|
|
|
- } else {
|
|
|
- // 继续轮询
|
|
|
- await new Promise(resolve => setTimeout(resolve, 3000));
|
|
|
- return checkResult();
|
|
|
+ // 打印到控制台,便于在结果区域显示
|
|
|
+ console.log('视频生成完成:', video.videoUrl);
|
|
|
+ } else if (video.status === AiImageStatusEnum.FAIL) {
|
|
|
+ // 视频生成失败
|
|
|
+ ElMessage.error('视频生成失败: ' + (video.error || '未知错误'));
|
|
|
+ console.error('视频生成失败:', video.id, video.error);
|
|
|
}
|
|
|
- };
|
|
|
+ });
|
|
|
+
|
|
|
+ state.inProgressVideoMap = newWatchVideos;
|
|
|
|
|
|
- return checkResult();
|
|
|
+ // 如果没有正在处理的视频,清除定时器
|
|
|
+ if (Object.keys(newWatchVideos).length === 0 && inProgressVideoTimer.value) {
|
|
|
+ clearInterval(inProgressVideoTimer.value);
|
|
|
+ inProgressVideoTimer.value = null;
|
|
|
+ }
|
|
|
} catch (error) {
|
|
|
- console.error('生成视频失败:', error);
|
|
|
- ElMessage.error('生成视频失败: ' + error.message);
|
|
|
- return null;
|
|
|
- } finally {
|
|
|
- state.isProcessing = false;
|
|
|
+ console.error('轮询视频状态失败:', error);
|
|
|
}
|
|
|
},
|
|
|
|
|
|
+ // 【视频生成】等待视频生成完成
|
|
|
+ async waitForVideoCompletion(videoId) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ const checkInterval = setInterval(async () => {
|
|
|
+ try {
|
|
|
+ const list = await VideoGetMys([videoId]);
|
|
|
+ if (list.data && list.data.length > 0) {
|
|
|
+ const video = list.data[0];
|
|
|
+ console.log('视频状态:', video.status, video.videoUrl);
|
|
|
+ if (video.status === AiImageStatusEnum.SUCCESS) {
|
|
|
+ clearInterval(checkInterval);
|
|
|
+ // 视频生成完成,设置预览状态
|
|
|
+ state.generatedContent.videoUrl = video.videoUrl;
|
|
|
+ state.previewType = 'video';
|
|
|
+ state.previewContent = video.videoUrl;
|
|
|
+ state.previewVisible = true;
|
|
|
+ resolve(video.videoUrl);
|
|
|
+ } else if (video.status === AiImageStatusEnum.FAIL) {
|
|
|
+ clearInterval(checkInterval);
|
|
|
+ reject(new Error(video.error || '视频生成失败'));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ clearInterval(checkInterval);
|
|
|
+ reject(error);
|
|
|
+ }
|
|
|
+ }, 3000);
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
// 文本生成文本(如AI对话)
|
|
|
async textToText(prompt, model = 'default') {
|
|
|
-
|
|
|
console.log('textToText', prompt, model);
|
|
|
state.isProcessing = true;
|
|
|
+
|
|
|
try {
|
|
|
- const response = await fetch(`${aiServiceConfig.baseUrl}${aiServiceConfig.endpoints.textToText}`, {
|
|
|
- method: 'POST',
|
|
|
- headers: { 'Content-Type': 'application/json' },
|
|
|
- body: JSON.stringify({
|
|
|
- prompt,
|
|
|
- model
|
|
|
- })
|
|
|
- });
|
|
|
+ // 如果没有活跃的对话ID,创建新对话
|
|
|
+ if (!state.activeConversationId) {
|
|
|
+ // 使用与TextToText.vue相同的方式创建对话
|
|
|
+ const res = await CreateDialogue({ roleId: 10 });
|
|
|
+ state.activeConversationId = res.data;
|
|
|
+ console.log("创建会话:", res.data);
|
|
|
+ }
|
|
|
|
|
|
- if (!response.ok) throw new Error('AI处理失败');
|
|
|
+ // 创建AbortController实例
|
|
|
+ state.conversationInAbortController = new AbortController();
|
|
|
+ state.conversationInProgress = true;
|
|
|
|
|
|
- const data = await response.json();
|
|
|
- state.generatedContent.text = data.result;
|
|
|
+ // 使用流式API发送消息
|
|
|
+ let resultText = '';
|
|
|
+ let isFirstChunk = true;
|
|
|
|
|
|
- // 显示预览
|
|
|
- state.previewType = 'text';
|
|
|
- state.previewContent = data.result;
|
|
|
- state.previewVisible = true;
|
|
|
+ await sendChatMessageStream(
|
|
|
+ state.activeConversationId,
|
|
|
+ prompt,
|
|
|
+ null,
|
|
|
+ state.conversationInAbortController,
|
|
|
+ true, // 启用上下文
|
|
|
+ async (res) => {
|
|
|
+ try {
|
|
|
+ const { code, data, msg } = JSON.parse(res.data);
|
|
|
+ if (code !== 0) {
|
|
|
+ console.log(`对话异常! ${msg}`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据事件类型处理
|
|
|
+ if (data.eventType === "TEXT") {
|
|
|
+ // 如果内容为空,就不处理
|
|
|
+ if (data.receive?.content === "") {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理文本消息
|
|
|
+ resultText += data.receive.content;
|
|
|
+
|
|
|
+ // 首次返回时更新预览内容
|
|
|
+ if (isFirstChunk) {
|
|
|
+ isFirstChunk = false;
|
|
|
+ // 设置预览内容
|
|
|
+ state.generatedContent.text = resultText;
|
|
|
+ state.previewType = 'text';
|
|
|
+ state.previewContent = resultText;
|
|
|
+ if (!state.previewVisible) {
|
|
|
+ state.previewVisible = true;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 更新预览内容
|
|
|
+ state.generatedContent.text = resultText;
|
|
|
+ state.previewContent = resultText;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('处理流式响应失败:', error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ (error) => {
|
|
|
+ console.log(`对话异常! ${error}`);
|
|
|
+ this.stopTextToTextStream();
|
|
|
+ throw error;
|
|
|
+ },
|
|
|
+ () => {
|
|
|
+ console.log(`结束对话!`);
|
|
|
+ this.stopTextToTextStream();
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ // 确保最终结果被设置
|
|
|
+ if (resultText) {
|
|
|
+ state.generatedContent.text = resultText;
|
|
|
+ state.previewType = 'text';
|
|
|
+ state.previewContent = resultText;
|
|
|
+ if (!state.previewVisible) {
|
|
|
+ state.previewVisible = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- return data.result;
|
|
|
+ return resultText;
|
|
|
} catch (error) {
|
|
|
console.error('AI文本处理失败:', error);
|
|
|
ElMessage.error('AI文本处理失败: ' + error.message);
|
|
|
@@ -928,6 +1097,14 @@ const aiService = {
|
|
|
}
|
|
|
},
|
|
|
|
|
|
+ // 停止文本生成流
|
|
|
+ stopTextToTextStream() {
|
|
|
+ if (state.conversationInAbortController) {
|
|
|
+ state.conversationInAbortController.abort();
|
|
|
+ }
|
|
|
+ state.conversationInProgress = false;
|
|
|
+ },
|
|
|
+
|
|
|
// 语义识别分析
|
|
|
async semanticAnalysis(text, type = 'general') {
|
|
|
console.log('semanticAnalysis', text, type);
|
|
|
@@ -1079,14 +1256,14 @@ function registerJavaScriptGenerators() {
|
|
|
javascriptGenerator.forBlock['ai_smart_lamp'] = function(block, generator) {
|
|
|
const brightness = generator.valueToCode(block, 'BRIGHTNESS', javascriptGenerator.ORDER_ATOMIC);
|
|
|
const color = generator.valueToCode(block, 'COLOR', javascriptGenerator.ORDER_ATOMIC);
|
|
|
- const code = `await aiService.controlLamp(${brightness || '50'}, ${color || "'白'"});`;
|
|
|
+ const code = `await aiService.controlLamp(${brightness || '0'}, ${color || "'白'"});`;
|
|
|
return code;
|
|
|
};
|
|
|
|
|
|
// 设置台灯亮度
|
|
|
javascriptGenerator.forBlock['ai_lamp_set_brightness'] = function(block, generator) {
|
|
|
const brightness = generator.valueToCode(block, 'BRIGHTNESS', javascriptGenerator.ORDER_ATOMIC);
|
|
|
- const code = `await aiService.setLampBrightness(${brightness || '50'});`;
|
|
|
+ const code = `await aiService.setLampBrightness(${brightness || '0'});`;
|
|
|
return code;
|
|
|
};
|
|
|
|
|
|
@@ -1144,14 +1321,14 @@ function registerPythonGenerators() {
|
|
|
pythonGenerator.forBlock['ai_smart_lamp'] = function(block, generator) {
|
|
|
const brightness = generator.valueToCode(block, 'BRIGHTNESS', pythonGenerator.ORDER_ATOMIC);
|
|
|
const color = generator.valueToCode(block, 'COLOR', pythonGenerator.ORDER_ATOMIC);
|
|
|
- const code = `ai_service.control_lamp(${brightness || '50'}, ${color || "'白'"})`;
|
|
|
+ const code = `ai_service.control_lamp(${brightness || '0'}, ${color || "'白'"})`;
|
|
|
return code;
|
|
|
};
|
|
|
|
|
|
// 设置台灯亮度
|
|
|
pythonGenerator.forBlock['ai_lamp_set_brightness'] = function(block, generator) {
|
|
|
const brightness = generator.valueToCode(block, 'BRIGHTNESS', pythonGenerator.ORDER_ATOMIC);
|
|
|
- const code = `ai_service.set_lamp_brightness(${brightness || '50'})`;
|
|
|
+ const code = `ai_service.set_lamp_brightness(${brightness || '0'})`;
|
|
|
return code;
|
|
|
};
|
|
|
|
|
|
@@ -1296,6 +1473,9 @@ function load() {
|
|
|
if (!input.value) return;
|
|
|
|
|
|
try {
|
|
|
+ //关闭预览
|
|
|
+ handleClosePreview();
|
|
|
+
|
|
|
const valid = saveIsValid(input.value);
|
|
|
if (valid.json) {
|
|
|
const parsedState = JSON.parse(input.value);
|
|
|
@@ -1343,6 +1523,9 @@ function handleClosePreview() {
|
|
|
state.previewVisible = false;
|
|
|
state.previewContent = '';
|
|
|
state.previewType = '';
|
|
|
+ state.generatedContent.text = null;
|
|
|
+ state.generatedContent.imageUrl = null;
|
|
|
+ state.generatedContent.videoUrl = null;
|
|
|
}
|
|
|
|
|
|
// 将aiService挂载到window,以便执行生成的代码时可以访问
|
|
|
@@ -1466,6 +1649,7 @@ window.aiService = aiService;
|
|
|
|
|
|
//【文生图预览】
|
|
|
.extra-image-preview {
|
|
|
+ color: black;
|
|
|
margin-top: 10px;
|
|
|
padding: 10px;
|
|
|
border: 1px solid #ddd;
|