0225268 преди 4 месеца
родител
ревизия
302485d01b
променени са 5 файла, в които са добавени 156 реда и са изтрити 3 реда
  1. 14 0
      src/app/aichat/aichat.page.html
  2. 29 1
      src/app/aichat/aichat.page.ts
  3. 111 0
      src/app/aichat/chat-completion.ts
  4. 1 1
      src/app/tab3/tab3.page.html
  5. 1 1
      src/app/tab3/tab3.page.scss

+ 14 - 0
src/app/aichat/aichat.page.html

@@ -19,6 +19,20 @@
     </ion-item>
   </ion-list>
 
+  <ion-list lines="none">
+    <ion-item *ngFor="let message of messageList">
+      <ion-avatar slot="start">
+        <ion-img *ngIf="message?.role=='assistant'" src="assets/img/d.webp"></ion-img>
+        <ion-img *ngIf="message?.role=='user'" src="assets/img/a.png"></ion-img>
+      </ion-avatar>
+      <ion-label>
+        <h3>{{ message.role }}</h3>
+        <p>{{ message.content }}</p>
+        <!-- <p class="message-time">{{ message.timestamp | date: 'medium' }}</p> -->
+      </ion-label>
+    </ion-item>
+  </ion-list>
+
   <ion-item>
     <ion-input placeholder="请输入消息" [(ngModel)]="newMessage"></ion-input>
     <ion-button slot="end" (click)="sendMessage()">发送</ion-button>

+ 29 - 1
src/app/aichat/aichat.page.ts

@@ -1,4 +1,5 @@
 import { Component, OnInit } from '@angular/core';
