Files
TechHelper/TechHelper.Client/Pages/Editor/EditorMain.razor.cs
SpecialX e824c081bf change
2025-05-30 12:46:55 +08:00

561 lines
16 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 为 trueResult 也可能不是预期的类型
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
}
}