Browse Source

互动联系

15270821319 2 months ago
parent
commit
7f3b89159d

+ 159 - 0
AiStudy-app/src/agent/tasks/exercise/analyze-answers.ts

@@ -0,0 +1,159 @@
+import { Component } from '@angular/core';
+import { 
+  IonHeader, 
+  IonToolbar, 
+  IonTitle, 
+  IonContent,
+  IonList,
+  IonItem,
+  IonLabel,
+  IonButton,
+  IonButtons,
+  IonIcon,
+  IonBadge
+} from '@ionic/angular/standalone';
+import { NgFor, NgIf } from '@angular/common';
+import { AgentTaskStep } from '../../agent.task';
+import { ModalController } from '@ionic/angular/standalone';
+import { AnalyzeAnswersOptions } from './types';
+import { checkmarkCircle, closeCircle } from 'ionicons/icons';
+import { addIcons } from 'ionicons';
+
+// 分析结果展示模态框
+@Component({
+  selector: 'app-analysis-modal',
+  template: `
+    <ion-header>
+      <ion-toolbar>
+        <ion-title>练习分析</ion-title>
+        <ion-buttons slot="end">
+          <ion-button (click)="dismiss()">关闭</ion-button>
+        </ion-buttons>
+      </ion-toolbar>
+    </ion-header>
+
+    <ion-content class="ion-padding">
+      <div class="score-summary">
+        <h2>得分:{{ score }}分</h2>
+        <p>正确率:{{ correctRate }}%</p>
+      </div>
+
+      <ion-list>
+        <ion-item *ngFor="let exercise of exercises; let i = index">
+          <ion-label>
+            <h2>
+              <ion-icon [name]="exercise.isCorrect ? 'checkmark-circle' : 'close-circle'"
+                       [color]="exercise.isCorrect ? 'success' : 'danger'">
+              </ion-icon>
+              第{{ i + 1 }}题
+            </h2>
+            <p>{{ exercise.question }}</p>
+            <div class="answer-info">
+              <p>你的答案:{{ exercise.userAnswer }}</p>
+              <p>正确答案:{{ exercise.answer }}</p>
+            </div>
+            <div class="explanation">
+              <p>解析:{{ exercise.explanation }}</p>
+            </div>
+          </ion-label>
+        </ion-item>
+      </ion-list>
+    </ion-content>
+  `,
+  styles: [`
+    .score-summary {
+      text-align: center;
+      margin: 20px 0;
+    }
+    .answer-info {
+      margin: 10px 0;
+      color: var(--ion-color-medium);
+    }
+    .explanation {
+      margin-top: 10px;
+      color: var(--ion-color-dark);
+    }
+    ion-icon {
+      vertical-align: middle;
+      margin-right: 8px;
+    }
+  `],
+  standalone: true,
+  imports: [
+    IonHeader,
+    IonToolbar,
+    IonTitle,
+    IonContent,
+    IonList,
+    IonItem,
+    IonLabel,
+    IonButton,
+    IonButtons,
+    IonIcon,
+    IonBadge,
+    NgFor,
+    NgIf
+  ]
+})
+export class AnalysisModalComponent {
+  exercises: any[] = [];
+  score: number = 0;
+  correctRate: number = 0;
+
+  constructor(
+    private modalCtrl: ModalController
+  ) {
+    addIcons({ checkmarkCircle, closeCircle });
+  }
+
+  dismiss() {
+    this.modalCtrl.dismiss();
+  }
+}
+
+// 分析答案任务
+export function TaskAnalyzeAnswers(options: AnalyzeAnswersOptions): AgentTaskStep {
+  const task = new AgentTaskStep({
+    title: "分析答案",
+    shareData: options.shareData
+  });
+  
+  task.handle = () => {
+    return new Promise(async (resolve) => {
+      try {
+        const exercises = options.shareData.exercises;
+        
+        // 计算得分和正确率
+        let correctCount = 0;
+        exercises.forEach((exercise: any) => {
+          exercise.isCorrect = exercise.userAnswer === exercise.answer;
+          if (exercise.isCorrect) correctCount++;
+        });
+        
+        const score = Math.round((correctCount / exercises.length) * 100);
+        const correctRate = Math.round((correctCount / exercises.length) * 100);
+
+        // 显示分析结果
+        const modal = await options.modalCtrl.create({
+          component: AnalysisModalComponent,
+          componentProps: {
+            exercises,
+            score,
+            correctRate
+          }
+        });
+
+        await modal.present();
+        await modal.onWillDismiss();
+
+        task.progress = 1;
+        resolve(true);
+      } catch (error: any) {
+        task.error = error.message || '分析答案时发生错误';
+        resolve(false);
+      }
+    });
+  };
+  
+  return task;
+} 

