|
@@ -57,7 +57,6 @@
|
|
:step="1"
|
|
:step="1"
|
|
step-strictly
|
|
step-strictly
|
|
:controls="false"
|
|
:controls="false"
|
|
- disabled
|
|
|
|
></el-input-number>
|
|
></el-input-number>
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
|
|
|
@@ -227,10 +226,20 @@ export default {
|
|
],
|
|
],
|
|
},
|
|
},
|
|
courseOutlineParsed: false,
|
|
courseOutlineParsed: false,
|
|
- // ai question result
|
|
|
|
- taskId: "",
|
|
|
|
- aiResult: "",
|
|
|
|
- aiThinkingResult: "",
|
|
|
|
|
|
+ // Data for multiple question generation
|
|
|
|
+ taskIds: [], // Array to store task IDs from each call
|
|
|
|
+ aiResults: [], // Array to store complete AI result string from each call
|
|
|
|
+ aiThinkingResults: [], // Array to store complete AI thinking result string from each call
|
|
|
|
+ // Temporary holders for the currently streaming question's data
|
|
|
|
+ currentStreamingAiResult: "",
|
|
|
|
+ currentStreamingAiThinkingResult: "",
|
|
|
|
+ currentStreamingTaskId: null,
|
|
|
|
+ // These are for the SseResultView components, showing concatenated results
|
|
|
|
+ aiResult: "", // Bound to SseResultView for AI results (cumulative)
|
|
|
|
+ // aiThinkingResult is already defined below
|
|
|
|
+ // ai question result (aiResult and aiThinkingResult are now cumulative display strings)
|
|
|
|
+ // taskIds and aiResults are now arrays defined above
|
|
|
|
+ aiThinkingResult: "", // For SseResultView for thinking (cumulative)
|
|
thinking: false,
|
|
thinking: false,
|
|
thinkVisible: true,
|
|
thinkVisible: true,
|
|
thinkDuration: "",
|
|
thinkDuration: "",
|
|
@@ -302,18 +311,29 @@ export default {
|
|
syllabusNotes: "",
|
|
syllabusNotes: "",
|
|
propertyIdList: [],
|
|
propertyIdList: [],
|
|
knowledgeNotes: "",
|
|
knowledgeNotes: "",
|
|
|
|
+ enableThinking: true,
|
|
};
|
|
};
|
|
},
|
|
},
|
|
handleOpened() {
|
|
handleOpened() {
|
|
this.formModel = this.getInitForm();
|
|
this.formModel = this.getInitForm();
|
|
this.getQuestionTypes();
|
|
this.getQuestionTypes();
|
|
- this.getInitForm();
|
|
|
|
|
|
+ // this.getInitForm(); // Called by this.formModel assignment already
|
|
this.checkCourseOutlineParsed();
|
|
this.checkCourseOutlineParsed();
|
|
|
|
+ // Clear previous results on open
|
|
|
|
+ this.aiResult = "";
|
|
|
|
+ this.aiThinkingResult = "";
|
|
|
|
+ this.taskIds = [];
|
|
|
|
+ this.aiResults = [];
|
|
|
|
+ this.aiThinkingResults = [];
|
|
|
|
+ this.thinkDuration = "";
|
|
|
|
+ this.thinking = false;
|
|
},
|
|
},
|
|
handleClose() {
|
|
handleClose() {
|
|
this.aiResult = "";
|
|
this.aiResult = "";
|
|
this.aiThinkingResult = "";
|
|
this.aiThinkingResult = "";
|
|
- this.taskId = "";
|
|
|
|
|
|
+ this.taskIds = [];
|
|
|
|
+ this.aiResults = [];
|
|
|
|
+ this.aiThinkingResults = [];
|
|
this.thinking = false;
|
|
this.thinking = false;
|
|
this.thinkDuration = "";
|
|
this.thinkDuration = "";
|
|
this.courseOutlineParsed = false;
|
|
this.courseOutlineParsed = false;
|
|
@@ -346,29 +366,50 @@ export default {
|
|
|
|
|
|
if (this.loading) return;
|
|
if (this.loading) return;
|
|
|
|
|
|
- this.stopStream(); // 清理前一个流
|
|
|
|
- this.aiResult = "";
|
|
|
|
- this.aiThinkingResult = "";
|
|
|
|
- this.thinkDuration = "";
|
|
|
|
- this.taskId = "";
|
|
|
|
|
|
+ this.stopStream(); // Clear any previous stream
|
|
|
|
+
|
|
|
|
+ // Initialize/clear result containers
|
|
|
|
+ this.aiResult = ""; // For cumulative display
|
|
|
|
+ this.aiThinkingResult = ""; // For cumulative display
|
|
|
|
+ this.taskIds = [];
|
|
|
|
+ this.aiResults = [];
|
|
|
|
+ this.aiThinkingResults = [];
|
|
|
|
+ this.thinkDuration = ""; // Reset global duration
|
|
|
|
+ // this.thinking will be set based on the first call
|
|
|
|
+
|
|
this.loading = true;
|
|
this.loading = true;
|
|
|
|
+ const totalQuestionsToGenerate = this.formModel.questionCount;
|
|
|
|
+ let overallStartThinkingTime = null;
|
|
|
|
|
|
- // 设置60秒超时
|
|
|
|
- const timeoutId = setTimeout(() => {
|
|
|
|
- this.controller.abort();
|
|
|
|
- this.$message.error("请求超时!");
|
|
|
|
- this.loading = false;
|
|
|
|
- }, 60000);
|
|
|
|
|
|
+ for (let i = 0; i < totalQuestionsToGenerate; i++) {
|
|
|
|
+ this.currentStreamingAiResult = "";
|
|
|
|
+ this.currentStreamingAiThinkingResult = "";
|
|
|
|
+ this.currentStreamingTaskId = null;
|
|
|
|
|
|
- try {
|
|
|
|
- this.controller = new AbortController();
|
|
|
|
- const res = await aiBuildQuestionApi(
|
|
|
|
- {
|
|
|
|
- ...this.formModel,
|
|
|
|
- courseId: this.courseInfo.id,
|
|
|
|
- rootOrgId: this.$store.state.user.rootOrgId,
|
|
|
|
- },
|
|
|
|
- {
|
|
|
|
|
|
+ const paramsForThisCall = {
|
|
|
|
+ ...this.formModel,
|
|
|
|
+ questionCount: 1, // Generate one question per call
|
|
|
|
+ enableThinking: i === 0 && this.formModel.enableThinking, // Only true for the first call if initially enabled
|
|
|
|
+ courseId: this.courseInfo.id,
|
|
|
|
+ rootOrgId: this.$store.state.user.rootOrgId,
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ if (i === 0 && paramsForThisCall.enableThinking) {
|
|
|
|
+ this.thinking = true; // Global thinking status on for the first question
|
|
|
|
+ overallStartThinkingTime = Date.now();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ let currentCallTimeoutId;
|
|
|
|
+ const timeoutPromise = new Promise((_, reject) => {
|
|
|
|
+ currentCallTimeoutId = setTimeout(() => {
|
|
|
|
+ this.controller?.abort(); // Abort the current controller
|
|
|
|
+ reject(new Error(`第 ${i + 1} 题请求超时!`));
|
|
|
|
+ }, 60000); // 60-second timeout per question
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ this.controller = new AbortController(); // New controller for each stream
|
|
|
|
+ const apiCallConfig = {
|
|
headers: {
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Content-Type": "application/json",
|
|
Accept: "text/event-stream",
|
|
Accept: "text/event-stream",
|
|
@@ -379,94 +420,143 @@ export default {
|
|
url: "/api/uq_basic/ai/question/stream/build",
|
|
url: "/api/uq_basic/ai/question/stream/build",
|
|
}),
|
|
}),
|
|
},
|
|
},
|
|
- signal: this.controller.signal, // 使用AbortController的signal
|
|
|
|
|
|
+ signal: this.controller.signal,
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const fetchPromise = aiBuildQuestionApi(
|
|
|
|
+ paramsForThisCall,
|
|
|
|
+ apiCallConfig
|
|
|
|
+ );
|
|
|
|
+ const res = await Promise.race([fetchPromise, timeoutPromise]);
|
|
|
|
+ if (res.status !== 200) {
|
|
|
|
+ throw new Error(`第 ${i + 1} 题请求失败!`);
|
|
}
|
|
}
|
|
- );
|
|
|
|
|
|
|
|
- // 请求成功后清除超时定时器
|
|
|
|
- clearTimeout(timeoutId);
|
|
|
|
- const startThinkingTime = Date.now();
|
|
|
|
- this.thinking = true;
|
|
|
|
|
|
+ clearTimeout(currentCallTimeoutId); // Clear timeout if fetch succeeded/failed fast enough
|
|
|
|
|
|
- const onEvent = (event) => {
|
|
|
|
- if (event.data === "[DONE]") {
|
|
|
|
- // console.log(this.aiResult);
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
- try {
|
|
|
|
- const parsed = JSON.parse(event.data);
|
|
|
|
- if (!this.taskId && parsed.taskId) {
|
|
|
|
- this.taskId = parsed.taskId;
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if (
|
|
|
|
- parsed.choices &&
|
|
|
|
- parsed.choices[0] &&
|
|
|
|
- parsed.choices[0].delta
|
|
|
|
- ) {
|
|
|
|
- const { content, reasoning_content } = parsed.choices[0].delta;
|
|
|
|
-
|
|
|
|
- // 只要content不为null,则表示思考结束,开始处理生成结果内容
|
|
|
|
- if (content !== null) {
|
|
|
|
- this.thinking = false;
|
|
|
|
-
|
|
|
|
- if (!this.thinkDuration) {
|
|
|
|
- this.thinkDuration = Math.round(
|
|
|
|
- (Date.now() - startThinkingTime) / 1000
|
|
|
|
- );
|
|
|
|
|
|
+ // Stream processing for the current question
|
|
|
|
+ await new Promise((resolveStream, rejectStream) => {
|
|
|
|
+ const onEvent = (event) => {
|
|
|
|
+ if (event.data === "[DONE]") {
|
|
|
|
+ this.aiResults.push(this.currentStreamingAiResult);
|
|
|
|
+ this.aiThinkingResults.push(
|
|
|
|
+ this.currentStreamingAiThinkingResult
|
|
|
|
+ );
|
|
|
|
+ if (this.currentStreamingTaskId) {
|
|
|
|
+ this.taskIds.push(this.currentStreamingTaskId);
|
|
|
|
+ }
|
|
|
|
+ // Append a separator for cumulative display if not the first question's result
|
|
|
|
+ if (i < totalQuestionsToGenerate - 1) {
|
|
|
|
+ // this.aiThinkingResult +=
|
|
|
|
+ // "\n<hr style='margin: 10px 0; border-top: 1px solid #eee;'/>\n";
|
|
|
|
+ this.aiResult +=
|
|
|
|
+ "\n<hr style='margin: 10px 0; border-top: 1px solid #eee;'/>\n";
|
|
}
|
|
}
|
|
|
|
|
|
- requestAnimationFrame(() => {
|
|
|
|
- this.aiResult += content || "";
|
|
|
|
- this.scrollToBottom();
|
|
|
|
- });
|
|
|
|
|
|
+ resolveStream();
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
- // 处理思考过程内容
|
|
|
|
- requestAnimationFrame(() => {
|
|
|
|
- this.aiThinkingResult += reasoning_content || "";
|
|
|
|
- this.scrollToBottom();
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
- } catch (e) {
|
|
|
|
- console.error(
|
|
|
|
- "Error parsing SSE JSON:",
|
|
|
|
- e,
|
|
|
|
- "Event data:",
|
|
|
|
- event.data
|
|
|
|
- );
|
|
|
|
- }
|
|
|
|
- };
|
|
|
|
- const onError = (error) => {
|
|
|
|
- this.thinking = false;
|
|
|
|
- console.error("Error parsing event:", error);
|
|
|
|
- };
|
|
|
|
|
|
+ try {
|
|
|
|
+ const parsed = JSON.parse(event.data);
|
|
|
|
+ if (!this.currentStreamingTaskId && parsed.taskId) {
|
|
|
|
+ this.currentStreamingTaskId = parsed.taskId;
|
|
|
|
+ }
|
|
|
|
|
|
- const parser = createParser({ onEvent, onError });
|
|
|
|
- this.parser = parser;
|
|
|
|
-
|
|
|
|
- // Pipe the response stream through TextDecoderStream and feed to parser
|
|
|
|
- const reader = res.body
|
|
|
|
- .pipeThrough(new TextDecoderStream())
|
|
|
|
- .getReader();
|
|
|
|
- // eslint-disable-next-line no-constant-condition
|
|
|
|
- while (true) {
|
|
|
|
- const { done, value } = await reader.read();
|
|
|
|
- if (done) {
|
|
|
|
- break;
|
|
|
|
- }
|
|
|
|
- this.parser.feed(value);
|
|
|
|
|
|
+ if (
|
|
|
|
+ parsed.choices &&
|
|
|
|
+ parsed.choices[0] &&
|
|
|
|
+ parsed.choices[0].delta
|
|
|
|
+ ) {
|
|
|
|
+ const { content, reasoning_content } =
|
|
|
|
+ parsed.choices[0].delta;
|
|
|
|
+
|
|
|
|
+ if (content !== null) {
|
|
|
|
+ if (i === 0 && this.thinking) {
|
|
|
|
+ // First call was thinking
|
|
|
|
+ this.thinking = false; // Global thinking UI flag off
|
|
|
|
+ if (!this.thinkDuration && overallStartThinkingTime) {
|
|
|
|
+ this.thinkDuration = Math.round(
|
|
|
|
+ (Date.now() - overallStartThinkingTime) / 1000
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ this.currentStreamingAiResult += content || "";
|
|
|
|
+ // Live update cumulative display (without extra HR, [DONE] handles that)
|
|
|
|
+ this.aiResult += content || "";
|
|
|
|
+ requestAnimationFrame(this.scrollToBottom);
|
|
|
|
+ } else if (reasoning_content !== null) {
|
|
|
|
+ this.currentStreamingAiThinkingResult +=
|
|
|
|
+ reasoning_content || "";
|
|
|
|
+ // Live update cumulative display
|
|
|
|
+ this.aiThinkingResult += reasoning_content || "";
|
|
|
|
+ requestAnimationFrame(this.scrollToBottom);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ } catch (e) {
|
|
|
|
+ console.error(
|
|
|
|
+ "Error parsing SSE JSON:",
|
|
|
|
+ e,
|
|
|
|
+ "Event data:",
|
|
|
|
+ event.data
|
|
|
|
+ );
|
|
|
|
+ // Do not reject stream here, parser's onError will handle it
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const onError = (error) => {
|
|
|
|
+ if (i === 0 && this.thinking) this.thinking = false;
|
|
|
|
+ console.error("Error parsing event:", error);
|
|
|
|
+ rejectStream(error); // Reject the promise for this stream
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ this.parser = createParser({ onEvent, onError });
|
|
|
|
+
|
|
|
|
+ const reader = res.body
|
|
|
|
+ .pipeThrough(new TextDecoderStream())
|
|
|
|
+ .getReader();
|
|
|
|
+ // IIFE to handle async reader loop and resolve/reject stream promise
|
|
|
|
+ (async () => {
|
|
|
|
+ try {
|
|
|
|
+ // eslint-disable-next-line no-constant-condition
|
|
|
|
+ while (true) {
|
|
|
|
+ const { done, value } = await reader.read();
|
|
|
|
+ if (done) break;
|
|
|
|
+ this.parser.feed(value);
|
|
|
|
+ }
|
|
|
|
+ // If stream ends without [DONE] but no error, resolveStream might not have been called.
|
|
|
|
+ // However, createParser's onEvent for [DONE] should handle normal completion.
|
|
|
|
+ } catch (readError) {
|
|
|
|
+ rejectStream(readError);
|
|
|
|
+ }
|
|
|
|
+ })();
|
|
|
|
+ });
|
|
|
|
+ } catch (err) {
|
|
|
|
+ console.error(`流式处理错误 (第 ${i + 1} 题):`, err);
|
|
|
|
+ if (i === 0 && this.thinking) this.thinking = false;
|
|
|
|
+ clearTimeout(currentCallTimeoutId); // Clear timeout if any for this iteration
|
|
|
|
+ this.$message.error(err.message || `生成第 ${i + 1} 题时出错`);
|
|
|
|
+ this.controller?.abort(); // Abort current controller before breaking
|
|
|
|
+ this.controller = null;
|
|
|
|
+ break; // Stop generating further questions if one fails
|
|
|
|
+ }
|
|
|
|
+ } // End for loop
|
|
|
|
+
|
|
|
|
+ this.loading = false;
|
|
|
|
+ this.controller = null; // Ensure controller is reset finally
|
|
|
|
+ // If loop completed fully and thinking was active for the first question but no content came to turn it off
|
|
|
|
+ if (
|
|
|
|
+ totalQuestionsToGenerate > 0 &&
|
|
|
|
+ this.formModel.enableThinking &&
|
|
|
|
+ this.thinking
|
|
|
|
+ ) {
|
|
|
|
+ this.thinking = false; // Ensure thinking is off if loop finishes
|
|
|
|
+ if (!this.thinkDuration && overallStartThinkingTime) {
|
|
|
|
+ // Calculate duration if not set
|
|
|
|
+ this.thinkDuration = Math.round(
|
|
|
|
+ (Date.now() - overallStartThinkingTime) / 1000
|
|
|
|
+ );
|
|
}
|
|
}
|
|
- } catch (err) {
|
|
|
|
- console.error("流式处理错误:", err);
|
|
|
|
- this.thinking = false;
|
|
|
|
- // 请求出错时清除超时定时器
|
|
|
|
- clearTimeout(timeoutId);
|
|
|
|
- } finally {
|
|
|
|
- this.loading = false;
|
|
|
|
- this.controller = null; // Ensure controller is reset
|
|
|
|
}
|
|
}
|
|
},
|
|
},
|
|
stopStream() {
|
|
stopStream() {
|
|
@@ -476,22 +566,36 @@ export default {
|
|
this.loading = false;
|
|
this.loading = false;
|
|
},
|
|
},
|
|
async saveQuestions() {
|
|
async saveQuestions() {
|
|
- if (this.loading) return;
|
|
|
|
|
|
+ if (this.loading || this.aiResults.length === 0) {
|
|
|
|
+ if (this.aiResults.length === 0 && !this.loading) {
|
|
|
|
+ this.$message.info("没有可保存的试题");
|
|
|
|
+ }
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
this.loading = true;
|
|
this.loading = true;
|
|
|
|
+
|
|
const res = await aiBuildQuestionSaveApi({
|
|
const res = await aiBuildQuestionSaveApi({
|
|
- aiResult: this.aiResult,
|
|
|
|
- taskId: this.taskId,
|
|
|
|
|
|
+ aiResults: this.aiResults,
|
|
|
|
+ taskIds: this.taskIds, // Assuming taskIds array matches aiResults
|
|
}).catch(() => {});
|
|
}).catch(() => {});
|
|
this.loading = false;
|
|
this.loading = false;
|
|
- if (!res) return;
|
|
|
|
- this.$message.success("保存成功");
|
|
|
|
|
|
+ if (!res) {
|
|
|
|
+ this.$message.error(`保存失败`);
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ this.$message.success(`保存成功`);
|
|
this.$emit("modified");
|
|
this.$emit("modified");
|
|
this.close();
|
|
this.close();
|
|
},
|
|
},
|
|
clearPreview() {
|
|
clearPreview() {
|
|
this.aiResult = "";
|
|
this.aiResult = "";
|
|
this.aiThinkingResult = "";
|
|
this.aiThinkingResult = "";
|
|
- this.taskId = "";
|
|
|
|
|
|
+ this.taskIds = [];
|
|
|
|
+ this.aiResults = [];
|
|
|
|
+ this.aiThinkingResults = [];
|
|
|
|
+ this.thinkDuration = "";
|
|
|
|
+ this.thinking = false;
|
|
},
|
|
},
|
|
},
|
|
},
|
|
beforeDestroy() {
|
|
beforeDestroy() {
|