15270821319 6 månader sedan
förälder
incheckning
c4023c8eee

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

@@ -1,5 +1,6 @@
 import { Routes } from '@angular/router';
 import { LearningDesignPage } from './pages/learning-design/learning-design.page';
+import { ChatHistoryPage } from './pages/chat-history/chat-history.page';
 
 export const routes: Routes = [
   {
@@ -26,5 +27,13 @@ export const routes: Routes = [
     path: 'learning-history',
     loadComponent: () => import('./pages/learning-history/learning-history.page')
       .then(m => m.LearningHistoryPage)
+  },
+  {
+    path: 'chat-history',
+    loadComponent: () => import('./pages/chat-history/chat-history.page').then(m => m.ChatHistoryPage)
+  },
+  {
+    path: 'chat-session/:id',
+    loadComponent: () => import('./pages/chat-session').then(m => m.ChatSessionPage)
   }
 ];

+ 48 - 0
AiStudy-app/src/app/models/chat-session.model.ts

@@ -0,0 +1,48 @@
+import { CloudObject } from 'src/lib/ncloud';
+
+// 聊天消息接口
+export interface ChatMessage {
+  content: string;
+  isUser: boolean;
+  timestamp: Date;
+  isHidden?: boolean;
+}
+
+// 聊天会话类
+export class ChatSession extends CloudObject {
+  static className = 'ChatSession';
+
+  constructor() {
+    super(ChatSession.className);
+  }
+
+  // 获取会话标题
+  getTitle(): string {
+    return this.get('title') || '未命名会话';
+  }
+
+  // 获取会话消息
+  getMessages(): ChatMessage[] {
+    return this.get('messages') || [];
+  }
+
+  // 获取创建时间
+  getCreatedAt(): Date {
+    return this.get('createdAt');
+  }
+
+  // 获取更新时间
+  getUpdatedAt(): Date {
+    return this.get('updatedAt');
+  }
+
+  // 获取所属用户ID
+  getUserId(): string {
+    return this.get('userId');
+  }
+
+  // 获取教师信息
+  getTeacher(): any {
+    return this.get('teacher');
+  }
+} 

+ 122 - 0
AiStudy-app/src/app/pages/chat-history/chat-history.page.ts

@@ -0,0 +1,122 @@
+import { Component, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { 
+  IonHeader, 
+  IonToolbar, 
+  IonTitle, 
+  IonContent,
+  IonList,
+  IonItem,
+  IonLabel,
+  IonButton,
+  IonButtons,
+  IonBackButton,
+  AlertController
+} from '@ionic/angular/standalone';
+import { ChatSessionService } from '../../services/chat-session.service';
+import { ChatSession } from '../../models/chat-session.model';
+import { DatePipe, NgFor } from '@angular/common';
+
+@Component({
+  selector: 'app-chat-history',
+  template: `
+    <ion-header>
+      <ion-toolbar>
+        <ion-buttons slot="start">
+          <ion-back-button defaultHref="/tabs/tab1"></ion-back-button>
+        </ion-buttons>
+        <ion-title>历史会话</ion-title>
+      </ion-toolbar>
+    </ion-header>
+
+    <ion-content>
+      <ion-list>
+        <ion-item *ngFor="let session of sessions">
+          <ion-label>
+            <h2>{{ session.getTitle() }}</h2>
+            <p>{{ session.getUpdatedAt() | date:'yyyy-MM-dd HH:mm' }}</p>
+          </ion-label>
+          <ion-button slot="end" (click)="viewSession(session)">
+            查看
+          </ion-button>
+          <ion-button slot="end" color="danger" (click)="deleteSession(session)">
+            删除
+          </ion-button>
+        </ion-item>
+      </ion-list>
+    </ion-content>
+  `,
+  standalone: true,
+  imports: [
+    IonHeader,
+    IonToolbar,
+    IonTitle,
+    IonContent,
+    IonList,
+    IonItem,
+    IonLabel,
+    IonButton,
+    IonButtons,
+    IonBackButton,
+    NgFor,
+    DatePipe
+  ]
+})
+export class ChatHistoryPage implements OnInit {
+  sessions: ChatSession[] = [];
+
+  constructor(
+    private chatSessionService: ChatSessionService,
+    private router: Router,
+    private alertController: AlertController
+  ) {}
+
+  async ngOnInit() {
+    await this.loadSessions();
+  }
+
+  async loadSessions() {
+    try {
+      this.sessions = await this.chatSessionService.getUserSessions();
+    } catch (error) {
+      console.error('加载会话失败:', error);
+    }
+  }
+
+  viewSession(session: ChatSession) {
+    // 导航到会话详情页面
+    this.router.navigate(['/chat-session', session.id]);
+  }
+
+  async deleteSession(session: ChatSession) {
+    // 确保 session.id 存在
+    const sessionId = session.id;
+    if (!sessionId) {
+      console.error('Session ID is missing');
+      return;
+    }
+
+    const alert = await this.alertController.create({
+      header: '确认删除',
+      message: '确定要删除这个会话吗?',
+      buttons: [
+        {
+          text: '取消',
+          role: 'cancel'
+        },
+        {
+          text: '删除',
+          handler: async () => {
+            try {
+              await this.chatSessionService.deleteSession(sessionId);
+              await this.loadSessions();
+            } catch (error) {
+              console.error('删除会话失败:', error);
+            }
+          }
+        }
+      ]
+    });
+    await alert.present();
+  }
+} 

+ 44 - 0
AiStudy-app/src/app/pages/chat-session/chat-session.page.scss

@@ -0,0 +1,44 @@
+.message-wrapper {
+  display: flex;
+  margin: 8px 0;
+  
+  &.user-message {
+    justify-content: flex-end;
+  }
+  
+  &.ai-message {
+    justify-content: flex-start;
+  }
+}
+
+.message-bubble {
+  max-width: 75%;
+  padding: 12px 16px;
+  border-radius: 18px;
+  word-wrap: break-word;
+  position: relative;
+}
+
+.user-message .message-bubble {
+  background-color: var(--ion-color-primary);
+  color: white;
+  margin-left: 20%;
+}
+
+.ai-message .message-bubble {
+  background-color: var(--ion-color-light);
+  color: var(--ion-color-dark);
+  margin-right: 20%;
+}
+
+.message-content {
+  font-size: 16px;
+  line-height: 1.4;
+}
+
+.message-time {
+  font-size: 12px;
+  opacity: 0.7;
+  margin-top: 4px;
+  text-align: right;
+} 

+ 78 - 0
AiStudy-app/src/app/pages/chat-session/chat-session.page.ts

@@ -0,0 +1,78 @@
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { CommonModule } from '@angular/common';
+import { 
+  IonHeader, 
+  IonToolbar, 
+  IonTitle, 
+  IonContent,
+  IonButtons,
+  IonBackButton
+} from '@ionic/angular/standalone';
+import { ChatSessionService } from '../../services/chat-session.service';
+import { ChatSession } from '../../models/chat-session.model';
+import { DatePipe } from '@angular/common';
+import { MarkdownPreviewModule } from 'fmode-ng';
+
+@Component({
+  selector: 'app-chat-session',
+  template: `
+    <ion-header>
+      <ion-toolbar>
+        <ion-buttons slot="start">
+          <ion-back-button defaultHref="/chat-history"></ion-back-button>
+        </ion-buttons>
+        <ion-title>{{ session?.getTitle() }}</ion-title>
+      </ion-toolbar>
+    </ion-header>
+
+    <ion-content class="ion-padding">
+      <div *ngIf="session">
+        <div *ngFor="let message of session.getMessages()" 
+             [ngClass]="{'user-message': message.isUser, 'ai-message': !message.isUser}" 
+             class="message-wrapper">
+          <div class="message-bubble">
+            <div *ngIf="message.isUser" class="message-content">
+              {{ message.content }}
+            </div>
+            <fm-markdown-preview *ngIf="!message.isUser" 
+                               class="message-content" 
+                               [content]="message.content">
+            </fm-markdown-preview>
+            <div class="message-time">
+              {{ message.timestamp | date:'yyyy-MM-dd HH:mm:ss' }}
+            </div>
+          </div>
+        </div>
+      </div>
+    </ion-content>
+  `,
+  styleUrls: ['./chat-session.page.scss'],
+  standalone: true,
+  imports: [
+    CommonModule,
+    IonHeader,
+    IonToolbar,
+    IonTitle,
+    IonContent,
+    IonButtons,
+    IonBackButton,
+    DatePipe,
+    MarkdownPreviewModule
+  ]
+})
+export class ChatSessionPage implements OnInit {
+  session: ChatSession | null = null;
+
+  constructor(
+    private route: ActivatedRoute,
+    private chatSessionService: ChatSessionService
+  ) {}
+
+  async ngOnInit() {
+    const sessionId = this.route.snapshot.paramMap.get('id');
+    if (sessionId) {
+      this.session = await this.chatSessionService.getSession(sessionId);
+    }
+  }
+} 

+ 1 - 0
AiStudy-app/src/app/pages/chat-session/index.ts

@@ -0,0 +1 @@
+export * from './chat-session.page'; 

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

@@ -0,0 +1,76 @@
+import { Injectable } from '@angular/core';
+import { CloudQuery, CloudUser } from 'src/lib/ncloud';
+import { ChatSession, ChatMessage } from '../models/chat-session.model';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class ChatSessionService {
+  constructor() {}
+
+  // 保存新的会话
+  async saveSession(title: string, messages: ChatMessage[], teacher: any): Promise<ChatSession> {
+    const session = new ChatSession();
+    const currentUser = new CloudUser();
+
+    // 创建一个包含所有数据的对象
+    const sessionData = {
+      title,
+      messages,
+      userId: currentUser.id,
+      teacher,
+      createdAt: new Date(),
+      updatedAt: new Date()
+    };
+
+    // 使用单个对象设置所有属性
+    session.set(sessionData);
+    await session.save();
+    return session;
+  }
+
+  // 获取用户的所有会话
+  async getUserSessions(): Promise<ChatSession[]> {
+    const currentUser = new CloudUser();
+    const query = new CloudQuery(ChatSession.className);
+    query.equalTo('userId', currentUser.id);
+    
+    // 添加排序参数到 queryParams
+    if (!query.queryParams["order"]) {
+      query.queryParams["order"] = "-updatedAt";  // 负号表示降序
+    }
+    
+    const results = await query.find();
+    return results.map(result => Object.assign(new ChatSession(), result));
+  }
+
+  // 获取单个会话
+  async getSession(sessionId: string): Promise<ChatSession | null> {
+    const query = new CloudQuery(ChatSession.className);
+    const result = await query.get(sessionId);
+    return result ? Object.assign(new ChatSession(), result) : null;
+  }
+
+  // 更新会话
+  async updateSession(sessionId: string, messages: ChatMessage[]): Promise<void> {
+    const session = await this.getSession(sessionId);
+    if (!session) {
+      throw new Error('Session not found');
+    }
+
+    // 使用单个对象设置属性
+    session.set({
+      messages,
+      updatedAt: new Date()
+    });
+    await session.save();
+  }
+
+  // 删除会话
+  async deleteSession(sessionId: string): Promise<void> {
+    const session = await this.getSession(sessionId);
+    if (session) {
+      await session.destroy();
+    }
+  }
+} 

+ 13 - 0
AiStudy-app/src/app/tab1/tab1.page.html

@@ -32,6 +32,19 @@
       </ng-template>
     </ion-modal>
   </div>
+  <ion-toolbar>
+    <ion-title>
+      对话
+    </ion-title>
+    <ion-buttons slot="end">
+      <ion-button (click)="saveCurrentSession()" [disabled]="messages.length === 0">
+        <ion-icon slot="icon-only" name="save-outline"></ion-icon>
+      </ion-button>
+      <ion-button (click)="viewChatHistory()">
+        <ion-icon slot="icon-only" name="time-outline"></ion-icon>
+      </ion-button>
+    </ion-buttons>
+  </ion-toolbar>
 </ion-header>
 
 <ion-content [fullscreen]="true">

+ 19 - 0
AiStudy-app/src/app/tab1/tab1.page.scss

@@ -175,3 +175,22 @@ ion-button#select-teacher {
 ion-icon[name="add-circle-outline"] {
   color: var(--ion-color-success);
 }
+
+// 在现有样式的末尾添加
+ion-toolbar {
+  ion-buttons {
+    ion-button {
+      --padding-start: 8px;
+      --padding-end: 8px;
+      
+      ion-icon {
+        font-size: 24px;
+      }
+    }
+  }
+}
+
+// 保存按钮禁用状态样式
+ion-button[disabled] {
+  opacity: 0.5;
+}

+ 108 - 4
AiStudy-app/src/app/tab1/tab1.page.ts

@@ -1,14 +1,16 @@
 import { Component, ViewChild } from '@angular/core';
-import { IonHeader, IonToolbar, IonTitle, IonContent, IonFooter, IonItem, IonTextarea, IonButton, IonIcon, IonModal, IonList } from '@ionic/angular/standalone';
+import { IonHeader, IonToolbar, IonTitle, IonContent, IonFooter, IonItem, IonTextarea, IonButton, IonIcon, IonModal, IonList, IonButtons } from '@ionic/angular/standalone';
 import { FormsModule } from '@angular/forms';
 import { NgClass, NgFor, NgIf, DatePipe } from '@angular/common';
 import { addIcons } from 'ionicons';
-import { send, personCircleOutline, addCircleOutline, createOutline } from 'ionicons/icons';
+import { send, personCircleOutline, addCircleOutline, createOutline, saveOutline, timeOutline } from 'ionicons/icons';
 import { FmodeChatCompletion, MarkdownPreviewModule } from 'fmode-ng';
 import { DatabaseService } from 'src/app/services/database.service';
 import { Router } from '@angular/router';
 import Parse from 'parse';
 import { CloudUser } from 'src/lib/ncloud';
+import { ChatSessionService } from '../services/chat-session.service';
+import { AlertController } from '@ionic/angular/standalone';
 
 // 定义消息接口
 interface ChatMessage {
@@ -46,6 +48,7 @@ interface Teacher {
     IonIcon,
     IonModal,
     IonList,
+    IonButtons,
     FormsModule,
     NgClass,
     NgFor,
@@ -109,9 +112,18 @@ export class Tab1Page {
 
   constructor(
     private dbService: DatabaseService,
-    private router: Router
+    private router: Router,
+    private chatSessionService: ChatSessionService,
+    private alertController: AlertController
   ) {
-    addIcons({ send, personCircleOutline, addCircleOutline, createOutline });
+    addIcons({ 
+      send, 
+      personCircleOutline, 
+      addCircleOutline, 
+      createOutline,
+      saveOutline,
+      timeOutline
+    });
     this.loadTeachers();
   }
 
@@ -364,4 +376,96 @@ ${this.selectedTeacher.description}
     event.stopPropagation(); // 阻止事件冒泡
     this.router.navigate(['/custom-teacher/edit', teacherId]);
   }
+
+  // 修改 viewChatHistory 方法,添加日志
+  viewChatHistory() {
+    console.log('Viewing chat history...'); // 添加日志
+    // 检查用户是否登录
+    const currentUser = new CloudUser();
+    if (!currentUser.id) {
+      this.showAlert('提示', '请先登录后再查看历史会话');
+      return;
+    }
+    this.router.navigate(['/chat-history']);
+  }
+
+  // 修改 saveCurrentSession 方法,添加日志
+  async saveCurrentSession() {
+    console.log('Saving current session...'); // 添加日志
+    // 检查用户是否登录
+    const currentUser = new CloudUser();
+    if (!currentUser.id) {
+      await this.showAlert('提示', '请先登录后再保存会话');
+      return;
+    }
+
+    // 检查是否有可保存的消息
+    if (this.messages.length === 0) {
+      await this.showAlert('提示', '当前没有可保存的对话');
+      return;
+    }
+
+    // 过滤掉隐藏的设置消息
+    const visibleMessages = this.messages.filter(msg => !msg.isHidden);
+    if (visibleMessages.length === 0) {
+      await this.showAlert('提示', '当前没有可保存的对话');
+      return;
+    }
+
+    const alert = await this.alertController.create({
+      header: '保存会话',
+      inputs: [
+        {
+          name: 'title',
+          type: 'text',
+          placeholder: '请输入会话标题(可选)'
+        }
+      ],
+      buttons: [
+        {
+          text: '取消',
+          role: 'cancel'
+        },
+        {
+          text: '保存',
+          handler: async (data) => {
+            try {
+              // 如果用户没有输入标题,使用默认标题
+              const title = data.title?.trim() || 
+                          `与${this.selectedTeacher.name}的对话 - ${new Date().toLocaleString()}`;
+              
+              // 保存会话
+              await this.chatSessionService.saveSession(
+                title,
+                visibleMessages, // 只保存可见消息
+                {
+                  id: this.selectedTeacher.id,
+                  name: this.selectedTeacher.name,
+                  description: this.selectedTeacher.description,
+                  systemPrompt: this.selectedTeacher.systemPrompt
+                }
+              );
+              
+              await this.showAlert('成功', '会话已保存');
+            } catch (error) {
+              console.error('保存会话失败:', error);
+              await this.showAlert('错误', '保存失败,请重试');
+            }
+          }
+        }
+      ]
+    });
+
+    await alert.present();
+  }
+
+  // 辅助方法:显示提示框
+  private async showAlert(header: string, message: string) {
+    const alert = await this.alertController.create({
+      header,
+      message,
+      buttons: ['确定']
+    });
+    await alert.present();
+  }
 }