+ 322 - 0
AiStudy-app/src/agent/tasks/exercise/generate-answer.ts

@@ -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;
+} 

+ 7 - 0
AiStudy-app/src/agent/tasks/exercise/index.ts

@@ -0,0 +1,7 @@
+export * from './types';  // 先导出类型定义
+export { TaskSelectSession } from './select-session';
+export { TaskGenerateAndAnswer } from './generate-answer';
+export { 
+    TaskAnalyzeAnswers,
+    AnalysisModalComponent 
+} from './analyze-answers';

+ 289 - 0
AiStudy-app/src/agent/tasks/exercise/select-session.ts

@@ -0,0 +1,289 @@
+import { Component } from '@angular/core';
+import { 
+  IonHeader, 
+  IonToolbar, 
+  IonTitle, 
+  IonContent,
+  IonList,
+  IonItem,
+  IonLabel,
+  IonButton,
+  IonButtons,
+  IonInput,
+  IonSelect,
+  IonSelectOption,
+  IonListHeader,
+  IonRange,
+  IonIcon
+} from '@ionic/angular/standalone';
+import { NgFor, NgIf, DatePipe } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { ModalController } from '@ionic/angular/standalone';
+import { AgentTaskStep } from '../../agent.task';
+import { ChatSessionService } from '../../../app/services/chat-session.service';
+import { CloudObject } from 'src/lib/ncloud';
+import { TaskOptions } from './types';
+
+@Component({
+  selector: 'app-select-session-modal',
+  template: `
+    <ion-header>
+      <ion-toolbar>
+        <ion-title>选择学习对话</ion-title>
+        <ion-buttons slot="end">
+          <ion-button (click)="dismiss()">取消</ion-button>
+        </ion-buttons>
+      </ion-toolbar>
+    </ion-header>
+
+    <ion-content>
+      <!-- 引导说明 -->
+      <div class="guide-section ion-padding">
+        <p>请选择一次学习对话作为练习题出题参考,并设置练习参数:</p>
+      </div>
+
+      <!-- 历史会话列表 -->
+      <ion-list>
+        <ion-list-header>
+          <ion-label>历史学习对话</ion-label>
+        </ion-list-header>
+        
+        <ion-item *ngFor="let session of sessions" 
+                  [class.selected]="selectedSession?.id === session.id"
+                  (click)="selectSession(session)"
+                  button detail>
+          <ion-icon name="chatbubbles-outline" slot="start" 
+                    [color]="selectedSession?.id === session.id ? 'primary' : 'medium'">
+          </ion-icon>
+          <ion-label>
+            <h2>{{ session.get('title') }}</h2>
+            <p class="ion-text-wrap">{{ session.get('lastMessage') }}</p>
+            <p class="session-time">{{ session.get('updatedAt') | date:'MM-dd HH:mm' }}</p>
+          </ion-label>
+          <ion-icon *ngIf="selectedSession?.id === session.id" 
+                    name="checkmark-circle" 
+                    color="primary" 
+                    slot="end">
+          </ion-icon>
+        </ion-item>
+      </ion-list>
+
+      <!-- 练习设置 -->
+      <div *ngIf="selectedSession" class="settings-section ion-padding">
+        <ion-list>
+          <ion-list-header>
+            <ion-label>练习参数设置</ion-label>
+          </ion-list-header>
+
+          <ion-item>
+            <ion-icon name="book-outline" slot="start" color="primary"></ion-icon>
+            <ion-label position="stacked">知识点范围</ion-label>
+            <ion-input [(ngModel)]="knowledgePoint" 
+                      placeholder="请输入要考察的知识点"
+                      class="ion-margin-top">
+            </ion-input>
+          </ion-item>
+
+          <ion-item>
+            <ion-icon name="list-outline" slot="start" color="primary"></ion-icon>
+            <ion-label position="stacked">题目类型</ion-label>
+            <ion-select [(ngModel)]="exerciseTypes" 
+                       multiple="true"
+                       placeholder="请选择题目类型"
+                       class="ion-margin-top">
+              <ion-select-option value="single">单选题</ion-select-option>
+              <ion-select-option value="multiple">多选题</ion-select-option>
+              <ion-select-option value="blank">填空题</ion-select-option>
+            </ion-select>
+          </ion-item>
+
+          <ion-item>
+            <ion-icon name="numbers-outline" slot="start" color="primary"></ion-icon>
+            <ion-label position="stacked">题目数量</ion-label>
+            <ion-range [(ngModel)]="exerciseCount" 
+                      min="1" max="10" step="1" snaps
+                      pin color="primary"
+                      class="ion-margin-top">
+              <ion-label slot="start">1</ion-label>
+              <ion-label slot="end">10</ion-label>
+            </ion-range>
+          </ion-item>
+        </ion-list>
+      </div>
+
+      <!-- 确认按钮 -->
+      <div class="ion-padding">
+        <ion-button expand="block" 
+                    [disabled]="!isValid()"
+                    (click)="confirm()">
+          <ion-icon name="checkmark-circle" slot="start"></ion-icon>
+          开始生成练习题
+        </ion-button>
+      </div>
+    </ion-content>
+  `,
+  styles: [`
+    .guide-section {
+      color: var(--ion-color-medium);
+      font-size: 14px;
+      margin-bottom: 8px;
+    }
+
+    .selected {
+      --background: var(--ion-color-primary-light);
+      
+      ion-label h2 {
+        color: var(--ion-color-primary);
+        font-weight: 500;
+      }
+    }
+
+    ion-item {
+      --padding-start: 16px;
+      --inner-padding-end: 16px;
+      --padding-top: 12px;
+      --padding-bottom: 12px;
+    }
+
+    .session-time {
+      font-size: 12px;
+      color: var(--ion-color-medium);
+      margin-top: 4px;
+    }
+
+    .settings-section {
+      margin-top: 16px;
+      background: var(--ion-color-light);
+      border-radius: 8px;
+    }
+
+    ion-list-header {
+      padding-top: 16px;
+      padding-bottom: 8px;
+      
+      ion-label {
+        font-weight: 600;
+        font-size: 16px;
+        color: var(--ion-color-dark);
+      }
+    }
+
+    ion-range {
+      padding-top: 8px;
+      padding-bottom: 8px;
+    }
+
+    ion-button {
+      margin-top: 24px;
+    }
+  `],
+  standalone: true,
+  imports: [
+    IonHeader,
+    IonToolbar,
+    IonTitle,
+    IonContent,
+    IonList,
+    IonItem,
+    IonLabel,
+    IonButton,
+    IonButtons,
+    IonInput,
+    IonSelect,
+    IonSelectOption,
+    NgFor,
+    NgIf,
+    FormsModule,
+    IonListHeader,
+    IonRange,
+    IonIcon,
+    DatePipe
+  ]
+})
+class SelectSessionModalComponent {
+  sessions: CloudObject[] = [];
+  selectedSession: CloudObject | null = null;
+  knowledgePoint: string = '';
+  exerciseTypes: string[] = [];
+  exerciseCount: number = 5;
+
+  constructor(
+    private modalCtrl: ModalController,
+    private chatSessionService: ChatSessionService
+  ) {}
+
+  async ngOnInit() {
+    // 加载历史会话
+    this.sessions = await this.chatSessionService.getUserSessions();
+  }
+
+  selectSession(session: CloudObject) {
+    this.selectedSession = session;
+  }
+
+  isValid(): boolean {
+    return !!(
+      this.selectedSession &&
+      this.knowledgePoint &&
+      this.exerciseTypes.length > 0 &&
+      this.exerciseCount > 0 &&
+      this.exerciseCount <= 10
+    );
+  }
+
+  dismiss() {
+    this.modalCtrl.dismiss();
+  }
+
+  confirm() {
+    if (!this.isValid()) return;
+
+    this.modalCtrl.dismiss({
+      session: this.selectedSession,
+      settings: {
+        knowledgePoint: this.knowledgePoint,
+        types: this.exerciseTypes,
+        count: this.exerciseCount
+      }
+    });
+  }
+}
+
+// 选择会话任务
+export function TaskSelectSession(options: TaskOptions): AgentTaskStep {
+  const task = new AgentTaskStep({
+    title: "选择学习对话",
+    shareData: options.shareData
+  });
+  
+  task.handle = () => {
+    return new Promise(async (resolve) => {
+      try {
+        const modal = await options.modalCtrl.create({
+          component: SelectSessionModalComponent
+        });
+
+        await modal.present();
+        const { data, role } = await modal.onWillDismiss();
+
+        if (role === 'dismiss' || !data) {
+          task.error = '未选择学习对话';
+          resolve(false);
+          return;
+        }
+
+        // 保存选择的会话和设置到共享数据
+        options.shareData.selectedSession = data.session;
+        options.shareData.exerciseSettings = data.settings;
+        
+        task.progress = 1;
+        resolve(true);
+      } catch (error: any) {
+        task.error = error.message || '选择会话时发生错误';
+        resolve(false);
+      }
+    });
+  };
+  
+  return task;
+} 

