Bläddra i källkod

0210348-AI聊天界面完善

0210348 2 månader sedan
förälder
incheckning
d62fe615ea

+ 17 - 0
src/app/ai-chat/ai-chat-routing.module.ts

@@ -0,0 +1,17 @@
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import { AiChatPage } from './ai-chat.page';
+
+const routes: Routes = [
+  {
+    path: '',
+    component: AiChatPage
+  }
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule],
+})
+export class AiChatPageRoutingModule {}

+ 20 - 0
src/app/ai-chat/ai-chat.module.ts

@@ -0,0 +1,20 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+import { IonicModule } from '@ionic/angular';
+
+import { AiChatPageRoutingModule } from './ai-chat-routing.module';
+
+import { AiChatPage } from './ai-chat.page';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    IonicModule,
+    AiChatPageRoutingModule
+  ],
+  declarations: [AiChatPage]
+})
+export class AiChatPageModule {}

+ 40 - 0
src/app/ai-chat/ai-chat.page.html

@@ -0,0 +1,40 @@
+<ion-header [translucent]="true">
+  <ion-toolbar>
+    <ion-title>ai-chat</ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content [fullscreen]="true">
+  <ion-header collapse="condense">
+    <ion-toolbar>
+      <ion-title size="large">Chat</ion-title>
+    </ion-toolbar>
+  </ion-header>
+
+  <!-- Displaying chat history -->
+  <div *ngIf="chatHistory.length > 0; else noChats">
+    <ion-list>
+      <ion-item *ngFor="let chat of chatHistory" (click)="goToChat(chat.chatId)" class="chat-item">
+        <div class="avatar">{{ chat.username[0].toUpperCase() }}</div>
+        <ion-label>
+          <h2>{{ chat.username }}</h2>
+          <p>{{ chat.userChat }}</p>
+        </ion-label>
+      </ion-item>
+    </ion-list>
+  </div>
+  <ng-template #noChats>
+    <ion-text color="medium" class="no-chats">
+      <p>暂无</p>
+    </ion-text>
+  </ng-template>
+</ion-content>
+
+<!-- Button to start new chat -->
+<ion-footer>
+  <ion-toolbar>
+    <ion-buttons slot="end" class="new-chat-button">
+      <ion-button expand="block" (click)="startNewChat()">开启新聊天</ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-footer>

+ 66 - 0
src/app/ai-chat/ai-chat.page.scss

@@ -0,0 +1,66 @@
+.chat-item {
+  display: flex;
+  align-items: center;
+  padding: 10px;
+  margin-top: 10px;
+  margin-bottom: 10px;
+  border-radius: 10px;
+  background-color: #ffffff; /* 更改为白色背景 */
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 添加阴影效果 */
+
+  .avatar {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 40px;
+    height: 40px;
+    border-radius: 50%;
+    background-color: #007bff; /* 蓝色头像背景 */
+    color: white;
+    font-size: 1.5rem;
+    margin-right: 10px;
+    text-transform: uppercase;
+  }
+
+  ion-label {
+    h2 {
+      font-size: 1.2rem;
+      margin: 0;
+    }
+    p {
+      font-size: 1rem;
+      margin: 0;
+    }
+  }
+}
+
+ion-content {
+  ion-list {
+    padding: 0 10px;
+  }
+}
+
+.no-chats {
+  text-align: center;
+  margin-top: 20px;
+  font-size: 1.2rem;
+  color: #888;
+}
+
+ion-footer {
+  ion-toolbar {
+    .new-chat-button {
+      width: 100%;
+      ion-button {
+        font-size: 1.2rem;
+        --background: #007bff;
+        --background-hover: #0056b3;
+        --border-radius: 0;
+        --color: white;
+        padding: 15px 0;
+        width: 100%;
+        justify-content: center;
+      }
+    }
+  }
+}

+ 17 - 0
src/app/ai-chat/ai-chat.page.spec.ts

