|
@@ -0,0 +1,322 @@
|
|
|
+import { Component } from '@angular/core';
|
|
|
+import {
|
|
|
+ IonHeader,
|
|
|
+ IonToolbar,
|
|
|
+ IonTitle,
|
|
|
+ IonContent,
|
|
|
+ IonList,
|
|
|
+ IonItem,
|
|
|
+ IonLabel,
|
|
|
+ IonButton,
|
|
|
+ IonButtons,
|
|
|
+ IonInput,
|
|
|
+ IonRadio,
|
|
|
+ IonRadioGroup,
|
|
|
+ IonCheckbox
|
|
|
+} from '@ionic/angular/standalone';
|
|
|
+import { NgFor, NgIf } from '@angular/common';
|
|
|
+import { FormsModule } from '@angular/forms';
|
|
|
+import { AgentTaskStep } from '../../agent.task';
|
|
|
+import { ModalController } from '@ionic/angular/standalone';
|
|
|
+import { ApiService } from '../../../app/services/api.service';
|
|
|
+import { GenerateAnswerOptions } from './types';
|
|
|
+import { FmodeChatCompletion } from 'fmode-ng';
|
|
|
+
|
|
|
+// 定义练习题类型
|
|
|
+interface Exercise {
|
|
|
+ type: 'single' | 'multiple' | 'blank';
|
|
|
+ question: string;
|
|
|
+ options?: string[];
|
|
|
+ userAnswer?: string;
|
|
|
+ answer?: string;
|
|
|
+ explanation?: string;
|
|
|
+}
|
|
|
+
|
|
|
+// 创建练习题生成和作答的模态框组件
|
|
|
+@Component({
|
|
|
+ selector: 'app-exercise-modal',
|
|
|
+ template: `
|
|
|
+ <ion-header>
|
|
|
+ <ion-toolbar>
|
|
|
+ <ion-title>{{ currentIndex + 1 }}/{{ exercises.length }}</ion-title>
|
|
|
+ <ion-buttons slot="end">
|
|
|
+ <ion-button (click)="dismiss()">关闭</ion-button>
|
|
|
+ </ion-buttons>
|
|
|
+ </ion-toolbar>
|
|
|
+ </ion-header>
|
|
|
+
|
|
|
+ <ion-content class="ion-padding">
|
|
|
+ <div *ngIf="currentExercise">
|
|
|
+ <!-- 题目内容 -->
|
|
|
+ <h2>{{ currentExercise.question }}</h2>
|
|
|
+
|
|
|
+ <!-- 选择题选项 -->
|
|
|
+ <ion-list *ngIf="currentExercise.type === 'single' || currentExercise.type === 'multiple'">
|
|
|
+ <ion-radio-group [(ngModel)]="currentExercise.userAnswer" *ngIf="currentExercise.type === 'single'">
|
|
|
+ <ion-item *ngFor="let option of currentExercise.options; let i = index">
|
|
|
+ <ion-radio [value]="option">{{ option }}</ion-radio>
|
|
|
+ </ion-item>
|
|
|
+ </ion-radio-group>
|
|
|
+
|
|
|
+ <ion-list *ngIf="currentExercise.type === 'multiple'">
|
|
|
+ <ion-item *ngFor="let option of currentExercise.options; let i = index">
|
|
|
+ <ion-checkbox [(ngModel)]="selectedOptions[i]">{{ option }}</ion-checkbox>
|
|
|
+ </ion-item>
|
|
|
+ </ion-list>
|
|
|
+ </ion-list>
|
|
|
+
|
|
|
+ <!-- 填空题答案 -->
|
|
|
+ <ion-item *ngIf="currentExercise.type === 'blank'">
|
|
|
+ <ion-input [(ngModel)]="currentExercise.userAnswer"
|
|
|
+ placeholder="请输入答案"></ion-input>
|
|
|
+ </ion-item>
|
|
|
+
|
|
|
+ <!-- 导航按钮 -->
|
|
|
+ <div class="navigation-buttons">
|
|
|
+ <ion-button (click)="previousQuestion()"
|
|
|
+ [disabled]="currentIndex === 0">
|
|
|
+ 上一题
|
|
|
+ </ion-button>
|
|
|
+ <ion-button (click)="nextQuestion()"
|
|
|
+ [disabled]="currentIndex === exercises.length - 1">
|
|
|
+ 下一题
|
|
|
+ </ion-button>
|
|
|
+ <ion-button (click)="submit()"
|
|
|
+ *ngIf="currentIndex === exercises.length - 1"
|
|
|
+ [disabled]="!isAllAnswered()">
|
|
|
+ 提交
|
|
|
+ </ion-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </ion-content>
|
|
|
+ `,
|
|
|
+ styles: [`
|
|
|
+ .navigation-buttons {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-top: 20px;
|
|
|
+ }
|
|
|
+ `],
|
|
|
+ standalone: true,
|
|
|
+ imports: [
|
|
|
+ IonHeader,
|
|
|
+ IonToolbar,
|
|
|
+ IonTitle,
|
|
|
+ IonContent,
|
|
|
+ IonList,
|
|
|
+ IonItem,
|
|
|
+ IonLabel,
|
|
|
+ IonButton,
|
|
|
+ IonButtons,
|
|
|
+ IonInput,
|
|
|
+ IonRadio,
|
|
|
+ IonRadioGroup,
|
|
|
+ IonCheckbox,
|
|
|
+ NgFor,
|
|
|
+ NgIf,
|
|
|
+ FormsModule
|
|
|
+ ]
|
|
|
+})
|
|
|
+class ExerciseModalComponent {
|
|
|
+ exercises: Exercise[] = [];
|
|
|
+ currentIndex: number = 0;
|
|
|
+ selectedOptions: boolean[] = [];
|
|
|
+
|
|
|
+ get currentExercise() {
|
|
|
+ return this.exercises[this.currentIndex];
|
|
|
+ }
|
|
|
+
|
|
|
+ constructor(
|
|
|
+ private modalCtrl: ModalController
|
|
|
+ ) {}
|
|
|
+
|
|
|
+ previousQuestion() {
|
|
|
+ if (this.currentIndex > 0) {
|
|
|
+ this.currentIndex--;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ nextQuestion() {
|
|
|
+ if (this.currentIndex < this.exercises.length - 1) {
|
|
|
+ this.currentIndex++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ isAllAnswered(): boolean {
|
|
|
+ return this.exercises.every(exercise => exercise.userAnswer);
|
|
|
+ }
|
|
|
+
|
|
|
+ dismiss() {
|
|
|
+ this.modalCtrl.dismiss();
|
|
|
+ }
|
|
|
+
|
|
|
+ submit() {
|
|
|
+ // 处理多选题答案
|
|
|
+ this.exercises.forEach(exercise => {
|
|
|
+ if (exercise.type === 'multiple') {
|
|
|
+ exercise.userAnswer = exercise.options
|
|
|
+ ?.filter((_: string, i: number) => this.selectedOptions[i])
|
|
|
+ .join(', ');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ this.modalCtrl.dismiss({
|
|
|
+ exercises: this.exercises
|
|
|
+ });
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 修改 ApiService 接口
|
|
|
+interface ChatResponse {
|
|
|
+ content: string;
|
|
|
+}
|
|
|
+
|
|
|
+// 生成题目和作答任务
|
|
|
+export function TaskGenerateAndAnswer(options: GenerateAnswerOptions): AgentTaskStep {
|
|
|
+ const task = new AgentTaskStep({
|
|
|
+ title: "生成题目和作答",
|
|
|
+ shareData: options.shareData
|
|
|
+ });
|
|
|
+
|
|
|
+ // 添加辅助函数来验证JSON格式
|
|
|
+ const validateExercises = (content: string): Exercise[] => {
|
|
|
+ try {
|
|
|
+ const exercises = JSON.parse(content);
|
|
|
+
|
|
|
+ // 验证是否为数组
|
|
|
+ if (!Array.isArray(exercises)) {
|
|
|
+ throw new Error('返回结果必须是数组格式');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证数组长度
|
|
|
+ if (exercises.length !== options.shareData.exerciseSettings.count) {
|
|
|
+ throw new Error(`题目数量不符合要求,期望 ${options.shareData.exerciseSettings.count} 道题`);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证每道题的格式
|
|
|
+ exercises.forEach((exercise: Exercise, index: number) => {
|
|
|
+ if (!['single', 'multiple', 'blank'].includes(exercise.type)) {
|
|
|
+ throw new Error(`第 ${index + 1} 题的类型不正确`);
|
|
|
+ }
|
|
|
+ if (!exercise.question?.trim()) {
|
|
|
+ throw new Error(`第 ${index + 1} 题缺少题目内容`);
|
|
|
+ }
|
|
|
+ if (!exercise.answer?.trim()) {
|
|
|
+ throw new Error(`第 ${index + 1} 题缺少答案`);
|
|
|
+ }
|
|
|
+ if (!exercise.explanation?.trim()) {
|
|
|
+ throw new Error(`第 ${index + 1} 题缺少解析`);
|
|
|
+ }
|
|
|
+ if ((exercise.type === 'single' || exercise.type === 'multiple') &&
|
|
|
+ (!Array.isArray(exercise.options) || exercise.options.length < 2)) {
|
|
|
+ throw new Error(`第 ${index + 1} 题的选项格式不正确`);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return exercises;
|
|
|
+ } catch (error) {
|
|
|
+ if (error instanceof SyntaxError) {
|
|
|
+ throw new Error('AI返回的JSON格式不正确');
|
|
|
+ }
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ task.handle = () => {
|
|
|
+ return new Promise(async (resolve) => {
|
|
|
+ try {
|
|
|
+ // 构建提示词
|
|
|
+ const prompt = `你是一位专业的教育工作者。请根据以下学习对话内容,生成练习题。
|
|
|
+注意:请只返回JSON格式的题目数组,不要包含任何其他内容。
|
|
|
+
|
|
|
+要求:
|
|
|
+1. 生成 ${options.shareData.exerciseSettings.count} 道关于"${options.shareData.exerciseSettings.knowledgePoint}"的练习题
|
|
|
+2. 题型包括:${options.shareData.exerciseSettings.types.join('、')}
|
|
|
+3. 每道题必须包含标准答案和详细解析
|
|
|
+
|
|
|
+对话内容:
|
|
|
+${options.shareData.selectedSession.get('messages').map((msg: any) =>
|
|
|
+ `${msg.role === 'user' ? '学生' : '老师'}: ${msg.content}`
|
|
|
+).join('\n')}
|
|
|
+
|
|
|
+输出格式示例:
|
|
|
+[
|
|
|
+ {
|
|
|
+ "type": "single",
|
|
|
+ "question": "题目内容",
|
|
|
+ "options": ["选项A", "选项B", "选项C", "选项D"],
|
|
|
+ "answer": "选项A",
|
|
|
+ "explanation": "解析内容"
|
|
|
+ }
|
|
|
+]`;
|
|
|
+
|
|
|
+ task.progress = 0.2;
|
|
|
+ let retryCount = 0;
|
|
|
+ const maxRetries = 3;
|
|
|
+
|
|
|
+ while (retryCount < maxRetries) {
|
|
|
+ try {
|
|
|
+ let completion = new FmodeChatCompletion([
|
|
|
+ {
|
|
|
+ role: "system",
|
|
|
+ content: "你是一位专业的教育工作者。请只返回JSON格式的题目数组,不要包含任何其他内容。确保JSON格式完全正确。"
|
|
|
+ },
|
|
|
+ {role: "user", content: prompt}
|
|
|
+ ]);
|
|
|
+
|
|
|
+ const result = await new Promise<Exercise[]>((resolveCompletion, rejectCompletion) => {
|
|
|
+ completion.sendCompletion().subscribe({
|
|
|
+ next: (message: any) => {
|
|
|
+ if (message.complete) {
|
|
|
+ try {
|
|
|
+ const exercises = validateExercises(message.content);
|
|
|
+ resolveCompletion(exercises);
|
|
|
+ } catch (error) {
|
|
|
+ rejectCompletion(error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ error: (error) => rejectCompletion(error)
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 如果验证通过,显示练习题
|
|
|
+ const modal = await options.modalCtrl.create({
|
|
|
+ component: ExerciseModalComponent,
|
|
|
+ componentProps: { exercises: result }
|
|
|
+ });
|
|
|
+
|
|
|
+ await modal.present();
|
|
|
+ const { data } = await modal.onWillDismiss();
|
|
|
+
|
|
|
+ if (!data) {
|
|
|
+ task.error = '练习未完成';
|
|
|
+ resolve(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ options.shareData.exercises = data.exercises;
|
|
|
+ task.progress = 1;
|
|
|
+ resolve(true);
|
|
|
+ return;
|
|
|
+
|
|
|
+ } catch (error: any) {
|
|
|
+ retryCount++;
|
|
|
+ if (retryCount === maxRetries) {
|
|
|
+ task.error = '多次尝试生成题目失败,请重试';
|
|
|
+ resolve(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ task.status = `生成题目失败,正在重试(${retryCount}/${maxRetries})...`;
|
|
|
+ await new Promise(r => setTimeout(r, 1000)); // 等待1秒后重试
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ task.error = error.message || '生成题目时发生错误';
|
|
|
+ resolve(false);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ return task;
|
|
|
+}
|