+ 27 - 0
AiStudy-app/src/agent/tasks/exercise/types.ts

@@ -0,0 +1,27 @@
+import { ModalController } from '@ionic/angular/standalone';
+import { ApiService } from '../../../app/services/api.service';
+import { ExerciseService } from '../../../app/services/exercise.service';
+
+export interface TaskOptions {
+  modalCtrl: ModalController;
+  shareData: any;
+}
+
+export interface GenerateAnswerOptions extends TaskOptions {
+  apiService: ApiService;
+}
+
+export interface AnalyzeAnswersOptions extends TaskOptions {
+  exerciseService: ExerciseService;  // 更新为具体类型
+}
+
+// 如果需要,添加其他类型定义
+export interface Exercise {
+  type: 'single' | 'multiple' | 'blank';
+  question: string;
+  options?: string[];
+  userAnswer?: string;
+  answer: string;
+  explanation: string;
+  isCorrect?: boolean;
+} 

+ 6 - 1
AiStudy-app/src/app/app.config.ts

@@ -10,4 +10,9 @@ export const appConfig: ApplicationConfig = {
     provideHttpClient(),
     { provide: IonicModule, useFactory: () => IonicModule.forRoot() }
   ]
-}; 
+};
+
+// 不需要在这里声明 standalone 组件
+// export const standaloneComponents = [
+//   AnalysisModalComponent
+// ]; 

