561 lines
16 KiB
C#
561 lines
16 KiB
C#
using Blazored.TextEditor;
|
||
using Entities.Contracts;
|
||
using Entities.DTO;
|
||
using Microsoft.AspNetCore.Components;
|
||
using Microsoft.JSInterop;
|
||
using MudBlazor;
|
||
using System.Text.RegularExpressions;
|
||
using TechHelper.Client.AI;
|
||
using TechHelper.Client.Exam;
|
||
using TechHelper.Services;
|
||
using static Org.BouncyCastle.Crypto.Engines.SM2Engine;
|
||
|
||
namespace TechHelper.Client.Pages.Editor
|
||
{
|
||
public enum ProcessingStage
|
||
{
|
||
Idle, // 初始或完成状态
|
||
FetchingContent, // 正在获取编辑器内容
|
||
DividingExam, // 正在分割题组 (AI返回原始XML)
|
||
ConvertingDividedXml, // 正在将分割XML转换为StringsList
|
||
ParsingGroups, // 正在解析单个题组 (AI返回原始XML)
|
||
ConvertingParsedXmls, // 正在将解析后的XML转换为QuestionGroup对象
|
||
Completed, // 所有处理完成
|
||
ErrorOccurred, // 发生错误
|
||
Saving
|
||
}
|
||
|
||
public partial class EditorMain
|
||
{
|
||
[Inject]
|
||
public IExamService ExamService { get; set; } = default!;
|
||
[Inject]
|
||
public ISnackbar Snackbar { get; set; } = default!;
|
||
|
||
// --- UI 绑定和状态变量 ---
|
||
private BlazoredTextEditor? _quillHtmlEditor; // 富文本编辑器实例
|
||
private string _editorHtmlContent = string.Empty; // 存储编辑器获取到的 HTML/文本内容
|
||
private bool _isProcessing = false; // 控制加载状态的标志
|
||
private string _processingStatusMessage = "等待操作..."; // 显示给用户的当前处理状态信息
|
||
|
||
// --- 内部数据结构 (存储每一步的结果,包括原始文本) ---
|
||
// 1. AI 分割原始响应 (XML 文本)
|
||
private string? _rawDividedExamXmlContent;
|
||
// 2. 将 _rawDividedExamXmlContent 转换后的 StringsList
|
||
private StringsList? _dividedQuestionGroupList;
|
||
|
||
// 3. AI 解析每个题组后的原始响应 (XML 文本列表)
|
||
private List<string> _rawParsedQuestionXmls = new();
|
||
// 4. 将 _rawParsedQuestionXmls 转换后的 QuestionGroup 对象列表
|
||
private List<QuestionGroup> _finalQuestionGroups = new();
|
||
|
||
|
||
// --- Blazor 生命周期方法 ---
|
||
protected override void OnInitialized()
|
||
{
|
||
_processingStatusMessage = ProcessingStage.Idle.ToString();
|
||
}
|
||
|
||
// --- 辅助方法 ---
|
||
|
||
// 统一处理错误并更新 UI
|
||
private void HandleProcessError(string errorMessage, Exception? ex = null)
|
||
{
|
||
_processingStatusMessage = $"{ProcessingStage.ErrorOccurred}: {errorMessage}";
|
||
Snackbar.Add(errorMessage, Severity.Error);
|
||
_isProcessing = false;
|
||
StateHasChanged();
|
||
if (ex != null)
|
||
{
|
||
// 记录更详细的错误日志到控制台或日志系统
|
||
Console.Error.WriteLine($"错误详情: {ex.Message}\n{ex.StackTrace}\n内部异常: {ex.InnerException?.Message}");
|
||
}
|
||
}
|
||
|
||
// 更新处理状态和 UI
|
||
private void UpdateProcessingStatus(ProcessingStage stage, string message)
|
||
{
|
||
_processingStatusMessage = $"{stage}: {message}";
|
||
Snackbar.Add(message, Severity.Info);
|
||
StateHasChanged();
|
||
}
|
||
|
||
// --- 公共工具方法 (可手动触发,用于获取编辑器内容) ---
|
||
|
||
// 获取编辑器 HTML 内容
|
||
public async Task GetEditorHtmlContentAsync()
|
||
{
|
||
if (_isProcessing) return;
|
||
_isProcessing = true;
|
||
UpdateProcessingStatus(ProcessingStage.FetchingContent, "正在获取编辑器HTML内容...");
|
||
|
||
try
|
||
{
|
||
if (_quillHtmlEditor != null)
|
||
{
|
||
_editorHtmlContent = await _quillHtmlEditor.GetHTML();
|
||
UpdateProcessingStatus(ProcessingStage.FetchingContent, "编辑器HTML内容已成功获取。");
|
||
}
|
||
else
|
||
{
|
||
HandleProcessError("编辑器实例未准备好,无法获取内容。");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
HandleProcessError($"获取编辑器内容时发生错误: {ex.Message}", ex);
|
||
}
|
||
finally
|
||
{
|
||
_isProcessing = false;
|
||
StateHasChanged();
|
||
}
|
||
}
|
||
|
||
// 获取编辑器纯文本内容 (根据需求决定是否保留)
|
||
public async Task GetEditorTextContentAsync()
|
||
{
|
||
if (_isProcessing) return;
|
||
_isProcessing = true;
|
||
UpdateProcessingStatus(ProcessingStage.FetchingContent, "正在获取编辑器纯文本内容...");
|
||
|
||
try
|
||
{
|
||
if (_quillHtmlEditor != null)
|
||
{
|
||
_editorHtmlContent = await _quillHtmlEditor.GetText();
|
||
UpdateProcessingStatus(ProcessingStage.FetchingContent, "编辑器纯文本内容已成功获取。");
|
||
}
|
||
else
|
||
{
|
||
HandleProcessError("编辑器实例未准备好,无法获取内容。");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
HandleProcessError($"获取编辑器纯文本内容时发生错误: {ex.Message}", ex);
|
||
}
|
||
finally
|
||
{
|
||
_isProcessing = false;
|
||
StateHasChanged();
|
||
}
|
||
}
|
||
|
||
|
||
// --- 核心业务逻辑步骤 (每个步骤都可手动触发) ---
|
||
|
||
// 步骤 1: 调用 AI 服务分割题组 (只获取原始 XML 文本)
|
||
public async Task DivideExamContentByAIAsync()
|
||
{
|
||
if (_isProcessing) return;
|
||
_isProcessing = true;
|
||
// 清空所有依赖于此步骤的结果
|
||
_rawDividedExamXmlContent = null;
|
||
_dividedQuestionGroupList = null;
|
||
_rawParsedQuestionXmls.Clear();
|
||
_finalQuestionGroups.Clear();
|
||
|
||
UpdateProcessingStatus(ProcessingStage.DividingExam, "正在请求 AI 分割题组,请等待...");
|
||
|
||
if (string.IsNullOrWhiteSpace(_editorHtmlContent))
|
||
{
|
||
HandleProcessError("编辑器内容为空,请先获取内容。");
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
var response = await ExamService.DividExam(_editorHtmlContent);
|
||
if (!response.Status)
|
||
{
|
||
HandleProcessError(response.Message ?? "AI题组分割失败。", response.Result as Exception);
|
||
return;
|
||
}
|
||
|
||
// **仅保存原始的 XML 文本,不在此步进行转换**
|
||
_rawDividedExamXmlContent = response.Result as string;
|
||
|
||
if (string.IsNullOrWhiteSpace(_rawDividedExamXmlContent))
|
||
{
|
||
HandleProcessError("AI 服务返回的分割内容为空。");
|
||
return;
|
||
}
|
||
UpdateProcessingStatus(ProcessingStage.DividingExam, response.Message ?? "AI题组分割成功,原始XML已保存。");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
HandleProcessError($"分割题组时发生错误: {ex.Message}", ex);
|
||
}
|
||
finally
|
||
{
|
||
_isProcessing = false;
|
||
StateHasChanged();
|
||
}
|
||
}
|
||
|
||
// 步骤 2: 将原始分割 XML 文本转换为 StringsList
|
||
public void ConvertDividedXmlToQuestionList()
|
||
{
|
||
if (_isProcessing) return; // 这是一个同步方法,但为了UI禁用按钮,仍检查
|
||
_isProcessing = true;
|
||
_dividedQuestionGroupList = null; // 清空上次结果
|
||
_rawParsedQuestionXmls.Clear();
|
||
_finalQuestionGroups.Clear();
|
||
|
||
UpdateProcessingStatus(ProcessingStage.ConvertingDividedXml, "正在将分割XML转换为题组列表...");
|
||
|
||
if (string.IsNullOrWhiteSpace(_rawDividedExamXmlContent))
|
||
{
|
||
HandleProcessError("没有原始分割XML文本可供转换。请先执行 '分割题组 (AI)' 步骤。");
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
// 调用 ExamService 的同步方法进行转换
|
||
var xmlConversionResponse = ExamService.ConvertToXML<StringsList>(_rawDividedExamXmlContent);
|
||
|
||
if (!xmlConversionResponse.Status)
|
||
{
|
||
// 如果从原始XML转换为StringsList失败,但原始XML仍然保留
|
||
HandleProcessError(xmlConversionResponse.Message ?? "AI返回的XML无法转换为题组列表。", xmlConversionResponse.Result as Exception);
|
||
return;
|
||
}
|
||
|
||
_dividedQuestionGroupList = xmlConversionResponse.Result as StringsList;
|
||
if (_dividedQuestionGroupList == null || !_dividedQuestionGroupList.Items.Any())
|
||
{
|
||
HandleProcessError("AI 服务返回的XML已转换,但题组列表为空。");
|
||
return;
|
||
}
|
||
UpdateProcessingStatus(ProcessingStage.ConvertingDividedXml, "分割XML已成功转换为题组列表。");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
HandleProcessError($"转换分割XML到列表时发生错误: {ex.Message}", ex);
|
||
}
|
||
finally
|
||
{
|
||
_isProcessing = false;
|
||
StateHasChanged();
|
||
}
|
||
}
|
||
|
||
|
||
// 步骤 3: 循环调用 AI 服务解析每个分割后的题组 (只获取原始 XML 文本)
|
||
public async Task ParseEachQuestionGroupAsync()
|
||
{
|
||
if (_isProcessing) return;
|
||
_isProcessing = true;
|
||
_rawParsedQuestionXmls.Clear(); // 清空上次结果
|
||
_finalQuestionGroups.Clear();
|
||
|
||
UpdateProcessingStatus(ProcessingStage.ParsingGroups, "正在解析每个题组,请等待...");
|
||
|
||
if (_dividedQuestionGroupList == null || !_dividedQuestionGroupList.Items.Any())
|
||
{
|
||
HandleProcessError("没有可解析的题组列表。请先执行 '转换为题组列表' 步骤。");
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
int currentGroupIndex = 1;
|
||
foreach (var itemXml in _dividedQuestionGroupList.Items)
|
||
{
|
||
UpdateProcessingStatus(ProcessingStage.ParsingGroups, $"正在解析第 {currentGroupIndex} 个题组...");
|
||
|
||
var parseResponse = await ExamService.ParseSingleQuestionGroup(itemXml);
|
||
if (!parseResponse.Status)
|
||
{
|
||
// 即使解析失败,原始的 _dividedQuestionGroupList 仍然保留
|
||
HandleProcessError($"解析第 {currentGroupIndex} 个题组失败: {parseResponse.Message}", parseResponse.Result as Exception);
|
||
// 可以选择跳过当前失败的项并继续,这里选择停止
|
||
return;
|
||
}
|
||
// **仅保存原始的 XML 文本,不在此步进行转换**
|
||
_rawParsedQuestionXmls.Add(parseResponse.Result as string ?? string.Empty);
|
||
UpdateProcessingStatus(ProcessingStage.ParsingGroups, $"第 {currentGroupIndex} 个题组解析成功。");
|
||
currentGroupIndex++;
|
||
}
|
||
UpdateProcessingStatus(ProcessingStage.ParsingGroups, "所有题组已成功解析。原始XML已保存。");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
HandleProcessError($"解析题组时发生错误: {ex.Message}", ex);
|
||
}
|
||
finally
|
||
{
|
||
_isProcessing = false;
|
||
StateHasChanged();
|
||
}
|
||
}
|
||
|
||
// 步骤 4: 将原始解析 XML 文本转换为 QuestionGroup 对象
|
||
public void ConvertParsedXmlsToQuestionGroups()
|
||
{
|
||
if (_isProcessing) return; // 这是一个同步方法
|
||
_isProcessing = true;
|
||
_finalQuestionGroups.Clear(); // 清空上次结果
|
||
|
||
UpdateProcessingStatus(ProcessingStage.ConvertingParsedXmls, "正在将解析后的XML转换为对象,请等待...");
|
||
|
||
if (!_rawParsedQuestionXmls.Any())
|
||
{
|
||
HandleProcessError("没有可转换的原始解析XML数据。请先执行 '解析每个题组 (AI)' 步骤。");
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
foreach (var xmlString in _rawParsedQuestionXmls)
|
||
{
|
||
// 调用 ExamService 的同步方法进行转换
|
||
ApiResponse xmlConversionResponse = ExamService.ConvertToXML<QuestionGroup>(xmlString);
|
||
|
||
if (!xmlConversionResponse.Status)
|
||
{
|
||
// 如果单个转换失败,但原始XML仍然保留
|
||
HandleProcessError($"XML 转换为 QuestionGroup 失败: {xmlConversionResponse.Message}", xmlConversionResponse.Result as Exception);
|
||
// 可以选择跳过当前失败的项并继续,这里选择停止
|
||
return;
|
||
}
|
||
|
||
QuestionGroup? questionGroup = xmlConversionResponse.Result as QuestionGroup;
|
||
if (questionGroup != null)
|
||
{
|
||
_finalQuestionGroups.Add(questionGroup);
|
||
}
|
||
else
|
||
{
|
||
// 即使 Status 为 true,Result 也可能不是预期的类型
|
||
HandleProcessError("XML 转换成功,但返回结果类型不匹配 QuestionGroup。");
|
||
return;
|
||
}
|
||
}
|
||
UpdateProcessingStatus(ProcessingStage.ConvertingParsedXmls, "所有题组的XML已成功转换为对象。");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
HandleProcessError($"转换解析XML到对象时发生错误: {ex.Message}", ex);
|
||
}
|
||
finally
|
||
{
|
||
if (_finalQuestionGroups.Count > 0)
|
||
OrderQuestionGroup(_finalQuestionGroups);
|
||
_isProcessing = false;
|
||
StateHasChanged();
|
||
}
|
||
}
|
||
|
||
|
||
|
||
private void OrderQuestionGroup(List<QuestionGroup> QG)
|
||
{
|
||
int index = 1;
|
||
QG.ForEach(qg =>
|
||
{
|
||
qg.Id = (byte)index++;
|
||
int sqIndex = 1;
|
||
qg.SubQuestions.ForEach(sq => sq.SubId = (byte)sqIndex++);
|
||
|
||
});
|
||
|
||
QG.ForEach(qg => OrderQuestionGroup(qg.SubQuestionGroups));
|
||
}
|
||
|
||
private ExamDto MapToCreateExamDto(List<QuestionGroup> questionGroups)
|
||
{
|
||
var createDto = new ExamDto();
|
||
createDto.QuestionGroups = MapQuestionGroupsToDto(questionGroups);
|
||
// 如果您需要为这次保存指定一个AssignmentId,可以在这里设置
|
||
// createDto.AssignmentId = YourCurrentAssignmentId;
|
||
return createDto;
|
||
}
|
||
|
||
private List<QuestionGroupDto> MapQuestionGroupsToDto(List<QuestionGroup> qgs)
|
||
{
|
||
var dtos = new List<QuestionGroupDto>();
|
||
foreach (var qg in qgs)
|
||
{
|
||
var qgDto = new QuestionGroupDto
|
||
{
|
||
Title = qg.Title,
|
||
Score = qg.Score,
|
||
QuestionReference = qg.QuestionReference,
|
||
SubQuestions = MapSubQuestionsToDto(qg.SubQuestions),
|
||
SubQuestionGroups = MapQuestionGroupsToDto(qg.SubQuestionGroups)
|
||
};
|
||
dtos.Add(qgDto);
|
||
}
|
||
return dtos;
|
||
}
|
||
|
||
private List<SubQuestionDto> MapSubQuestionsToDto(List<SubQuestion> sqs)
|
||
{
|
||
var dtos = new List<SubQuestionDto>();
|
||
foreach (var sq in sqs)
|
||
{
|
||
var sqDto = new SubQuestionDto
|
||
{
|
||
Index = sq.SubId,
|
||
Stem = sq.Stem,
|
||
Score = sq.Score,
|
||
SampleAnswer = sq.SampleAnswer,
|
||
Options = sq.Options.Select(o => new OptionDto { Value = o.Value }).ToList(),
|
||
// TODO: 假设这些值能从AI结果或某个默认配置中获取
|
||
// 例如:QuestionType = "SingleChoice",
|
||
// DifficultyLevel = "Medium",
|
||
// SubjectArea = "Math"
|
||
};
|
||
dtos.Add(sqDto);
|
||
}
|
||
return dtos;
|
||
}
|
||
|
||
public async Task Save()
|
||
{
|
||
if (_isProcessing) return;
|
||
_isProcessing = true;
|
||
UpdateProcessingStatus(ProcessingStage.Saving, "正在准备发送试题数据到服务器...");
|
||
|
||
if (!_finalQuestionGroups.Any())
|
||
{
|
||
HandleProcessError("没有可保存的最终题组数据。请先完成前面的所有解析步骤。");
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
// 确保 _finalQuestionGroups 已经按照 Index 排序 (这在之前的 ConvertParsedXmlsToQuestionGroups 之后或 Save 开始时完成)
|
||
_finalQuestionGroups = _finalQuestionGroups.OrderBy(qg => qg.Id).ToList();
|
||
|
||
// 映射到 DTO
|
||
var createExamDto = MapToCreateExamDto(_finalQuestionGroups);
|
||
|
||
// 调用 ExamService 发送 DTO
|
||
// 假设 ExamService 有一个方法来接收这个 DTO
|
||
var response = await ExamService.SaveParsedExam(createExamDto);
|
||
|
||
if (!response.Status)
|
||
{
|
||
HandleProcessError(response.Message ?? "保存试题到服务器失败。", response.Result as Exception);
|
||
return;
|
||
}
|
||
|
||
UpdateProcessingStatus(ProcessingStage.Saving, response.Message ?? "试题数据已成功保存到服务器。");
|
||
Snackbar.Add("试题数据已成功保存。", Severity.Success);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
HandleProcessError($"发送数据到服务器时发生错误: {ex.Message}", ex);
|
||
}
|
||
finally
|
||
{
|
||
_isProcessing = false;
|
||
StateHasChanged();
|
||
}
|
||
}
|
||
|
||
|
||
|
||
// --- 全自动流程 ---
|
||
|
||
// 触发 AI 题组解析的完整流程 (全自动)
|
||
public async Task TriggerFullAIParsingProcessAsync()
|
||
{
|
||
if (_isProcessing) return;
|
||
_isProcessing = true; // 开始加载状态
|
||
// 全局清空所有结果
|
||
_rawDividedExamXmlContent = null;
|
||
_dividedQuestionGroupList = null;
|
||
_rawParsedQuestionXmls.Clear();
|
||
_finalQuestionGroups.Clear();
|
||
|
||
UpdateProcessingStatus(ProcessingStage.Idle, "开始全自动解析流程...");
|
||
|
||
try
|
||
{
|
||
// 1. 获取编辑器内容
|
||
await GetEditorTextContentAsync();
|
||
if (_processingStatusMessage.Contains(ProcessingStage.ErrorOccurred.ToString())) return;
|
||
|
||
// 2. 调用 AI 分割题组 (获取原始 XML)
|
||
await DivideExamContentByAIAsync();
|
||
if (_processingStatusMessage.Contains(ProcessingStage.ErrorOccurred.ToString())) return;
|
||
|
||
// 3. 转换分割 XML 为 StringsList
|
||
ConvertDividedXmlToQuestionList(); // 注意这里是同步调用
|
||
if (_processingStatusMessage.Contains(ProcessingStage.ErrorOccurred.ToString())) return;
|
||
|
||
// 4. 循环解析每个题组 (获取原始 XML)
|
||
await ParseEachQuestionGroupAsync();
|
||
if (_processingStatusMessage.Contains(ProcessingStage.ErrorOccurred.ToString())) return;
|
||
|
||
// 5. 转换解析 XML 为 QuestionGroup 对象
|
||
ConvertParsedXmlsToQuestionGroups(); // 注意这里是同步调用
|
||
if (_processingStatusMessage.Contains(ProcessingStage.ErrorOccurred.ToString())) return;
|
||
|
||
UpdateProcessingStatus(ProcessingStage.Completed, "全自动题组解析流程已全部完成!");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
HandleProcessError($"全自动解析流程中发生未预期错误: {ex.Message}", ex);
|
||
}
|
||
finally
|
||
{
|
||
_isProcessing = false; // 结束加载状态
|
||
StateHasChanged();
|
||
}
|
||
}
|
||
|
||
|
||
|
||
private void DeleteFromParse(int index)
|
||
{
|
||
if (index >= 0 && index < _finalQuestionGroups.Count)
|
||
{
|
||
_finalQuestionGroups.RemoveAt(index);
|
||
StateHasChanged();
|
||
}
|
||
}
|
||
|
||
|
||
#region JS
|
||
[Inject]
|
||
public IJSRuntime JSRuntime { get; set; }
|
||
|
||
private IJSObjectReference jSObjectReference { get; set; }
|
||
protected override async Task OnInitializedAsync()
|
||
{
|
||
jSObjectReference = await JSRuntime.InvokeAsync<IJSObjectReference>("import",
|
||
"./scripts/jsTools.js");
|
||
}
|
||
|
||
private async Task CopyToClipboard()
|
||
{
|
||
try
|
||
{
|
||
// 调用 JavaScript 函数
|
||
bool success = await jSObjectReference.InvokeAsync<bool>("copyTextToClipboard", AIConfiguration.ParseSignelQuestion2);
|
||
|
||
if (success)
|
||
{
|
||
Snackbar.Add("文本已成功复制到剪贴板!");
|
||
}
|
||
else
|
||
{
|
||
Snackbar.Add("复制文本到剪贴板失败。");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"发生错误: {ex.Message}");
|
||
Console.WriteLine($"复制到剪贴板时出错: {ex.Message}");
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
}
|
||
} |