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 _rawParsedQuestionXmls = new(); // 4. 将 _rawParsedQuestionXmls 转换后的 QuestionGroup 对象列表 private List _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(_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(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 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 questionGroups) { var createDto = new ExamDto(); createDto.QuestionGroups = MapQuestionGroupsToDto(questionGroups); // 如果您需要为这次保存指定一个AssignmentId,可以在这里设置 // createDto.AssignmentId = YourCurrentAssignmentId; return createDto; } private List MapQuestionGroupsToDto(List qgs) { var dtos = new List(); 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 MapSubQuestionsToDto(List sqs) { var dtos = new List(); 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("import", "./scripts/jsTools.js"); } private async Task CopyToClipboard() { try { // 调用 JavaScript 函数 bool success = await jSObjectReference.InvokeAsync("copyTextToClipboard", AIConfiguration.ParseSignelQuestion2); if (success) { Snackbar.Add("文本已成功复制到剪贴板!"); } else { Snackbar.Add("复制文本到剪贴板失败。"); } } catch (Exception ex) { Snackbar.Add($"发生错误: {ex.Message}"); Console.WriteLine($"复制到剪贴板时出错: {ex.Message}"); } } #endregion } }