+ 5 - 0
AiStudy-app/src/app/app.routes.ts

@@ -35,5 +35,10 @@ export const routes: Routes = [
   {
     path: 'chat-session/:id',
     loadComponent: () => import('./pages/chat-session').then(m => m.ChatSessionPage)
+  },
+  {
+    path: 'interactive-practice',
+    loadComponent: () => import('./pages/interactive-practice/interactive-practice.page')
+      .then(m => m.InteractivePracticePage)
   }
 ];

+ 137 - 0
AiStudy-app/src/app/components/select-session-modal/select-session-modal.component.ts

@@ -0,0 +1,137 @@
+import { Component, OnInit } from '@angular/core';
+import { ModalController, IonHeader, IonToolbar, IonTitle, IonContent, IonList, IonItem, IonLabel, IonButton, IonButtons, IonInput, IonSelect, IonSelectOption } from '@ionic/angular/standalone';
+import { NgFor, NgIf } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { ChatSessionService } from '../../services/chat-session.service';
+import { CloudObject } from 'src/lib/ncloud';
+
+@Component({
+  selector: 'app-select-session-modal',
+  template: `
+    <ion-header>
+      <ion-toolbar>
+        <ion-title>选择学习对话</ion-title>
+        <ion-buttons slot="end">
+          <ion-button (click)="dismiss()">取消</ion-button>
+        </ion-buttons>
+      </ion-toolbar>
+    </ion-header>
+
+    <ion-content class="ion-padding">
+      <!-- 历史会话列表 -->
+      <ion-list>
+        <ion-item *ngFor="let session of sessions" 
+                  [class.selected]="selectedSession?.id === session.id"
+                  (click)="selectSession(session)">
+          <ion-label>
+            <h2>{{ session.get('title') }}</h2>
+            <p>{{ session.get('lastMessage') }}</p>
+          </ion-label>
+        </ion-item>
+      </ion-list>
+
+      <!-- 练习设置 -->
+      <div *ngIf="selectedSession" class="exercise-settings">
+        <ion-item>
+          <ion-label position="stacked">知识点范围</ion-label>
+          <ion-input [(ngModel)]="knowledgePoint" placeholder="请输入要考察的知识点"></ion-input>
+        </ion-item>
+
+        <ion-item>
+          <ion-label position="stacked">题目类型</ion-label>
+          <ion-select [(ngModel)]="exerciseTypes" multiple="true">
+            <ion-select-option value="single">单选题</ion-select-option>
+            <ion-select-option value="multiple">多选题</ion-select-option>
+            <ion-select-option value="blank">填空题</ion-select-option>
+          </ion-select>
+        </ion-item>
+
+        <ion-item>
+          <ion-label position="stacked">题目数量</ion-label>
+          <ion-input type="number" [(ngModel)]="exerciseCount" min="1" max="10"></ion-input>
+        </ion-item>
+
+        <ion-button expand="block" 
+                    class="ion-margin-top" 
+                    [disabled]="!isValid()"
+                    (click)="confirm()">
+          确认
+        </ion-button>
+      </div>
+    </ion-content>
+  `,
+  styles: [`
+    .selected {
+      --background: var(--ion-color-light);
+    }
+    .exercise-settings {
+      margin-top: 20px;
+    }
+  `],
+  standalone: true,
+  imports: [
+    IonHeader,
+    IonToolbar,
+    IonTitle,
+    IonContent,
+    IonList,
+    IonItem,
+    IonLabel,
+    IonButton,
+    IonButtons,
+    IonInput,
+    IonSelect,
+    IonSelectOption,
+    NgFor,
+    NgIf,
+    FormsModule
+  ]
+})
+export class SelectSessionModalComponent implements OnInit {
+  sessions: CloudObject[] = [];
+  selectedSession: CloudObject | null = null;
+  knowledgePoint: string = '';
+  exerciseTypes: string[] = [];
+  exerciseCount: number = 5;
+
+  constructor(
+    private modalCtrl: ModalController,
+    private chatSessionService: ChatSessionService
+  ) {}
+
+  async ngOnInit() {
+    // 加载历史会话
+    this.sessions = await this.chatSessionService.getSessionList();
+  }
+
+  selectSession(session: CloudObject) {
+    this.selectedSession = session;
+  }
+
+  isValid(): boolean {
+    return !!(
+      this.selectedSession &&
+      this.knowledgePoint &&
+      this.exerciseTypes.length > 0 &&
+      this.exerciseCount > 0 &&
+      this.exerciseCount <= 10
+    );
+  }
+
+  dismiss() {
+    this.modalCtrl.dismiss();
+  }
+
+  confirm() {
+    if (!this.isValid()) return;
+
+    this.modalCtrl.dismiss({
+      session: this.selectedSession,
+      settings: {
+        knowledgePoint: this.knowledgePoint,
+        types: this.exerciseTypes,
+        count: this.exerciseCount
+      }
+    });
+  }
+} 