+import { TestRxjsChatCompletion, TestRxjsChatMessage } from './chat-completion';
 
 @Component({
   selector: 'app-aichat',
@@ -13,7 +14,7 @@ export class AIChatPage implements OnInit {
   ];
   newMessage: string = '';
 
-  sendMessage() {
+  sendMessage123() {
     if (this.newMessage.trim() !== '') {
       this.chatMessages.push({ sender: 'User', senderAvatar: 'assets/user-avatar.png', content: this.newMessage, timestamp: new Date() });
       this.newMessage = ''; // 清空输入框
@@ -23,4 +24,31 @@ export class AIChatPage implements OnInit {
   ngOnInit() {
   }
 
+  messageList:Array<TestRxjsChatMessage> = []
+  sendMessage(){
+    this.messageList.push({
+      role:"user",
+      content: this.newMessage
+    })
+    this.newMessage = ""
+    
+    // this.completion.createCompletionByStream()
+
+    // messageList在competion内部,已经赋予了完整的message
+    // 下方暴露出来的可订阅内容,主要是用于关键字过滤,或者其他开发逻辑的续写
+    let testChatCompletion = new TestRxjsChatCompletion(this.messageList);
+    testChatCompletion.createCompletionByStream().subscribe({
+        next: ({ content, cumulativeContent, done }) => {
+            console.log(`Content: ${content}`);
+            console.log(`Cumulative Content: ${cumulativeContent}`);
+            if (done) {
+                console.log('Stream completed');
+            }
+        },
+        error: err => console.error(err),
+        complete: () => console.log('Observable completed')
+    });
+
+  }
+
 }

+ 111 - 0
src/app/aichat/chat-completion.ts

@@ -0,0 +1,111 @@
+import { Observable, from, of } from 'rxjs';
+import { switchMap, map, catchError, finalize } from 'rxjs/operators';
+
+export interface TestRxjsChatMessage {
+    role: string;
+    content: string;
+}
+
+export class TestRxjsChatCompletion {
+    messageList: Array<TestRxjsChatMessage>;
+
+    constructor(messageList: Array<TestRxjsChatMessage>) {
+        this.messageList = messageList;
+    }
+
+    createCompletionByStream(options?:{
+        model?:string
+    }): Observable<{ content: string, cumulativeContent: string, done: boolean }> {
+        const token = localStorage.getItem("token");
+        const bodyJson = {
+            "token": `Bearer ${token}`,
+            "messages": this.messageList,
+            "model": options?.model || "fmode-3.6-16k",
+            "temperature": 0.5,
+            "presence_penalty": 0,
+            "frequency_penalty": 0,
+            "top_p": 1,
+            "stream": true
+        };
+
+        return from(fetch("https://test.fmode.cn/api/apig/aigc/gpt/v1/chat/completions", {
+            "headers": {
+                "accept": "text/event-stream",
+                "sec-fetch-dest": "empty",
+                "sec-fetch-mode": "cors",
+                "sec-fetch-site": "same-site"
+            },
+            "referrer": "https://ai.fmode.cn/",
+            "referrerPolicy": "strict-origin-when-cross-origin",
+            "body": JSON.stringify(bodyJson),
+            "method": "POST",
+            "mode": "cors",
+            "credentials": "omit"
+        })).pipe(
+            switchMap(response => {
+                const reader = response.body?.getReader();
+                if (!reader) {
+                    throw new Error("Failed to get the response reader.");
+                }
+                const decoder = new TextDecoder();
+                let buffer = "";
+                let messageAiReply = "";
+                let messageIndex = this.messageList.length;
+
+                return new Observable<{ content: string, cumulativeContent: string, done: boolean }>(observer => {
+                    const read = () => {
+                        reader.read().then(({ done, value }) => {
+                            if (done) {
+                                observer.complete();
+                                return;
+                            }
+
+                            buffer += decoder.decode(value);
+                            let messages = buffer.split("\n");
+
+                            for (let i = 0; i < messages.length - 1; i++) {
+                                let message = messages[i];
+                                let dataText = message.replace("data: ", "");
+
+                                if (dataText.startsWith("{")) {
+                                    try {
+                                        let dataJson = JSON.parse(dataText);
+                                        let content = dataJson?.choices?.[0]?.delta?.content || "";
+                                        messageAiReply += content;
+                                        this.messageList[messageIndex] = {
+                                            role: "assistant",
+                                            content: messageAiReply
+                                        };
+                                        observer.next({ content, cumulativeContent: messageAiReply, done: false });
+                                    } catch (err) { }
+                                }
+
+                                if (dataText.startsWith("[")) {
+                                    this.messageList[messageIndex] = {
+                                        role: "assistant",
+                                        content: messageAiReply
+                                    };
+                                    observer.next({ content: "", cumulativeContent: messageAiReply, done: true });
+                                    messageAiReply = "";
+                                }
+
+                                buffer = buffer.slice(message.length + 1);
+                            }
+
+                            read();
+                        }).catch(err => observer.error(err));
+                    };
+
+                    read();
+                });
+            }),
+            catchError(err => {
+                console.error(err);
+                return of({ content: "", cumulativeContent: "", done: true });
+            }),
+            finalize(() => {
+                console.log("Stream completed");
+            })
+        );
+    }
+}

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

@@ -11,7 +11,7 @@
 </ion-header>
 
 <ion-content>
-<div style="background-color: rgb(0, 132, 255);">
+<div style="background-color: rgb(245, 246, 246);">
   <ion-searchbar placeholder="输入关键字搜索"></ion-searchbar>
   <ion-card class="card">
     <ion-card-header >

+ 1 - 1
src/app/tab3/tab3.page.scss

@@ -14,7 +14,7 @@ ion-avatar {
   width: 365px;
   height: 250px;
   background-color: #4158D0;
-  background-image: linear-gradient(43deg, #41d0d0 0%, #0c94d8 46%, #baff70 100%);
+  background-image: linear-gradient(43deg, #41d0d0 0%, #0c6bd8 46%, #baff70 100%);
   border-radius: 8px;
   color: white;
   overflow: hidden;