@@ -0,0 +1,17 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { AiChatPage } from './ai-chat.page';
+
+describe('AiChatPage', () => {
+  let component: AiChatPage;
+  let fixture: ComponentFixture<AiChatPage>;
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(AiChatPage);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 95 - 0
src/app/ai-chat/ai-chat.page.ts

@@ -0,0 +1,95 @@
+import { Component, OnInit,OnDestroy } from '@angular/core';
+import { NavController } from '@ionic/angular';
+import * as Parse from 'parse';
+import { interval, Subscription } from 'rxjs';
+
+interface ChatHistoryItem {
+  chatId: number; // Adjust chatId to number type
+  username: string;
+  userChatOrder: number; // Add userChatOrder field
+  userChat: string;
+  aiChat: string;
+}
+
+@Component({
+  selector: 'app-ai-chat',
+  templateUrl: './ai-chat.page.html',
+  styleUrls: ['./ai-chat.page.scss'],
+})
+export class AiChatPage implements OnInit {
+  chatHistory: ChatHistoryItem[] = [];
+  private loadChatHistorySubscription: Subscription | undefined;
+
+  constructor(private navCtrl: NavController) {}
+
+  ngOnInit() {
+    this.loadChatHistory();
+    //每隔一秒检测登录状况
+    this.loadChatHistorySubscription = interval(1000).subscribe(() => {
+      this.loadChatHistory();
+      //console.log(this.username);
+    });
+  }
+  ngOnDestroy() {
+    if (this.loadChatHistorySubscription) {
+      this.loadChatHistorySubscription.unsubscribe();
+    }
+  }
+
+  async loadChatHistory() {
+    try {
+      const Chat = Parse.Object.extend('ai_chat');
+      const query = new Parse.Query(Chat);
+      query.select('chatId', 'username', 'userChatOrder', 'userChat', 'aiChat');
+      query.descending('createdAt'); // Ensure latest chat is fetched first
+      const results = await query.find();
+
+      const groupedChats: { [key: number]: ChatHistoryItem } = results.reduce((acc: { [key: number]: ChatHistoryItem }, chat: any) => {
+        const chatId = chat.get('chatId');
+        if (!acc[chatId]) {
+          acc[chatId] = {
+            chatId,
+            username: chat.get('username'),
+            userChatOrder: chat.get('userChatOrder'),
+            userChat: chat.get('userChat'),
+            aiChat: chat.get('aiChat')
+          };
+        }
+        return acc;
+      }, {});
+
+      this.chatHistory = Object.values(groupedChats).map(chat => {
+        // Filter to only keep the first message for each chatId
+        const firstMessage = results.find(item => item.get('chatId') === chat.chatId && item.get('userChatOrder') === 1);
+        return {
+          chatId: chat.chatId,
+          username: firstMessage?.get('username') || chat.username,
+          userChatOrder: firstMessage?.get('userChatOrder') || chat.userChatOrder,
+          userChat: firstMessage?.get('userChat') || chat.userChat,
+          aiChat: firstMessage?.get('aiChat') || chat.aiChat
+        };
+      });
+
+    } catch (error) {
+      console.error('Error loading chat history:', error);
+    }
+  }
+
+  goToChat(chatId: number) {
+    this.navCtrl.navigateForward(`/tabs/tab2`, {
+      queryParams: { chatId: chatId.toString() } // Ensure chatId is passed as string
+    });
+  }
+
+  async startNewChat() {
+    const Chat = Parse.Object.extend('ai_chat');
+    const query = new Parse.Query(Chat);
+    query.descending('chatId');
+    const latestChat = await query.first();
+    const newChatId = latestChat ? latestChat.get('chatId') + 1 : 1;
+
+    this.navCtrl.navigateForward(`/tabs/tab2`, {
+      queryParams: { chatId: newChatId.toString() } // Ensure chatId is passed as string
+    });
+  }
+}

+ 35 - 27
src/app/app-routing.module.ts

@@ -1,29 +1,37 @@
-import { NgModule } from '@angular/core';
-import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
-
-const routes: Routes = [
-  {
-    path: '',
-    loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule)
-  },
-  {
-    path: 'user',
-    loadChildren: () => import('../modules/user/user.module').then(m => m.UserModule)
-  },
-  {
-    path: "tree",
-    loadChildren: () => import('../modules/tab/tree/tree.module').then(m => m.TreePageModule)
-  },
-  {
-    path: 'bounty-store',
-    loadChildren: () => import('../app/bounty-store/bounty-store.module').then(mod => mod.BountyStorePageModule)
+import { NgModule } from '@angular/core';
+import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
+
+const routes: Routes = [
+  {
+    path: '',
+    loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule)
+  },
+  {
+    path: 'user',
+    loadChildren: () => import('../modules/user/user.module').then(m => m.UserModule)
+  },
+  {
+    path: "tree",
+    loadChildren: () => import('../modules/tab/tree/tree.module').then(m => m.TreePageModule)
+  },
+  {
+    path: 'bounty-store',
+    loadChildren: () => import('../app/bounty-store/bounty-store.module').then(mod => mod.BountyStorePageModule)
+  },
+  {
+    path: 'memo',
+    loadChildren: () => import('../app/memo/memo.module').then(m => m.MemoPageModule)
+  },
  {
+    path: 'ai-chat',
+    loadChildren: () => import('./ai-chat/ai-chat.module').then( m => m.AiChatPageModule)
   },
 
-];
-@NgModule({
-  imports: [
-    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
-  ],
-  exports: [RouterModule]
-})
-export class AppRoutingModule {}
+
+];
+@NgModule({
+  imports: [
+    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
+  ],
+  exports: [RouterModule]
+})
+export class AppRoutingModule {}

+ 1 - 1
src/app/tab1/tab1.page.ts

@@ -30,7 +30,7 @@ export class Tab1Page implements OnInit,OnDestroy {
     //每隔一秒检测登录状况
     this.loadUserDataSubscription = interval(1000).subscribe(() => {
       this.loadUserData();
-      console.log(this.username);
+      //console.log(this.username);
     });
   }
 

+ 30 - 20
src/app/tab2/class-chat-completion.ts

@@ -1,13 +1,17 @@
 export interface TestChatMessage {
-  role: string
-  content: string
+  role: string;
+  content: string;
 }
+
 export class TestChatCompletion {
-  messageList: Array<TestChatMessage>
+  messageList: Array<TestChatMessage>;
+
   constructor(messageList: Array<TestChatMessage>) {
-    this.messageList = messageList
+    this.messageList = messageList;
   }
-  async createCompletionByStream() {
+
+  async createCompletionByStream(userMessage: TestChatMessage): Promise<TestChatMessage> {
+    this.messageList.push(userMessage); // 添加用户消息到消息列表
 
     let token = localStorage.getItem("token");
     let bodyJson = {
@@ -36,8 +40,8 @@ export class TestChatCompletion {
       "credentials": "omit"
     });
 
-    let messageAiReply = ""
-    let messageIndex = this.messageList.length
+    let messageAiReply = "";
+    let messageIndex = this.messageList.length;
     let reader = response.body?.getReader();
     if (!reader) {
       throw new Error("Failed to get the response reader.");
@@ -61,32 +65,38 @@ export class TestChatCompletion {
       for (let i = 0; i < messages.length - 1; i++) {
         let message = messages[i];
 
-
-        let dataText = message.replace("data:\ ", "")
+        // Process the message as needed
+        /**
+         * data: {"id":"chatcmpl-y2PLKqPDnwAFJIj2L5aqdH5TWK9Yv","object":"chat.completion.chunk","created":1696770162,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
+         * data: {"id":"chatcmpl-y2PLKqPDnwAFJIj2L5aqdH5TWK9Yv","object":"chat.completion.chunk","created":1696770162,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
+         * data: [DONE]
+         */
+        let dataText = message.replace("data: ", "");
         if (dataText.startsWith("{")) {
           try {
-            let dataJson = JSON.parse(dataText)
-            console.log(dataJson)
-            messageAiReply += dataJson?.choices?.[0]?.delta?.content || ""
+            let dataJson = JSON.parse(dataText);
+            console.log(dataJson);
+            messageAiReply += dataJson?.choices?.[0]?.delta?.content || "";
             this.messageList[messageIndex] = {
               role: "assistant",
               content: messageAiReply
-            }
-          } catch (err) { }
+            };
+          } catch (err) {}
         }
         if (dataText.startsWith("[")) {
-          console.log(message)
-          console.log("完成")
+          console.log(message);
+          console.log("完成");
           this.messageList[messageIndex] = {
             role: "assistant",
             content: messageAiReply
-          }
-          messageAiReply = ""
+          };
+          messageAiReply = "";
         }
-
         // Clear the processed message from the buffer
         buffer = buffer.slice(message.length + 1);
       }
     }
+
+    return { role: "assistant", content: this.messageList[messageIndex].content };
   }
-}
+}