+ 68 - 0
AiStudy-app/src/app/pages/favorite-exercises/favorite-exercises.page.ts

@@ -0,0 +1,68 @@
+import { Component } from '@angular/core';
+import {
+  IonHeader,
+  IonToolbar,
+  IonTitle,
+  IonContent,
+  IonButtons,
+  IonBackButton,
+  IonRefresher,
+  IonRefresherContent,
+  IonCard,
+  IonCardHeader,
+  IonCardContent,
+  IonChip,
+  IonButton,
+  IonIcon,
+  IonText,
+  IonAccordionGroup,
+  IonAccordion,
+  IonItem,
+  IonLabel
+} from '@ionic/angular/standalone';
+import { NgFor, NgIf } from '@angular/common';
+import { ExerciseService } from '../../services/exercise.service';
+
+@Component({
+  selector: 'app-favorite-exercises',
+  templateUrl: './favorite-exercises.page.html',
+  styleUrls: ['./favorite-exercises.page.scss'],
+  standalone: true,
+  imports: [
+    IonHeader,
+    IonToolbar,
+    IonTitle,
+    IonContent,
+    IonButtons,
+    IonBackButton,
+    IonRefresher,
+    IonRefresherContent,
+    IonCard,
+    IonCardHeader,
+    IonCardContent,
+    IonChip,
+    IonButton,
+    IonIcon,
+    IonText,
+    IonAccordionGroup,
+    IonAccordion,
+    IonItem,
+    IonLabel,
+    NgFor,
+    NgIf
+  ]
+})
+export class FavoriteExercisesPage {
+  favorites: any[] = [];
+
+  constructor(private exerciseService: ExerciseService) {}
+
+  async doRefresh(event: any) {
+    // TODO: 实现刷新逻辑
+    event.target.complete();
+  }
+
+  async removeFavorite(item: any) {
+    // TODO: 实现取消收藏逻辑
+  }
+} 

+ 161 - 0
AiStudy-app/src/app/pages/interactive-practice/interactive-practice.page.ts

@@ -0,0 +1,161 @@
+import { Component, OnInit } from '@angular/core';
+import { 
+  IonHeader, 
+  IonToolbar, 
+  IonTitle, 
+  IonContent,
+  IonButtons,
+  IonBackButton,
+  IonProgressBar,
+  IonButton,
+  ModalController
+} from '@ionic/angular/standalone';
+import { NgFor, NgIf } from '@angular/common';
+import { TaskExecutor } from '../../../agent/agent.start';
+import { AgentTaskStep } from '../../../agent/agent.task';
+import { ExerciseService } from '../../services/exercise.service';
+import { ApiService } from '../../services/api.service';
+import { 
+  TaskSelectSession, 
+  TaskGenerateAndAnswer, 
+  TaskAnalyzeAnswers,
+  TaskOptions,
+  GenerateAnswerOptions,
+  AnalyzeAnswersOptions
+} from '../../../agent/tasks/exercise';
+
+@Component({
+  selector: 'app-interactive-practice',
+  template: `
+    <ion-header>
+      <ion-toolbar>
+        <ion-buttons slot="start">
+          <ion-back-button defaultHref="/tabs/tab2"></ion-back-button>
+        </ion-buttons>
+        <ion-title>互动练习</ion-title>
+      </ion-toolbar>
+    </ion-header>
+
+    <ion-content class="ion-padding">
+      <!-- 任务进度显示 -->
+      <div *ngFor="let task of taskList" class="task-item">
+        <div class="task-header">
+          <h3>{{ task.title }}</h3>
+          <span *ngIf="task.status">{{ task.status }}</span>
+        </div>
+        <ion-progress-bar [value]="task.progress"></ion-progress-bar>
+        <div *ngIf="task.error" class="error-message">
+          {{ task.error }}
+        </div>
+      </div>
+
+      <!-- 完成后的操作按钮 -->
+      <div *ngIf="isComplete" class="completion-actions">
+        <ion-button expand="block" (click)="restartExercise()">
+          再来一次
+        </ion-button>
+      </div>
+    </ion-content>
+  `,
+  styles: [`
+    .task-item {
+      margin-bottom: 20px;
+    }
+    .task-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 8px;
+    }
+    .task-header h3 {
+      margin: 0;
+      font-size: 16px;
+      font-weight: 500;
+    }
+    .error-message {
+      color: var(--ion-color-danger);
+      margin-top: 8px;
+      font-size: 14px;
+    }
+    .completion-actions {
+      margin-top: 32px;
+    }
+  `],
+  standalone: true,
+  imports: [
+    IonHeader,
+    IonToolbar,
+    IonTitle,
+    IonContent,
+    IonButtons,
+    IonBackButton,
+    IonProgressBar,
+    IonButton,
+    NgFor,
+    NgIf
+  ]
+})
+export class InteractivePracticePage implements OnInit {
+  isComplete: boolean = false;
+  taskList: AgentTaskStep[] = [];
+  shareData: any = {};
+
+  constructor(
+    private modalCtrl: ModalController,
+    private exerciseService: ExerciseService,
+    private apiService: ApiService
+  ) {}
+
+  ngOnInit() {
+    this.startExerciseTask();
+  }
+
+  async startExerciseTask() {
+    this.isComplete = false;
+    this.shareData = {};
+
+    try {
+      // 创建任务链
+      const tasks = [
+        TaskSelectSession({
+          modalCtrl: this.modalCtrl,
+          shareData: this.shareData
+        } as TaskOptions),
+        TaskGenerateAndAnswer({
+          modalCtrl: this.modalCtrl,
+          shareData: this.shareData,
+          apiService: this.apiService
+        } as GenerateAnswerOptions),
+        TaskAnalyzeAnswers({
+          modalCtrl: this.modalCtrl,
+          shareData: this.shareData,
+          exerciseService: this.exerciseService
+        } as AnalyzeAnswersOptions)
+      ];
+
+      // 设置任务列表
+      this.taskList = tasks;
+
+      // 执行任务链
+      const success = await TaskExecutor(this.taskList);
+      this.isComplete = true;
+
+      if (!success) {
+        console.error('Task execution failed');
+        // 可以在这里添加用户提示
+      }
+    } catch (error) {
+      console.error('Error in exercise task:', error);
+      // 显示错误信息给用户
+      this.taskList.forEach(task => {
+        if (task.error) {
+          console.error(`Task error: ${task.title}:`, task.error);
+        }
+      });
+    }
+  }
+
+  restartExercise() {
+    this.startExerciseTask();
+  }
+} 

+ 14 - 23
AiStudy-app/src/app/services/api.service.ts

@@ -1,33 +1,24 @@
 import { Injectable } from '@angular/core';