+ 21 - 14
src/app/tab2/tab2.page.html

@@ -1,22 +1,29 @@
 <ion-header>
   <ion-toolbar>
-    <ion-title>每日记录</ion-title>
+    <ion-title>Chat</ion-title>
   </ion-toolbar>
 </ion-header>
 
 <ion-content>
-  <ion-content>
+  <div class="chat-container">
+    <div *ngFor="let message of messageList" [ngClass]="{'user-message': message.role === 'user', 'ai-message': message.role === 'assistant'}">
+      <div *ngIf="message.role === 'user'" class="avatar-container">
+        <div class="message-content">{{ message.content }}</div>
+        <div class="avatar1">{{ username.charAt(0).toUpperCase() }}</div>
+      </div>
+      <div *ngIf="message.role === 'assistant'" class="avatar-container">
+        <div class="avatar2">AI</div>
+        <div class="message-content">{{ message.content }}</div>
+      </div>
+    </div>
+  </div>
+</ion-content>
+
+<ion-footer>
+  <ion-toolbar>
     <ion-item>
-      <ion-input placeholder="输入消息" [(ngModel)]="userInput"></ion-input>
+      <ion-input [(ngModel)]="userInput" placeholder="Type a message..."></ion-input>
+      <ion-button (click)="sendMessage()">Send</ion-button>
     </ion-item>