-import { HttpClient } from '@angular/common/http';
-import { Observable } from 'rxjs';
+import { CloudApi } from 'src/lib/ncloud';
 
 @Injectable({
   providedIn: 'root'
 })
 export class ApiService {
-  // API基础URL
-  private baseUrl = 'http://your-api-base-url';
+  private api = new CloudApi();
 
-  constructor(private http: HttpClient) { }
+  constructor() {}
 
-  // GET请求示例
-  getData(): Observable<any> {
-    return this.http.get(`${this.baseUrl}/endpoint`);
-  }
-
-  // POST请求示例
-  postData(data: any): Observable<any> {
-    return this.http.post(`${this.baseUrl}/endpoint`, data);
-  }
-
-  // PUT请求示例
-  updateData(id: string, data: any): Observable<any> {
-    return this.http.put(`${this.baseUrl}/endpoint/${id}`, data);
-  }
-
-  // DELETE请求示例
-  deleteData(id: string): Observable<any> {
-    return this.http.delete(`${this.baseUrl}/endpoint/${id}`);
+  async chatCompletion(prompt: string) {
+    try {
+      const response = await this.api.fetch('chat/completion', {
+        prompt,
+        model: 'gpt-3.5-turbo'
+      });
+      return response;
+    } catch (error) {
+      console.error('Chat completion error:', error);
+      throw error;
+    }
   }
 }

+ 10 - 0
AiStudy-app/src/app/services/chat-session.service.ts

@@ -1,6 +1,7 @@
 import { Injectable } from '@angular/core';
 import { CloudQuery, CloudUser } from 'src/lib/ncloud';
 import { ChatSession, ChatMessage } from '../models/chat-session.model';
+import { CloudObject } from 'src/lib/ncloud';
 
 @Injectable({
   providedIn: 'root'
@@ -74,4 +75,13 @@ export class ChatSessionService {
       await session.destroy();
     }
   }
+
+  // 获取会话列表
+  async getSessionList(): Promise<CloudObject[]> {
+    const query = new CloudQuery('ChatSession');
+    if (!query.queryParams["order"]) {
+      query.queryParams["order"] = "-updatedAt";  // 负号表示降序
+    }
+    return await query.find();
+  }
 } 

+ 160 - 0
AiStudy-app/src/app/services/exercise.service.ts

@@ -0,0 +1,160 @@
+import { Injectable } from '@angular/core';
+import { CloudObject, CloudQuery, CloudUser } from 'src/lib/ncloud';
+
+// 练习题类型定义
+export interface ExerciseData {
+  sessionId: string;
+  userId: string;
+  knowledgePoint: string;
+  exercises: Array<{
+    type: string;
+    question: string;
+    options?: string[];
+    userAnswer?: string;
+    answer?: string;
+    explanation?: string;
+  }>;
+}
+
+export class Exercise extends CloudObject {
+  static className = 'Exercise';
+  
+  constructor() {
+    super(Exercise.className);
+  }
+  
+  getData(): ExerciseData {
+    return {
+      sessionId: this.get('sessionId'),
+      userId: this.get('userId'),
+      knowledgePoint: this.get('knowledgePoint'),
+      exercises: this.get('exercises')
+    };
+  }
+
+  // 修改 setData 方法,使用 set
+  setData(data: Partial<ExerciseData>): void {
+    this.set(data);  // CloudObject.set 接收一个对象参数
+  }
+}
+
+export class Favorite extends CloudObject {
+  static className = 'Favorite';
+  
+  constructor() {
+    super(Favorite.className);
+  }
+
+  // 修改 setData 方法,使用 set
+  setData(data: { userId: string; exerciseId: string }): void {
+    this.set(data);  // CloudObject.set 接收一个对象参数
+  }
+}
+
+@Injectable({
+  providedIn: 'root'
+})
+export class ExerciseService {
+  constructor() {}
+
+  // 保存练习题
+  async saveExercise(exerciseData: Partial<ExerciseData>): Promise<Exercise> {
+    const exercise = new Exercise();
+    
+    // 设置当前用户
+    const currentUser = new CloudUser();
+    if (!currentUser.id) throw new Error('User not logged in');
+    exerciseData.userId = currentUser.id;
+    
+    // 保存数据
+    exercise.setData(exerciseData);
+    
+    await exercise.save();
+    return exercise;
+  }
+
+  // 获取用户的练习记录
+  async getUserExercises(): Promise<Exercise[]> {
+    const currentUser = new CloudUser();
+    if (!currentUser.id) throw new Error('User not logged in');
+    
+    const query = new CloudQuery(Exercise.className);
+    query.equalTo('userId', currentUser.id);
+    
+    // 修改:在 where 条件中添加排序
+    if (!query.queryParams["where"]) {
+      query.queryParams["where"] = {};
+    }
+    query.queryParams["order"] = "-createdAt";  // 使用 order 参数进行排序
+    
+    const results = await query.find();
+    return results.map((result: CloudObject) => Object.assign(new Exercise(), result));
+  }
+
+  // 收藏练习题
+  async favoriteExercise(exerciseId: string): Promise<Favorite> {
+    const favorite = new Favorite();
+    
+    const currentUser = new CloudUser();
+    if (!currentUser.id) throw new Error('User not logged in');
+    
+    favorite.setData({
+      userId: currentUser.id,
+      exerciseId
+    });
+    
+    await favorite.save();
+    return favorite;
+  }
+
+  // 获取收藏的练习题
+  async getFavoriteExercises(): Promise<Exercise[]> {
+    const currentUser = new CloudUser();
+    if (!currentUser.id) throw new Error('User not logged in');
+    
+    // 先获取收藏记录
+    const favoriteQuery = new CloudQuery(Favorite.className);
+    favoriteQuery.equalTo('userId', currentUser.id);
+    const favorites = await favoriteQuery.find();
+    
+    // 获取对应的练习题
+    const exerciseIds = favorites.map((favorite: CloudObject) => favorite.get('exerciseId'));
+    if (exerciseIds.length === 0) return [];
+    
+    const exerciseQuery = new CloudQuery(Exercise.className);
+    // 修改:在 where 条件中添加 $in 查询
+    if (!exerciseQuery.queryParams["where"]) {
+      exerciseQuery.queryParams["where"] = {};
+    }
+    exerciseQuery.queryParams["where"]["objectId"] = { "$in": exerciseIds };
+    
+    const results = await exerciseQuery.find();
+    return results.map((result: CloudObject) => Object.assign(new Exercise(), result));
+  }
+
+  // 检查是否已收藏
+  async isFavorited(exerciseId: string): Promise<boolean> {
+    const currentUser = new CloudUser();
+    if (!currentUser.id) throw new Error('User not logged in');
+    
+    const query = new CloudQuery(Favorite.className);
+    query.equalTo('userId', currentUser.id);
+    query.equalTo('exerciseId', exerciseId);
+    const results = await query.find();
+    return results.length > 0;
+  }
+
+  // 取消收藏
+  async unfavoriteExercise(exerciseId: string): Promise<void> {
+    const currentUser = new CloudUser();
+    if (!currentUser.id) throw new Error('User not logged in');
+    
+    const query = new CloudQuery(Favorite.className);
+    query.equalTo('userId', currentUser.id);
+    query.equalTo('exerciseId', exerciseId);
+    const favorite = await query.first();
+    if (favorite) {
+      await favorite.destroy();
+    }
+  }
+} 

+ 9 - 0
AiStudy-app/src/app/tab2/tab2.page.ts

@@ -94,6 +94,15 @@ export class Tab2Page implements OnInit {
       case 'learning-history':
         this.router.navigate(['/learning-history']);
         break;
+      case 'learning-overview':
+        this.router.navigate(['/learning-history']);
+        break;
+      case 'interactive-practice':
+        this.router.navigate(['/interactive-practice']);
+        break;
+      case 'favorites':
+        this.router.navigate(['/favorites']);
+        break;
       default:
         console.log(`Navigating to ${feature}`);
     }

+ 24 - 15
AiStudy-app/src/lib/ncloud.ts

@@ -384,32 +384,41 @@ export class CloudUser extends CloudObject {
     }
 }
 
-export class CloudApi{
-    async fetch(path:string,body:any,options?:{
-        method:string
-        body:any
-    }){
-
-        let reqOpts:any =  {
+export class CloudApi {
+    async fetch(path: string, body: any, options?: {
+        method: string
+        body: any
+    }) {
+        let reqOpts: any = {
             headers: {
                 "x-parse-application-id": "dev",
                 "Content-Type": "application/json"
             },
             method: options?.method || "POST",
             mode: "cors",
-            credentials: "omit"
+            // 移除 credentials: "include",使用 "same-origin"
+            credentials: "same-origin"
         }
-        if(body||options?.body){
+        
+        if (body || options?.body) {
             reqOpts.body = JSON.stringify(body || options?.body);
             reqOpts.json = true;
         }
-        
+
         // 更新服务器地址
         let host = `https://dev.fmode.cn`
-        let url = `${host}/api/`+path
-        console.log(url,reqOpts)
-        const response = await fetch(url,reqOpts);
-        let json = await response.json();
-        return json
+        let url = `${host}/api/` + path
+        console.log(url, reqOpts)
+        
+        try {
+            const response = await fetch(url, reqOpts);
+            if (!response.ok) {
+                throw new Error(`HTTP error! status: ${response.status}`);
+            }
+            return await response.json();
+        } catch (error) {
+            console.error('API request failed:', error);
+            throw error;
+        }
     }
 }

+ 10 - 2
AiStudy-app/tsconfig.json

@@ -24,12 +24,20 @@
     "paths": {
       "@agent/*": ["src/agent/*"],
       "@app/*": ["src/app/*"]
-    }
+    },
+    "allowJs": true,
+    "resolveJsonModule": true,
+    "esModuleInterop": true
   },
   "angularCompilerOptions": {
     "enableI18nLegacyMessageIdFormat": false,
     "strictInjectionParameters": true,
     "strictInputAccessModifiers": true,
     "strictTemplates": true
-  }
+  },
+  "include": [
+    "src/**/*.ts",
+    "src/**/*.d.ts",
+    "src/**/*.js"
+  ]
 }