-    <ion-button expand="block" (click)="sendMessage()">发送</ion-button>
-    <ion-card *ngFor="let message of messageList">
-      <ion-card-header>
-        {{message?.role}}
-      </ion-card-header>
-      <ion-card-content>
-        {{ message?.content }}
-      </ion-card-content>
-    </ion-card>
-  </ion-content>
-</ion-content>
+  </ion-toolbar>
+</ion-footer>

+ 75 - 6
src/app/tab2/tab2.page.scss

@@ -1,9 +1,78 @@
-ion-label.ai {
-  color: blue;
-  text-align: left;
+.chat-container {
+  display: flex;
+  flex-direction: column;
+  padding: 10px;
 }
 
-ion-label.user {
-  color: green;
-  text-align: right;
+.avatar-container {
+  display: flex;
+  align-items: flex-start;
+  margin: 5px 0;
+  padding: 0px;
+}
+
+.user-message {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  margin: 5px 0;
+}
+
+.ai-message {
+  display: flex;
+  justify-content: flex-start;
+  align-items: center;
+  margin: 5px 0;
+}
+
+.message-content {
+  padding: 10px;
+  border-radius: 5px;
+  word-break: break-word; /* 处理消息内容换行问题 */
+  max-width: 330px; /* 设置消息内容的最大宽度,超过这个宽度会自动换行 */
+}
+
+
+.user-message .message-content {
+  background-color: #e0f7fa; /* 更合适的用户消息背景颜色 */
+}
+
+.ai-message .message-content {
+  background-color: #ffecb3; /* 更合适的AI消息背景颜色 */
+}
+
+.avatar1 {
+  width: 40px;
+  height: 40px;
+  border-radius: 50%;
+  font-size: 20px;
+  font-weight: bold;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-left: 10px;
+  border: 2px solid #ccc; /* 添加边框 */
+}
+
+.avatar2 {
+  width: 40px;
+  height: 40px;
+  border-radius: 50%;
+  font-size: 20px;
+  font-weight: bold;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-right: 10px;
+  border: 2px solid #ccc; /* 添加边框 */
+}
+
+.user-message .avatar1 {
+  background-color: #007bff;
+  color: white;
+}
+
+.ai-message .avatar2 {
+  background-color: #007bff;
+  color: white;
 }

+ 121 - 18
src/app/tab2/tab2.page.ts

@@ -1,29 +1,132 @@
-import { Component,OnInit } from '@angular/core';
-import { AlertController } from '@ionic/angular';
-// 引用FmodeChatCompletion类
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import * as Parse from 'parse';
 import { TestChatCompletion, TestChatMessage } from './class-chat-completion';
+import { ChangeDetectorRef } from '@angular/core';
+
 @Component({
   selector: 'app-tab2',
   templateUrl: './tab2.page.html',
   styleUrls: ['./tab2.page.scss'],
 })
 export class Tab2Page implements OnInit {
-  messages: { sender: string, text: string }[] = [];
-  newMessage: string = '';
-  messageList:Array<TestChatMessage> = []
-  userInput:string = ""
-  completion:TestChatCompletion
-  constructor(private alertController: AlertController) {this.completion = new TestChatCompletion(this.messageList)}
-  ngOnInit() {}
-  //ai对话
-  sendMessage(){
-    this.messageList.push({
-      role:"user",
+  chatId: number | null = null;
+  messageList: Array<TestChatMessage> = [];
+  userInput: string = '';
+  username: string = ''; // 用户名
+  completion: TestChatCompletion;
+
+  constructor(
+    private route: ActivatedRoute,
+    private router: Router,
+    private cd: ChangeDetectorRef // 注入ChangeDetectorRef
+  ) {
+    this.completion = new TestChatCompletion(this.messageList);
+  }
+
+  ngOnInit() {
+    this.loadUserData(); // 加载用户名
+    this.route.queryParams.subscribe(params => {
+      if (params['chatId']) {
+        this.chatId = +params['chatId'];
+        this.loadChatHistory(this.chatId);
+      } else {
+        this.chatId = null;
+        this.messageList = [];
+      }
+    });
+  }
+
+  async loadUserData() {
+    const currentUser = Parse.User.current();
+    if (currentUser) {
+      this.username = currentUser.getUsername()!;
+    } else {
+      this.username = '未登录';
+    }
+  }
+
+  async loadChatHistory(chatId: number | null) {
+    if (!chatId) return;
+
+    const Chat = Parse.Object.extend('ai_chat');
+    const query = new Parse.Query(Chat);
+    query.equalTo('chatId', chatId);
+    query.ascending('userChatOrder');
+
+    try {
+      const results = await query.find();
+      this.messageList = results.flatMap(chat => [
+        { role: 'user', content: chat.get('userChat') },
+        { role: 'assistant', content: chat.get('aiChat') }
+      ]).filter(message => message.content); // 过滤掉内容为空的消息
+      this.cd.detectChanges(); // 触发变更检测
+    } catch (error) {
+      console.error('Error loading chat history:', error);
+    }
+  }
+
+  async sendMessage() {
+    const userMessage: TestChatMessage = {
+      role: 'user',
       content: this.userInput
-    })
-    this.userInput = ""
-    this.completion.createCompletionByStream()
+    };
+    this.messageList.push(userMessage);
+    this.userInput = '';
 
+    this.cd.detectChanges(); // 触发变更检测
+
+    // 调用AI接口处理消息
+    const aiMessage = await this.completion.createCompletionByStream(userMessage);
+
+    if (this.chatId !== null) {
+      await this.saveMessagesToDatabase(userMessage, aiMessage);
+    }
+
+    this.messageList.push(aiMessage);
+    this.cd.detectChanges(); // 触发变更检测
+  }
+
+  async saveMessagesToDatabase(userMessage: TestChatMessage, aiMessage: TestChatMessage) {
+    if (this.chatId === null) return;
+
+    const Chat = Parse.Object.extend('ai_chat');
+    const chat = new Chat();
+
+    chat.set('username', this.username); // 设置用户名
+    chat.set('chatId', this.chatId); // 设置chatId
+
+    // 查询最后一个userChatOrder以确定此消息的新顺序
+    const lastMessage = await this.getLastMessageInChat(this.chatId);
+    let userChatOrder = 1; // 如果是对话的第一条消息,则默认为1
+    if (lastMessage) {
+      userChatOrder = lastMessage.get('userChatOrder') + 1;
+    }
+    chat.set('userChatOrder', userChatOrder); // 设置userChatOrder
+
+    chat.set('userChat', userMessage.content); // 设置userChat
+    chat.set('aiChat', aiMessage.content); // 设置aiChat
+
+    try {
+      await chat.save();
+    } catch (error) {
+      console.error('Error saving message:', error);
+    }
   }
 
-}
+  async getLastMessageInChat(chatId: number): Promise<Parse.Object | undefined> {
+    const Chat = Parse.Object.extend('ai_chat');
+    const query = new Parse.Query(Chat);
+    query.equalTo('chatId', chatId);
+    query.descending('userChatOrder');
+    query.limit(1); // 限制为1条结果以获取最后一条消息
+
+    try {
+      const result = await query.first();
+      return result; // 返回最后一条消息对象,如果找不到则返回undefined
+    } catch (error) {
+      console.error('Error getting last message:', error);
+      return undefined;
+    }
+  }
+}

+ 4 - 0
src/app/tabs/tabs-routing.module.ts

@@ -35,6 +35,10 @@ const routes: Routes = [
         path: 'bounty-store',
         loadChildren: () => import('../bounty-store/bounty-store.module').then(mod => mod.BountyStorePageModule)
       },
+      {
+        path: 'ai-chat',
+        loadChildren: () => import('../ai-chat/ai-chat.module').then( m => m.AiChatPageModule)
+      },
       {
         path: '',
         redirectTo: '/tabs/tab1',

+ 1 - 1
src/app/tabs/tabs.page.html

@@ -12,7 +12,7 @@
     </ion-tab-button>
 
     
-    <ion-tab-button tab="tab2" href="/tabs/tab2">
+    <ion-tab-button tab="ai-chat" href="/tabs/ai-chat">
       <ion-icon aria-hidden="true" name="chatbox-ellipses-outline"></ion-icon>
       <ion-label>ai对话</ion-label>
     </ion-tab-button>

+ 0 - 0
src/assets/img/sunny.jpg → src/assets/img/sun.jpg