Selaa lähdekoodia

feat: new lib user/ ncloud.ts

未来全栈 6 kuukautta sitten
vanhempi
commit
c8082071d9

+ 4 - 1
AiStudy-app/angular.json

@@ -126,7 +126,10 @@
     }
   },
   "cli": {
-    "schematicCollections": ["@ionic/angular-toolkit"]
+    "schematicCollections": [
+      "@ionic/angular-toolkit"
+    ],
+    "analytics": false
   },
   "schematics": {
     "@ionic/angular-toolkit:component": {

+ 2 - 1
AiStudy-app/src/app/pages/learning-history/learning-history.page.ts

@@ -14,6 +14,7 @@ import {
 } from '@ionic/angular/standalone';
 import { NgFor, DatePipe } from '@angular/common';
 import Parse from 'parse';
+import { CloudUser } from 'src/lib/ncloud';
 
 interface LearningPlan {
   id: string;
@@ -55,7 +56,7 @@ export class LearningHistoryPage implements OnInit {
 
   async loadLearningPlans() {
     try {
-      const currentUser = Parse.User.current();
+      const currentUser = new CloudUser();
       if (!currentUser) {
         throw new Error('User not logged in');
       }

+ 36 - 30
AiStudy-app/src/app/tab1/tab1.page.ts

@@ -8,6 +8,7 @@ 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';
 
 // 定义消息接口
 interface ChatMessage {
@@ -117,39 +118,44 @@ export class Tab1Page {
   // 添加加载教师方法
   async loadTeachers() {
     try {
-      const currentUser = await Parse.User.current();
+      const currentUser = new CloudUser();
       if (currentUser) {
-        const userId = currentUser.id;
-        const customTeachers = await this.dbService.getUserCustomTeachers(userId);
-        
-        // 将自定义教师转换为Teacher格式,确保与预设教师格式完全一致
-        const customTeachersList: Teacher[] = customTeachers.map(ct => ({
-          id: ct.objectId!, // 使用 objectId 作为唯一标识
-          name: ct.name,
-          description: ct.description,
-          systemPrompt: ct.systemPrompt,
-          isCustom: true,  // 标记为自定义教师
-          objectId: ct.objectId
-        }));
+        let userId = currentUser.id;
+        if(userId){
+          const customTeachers = await this.dbService.getUserCustomTeachers(userId);
+          
+          // 将自定义教师转换为Teacher格式,确保与预设教师格式完全一致
+          const customTeachersList: Teacher[] = customTeachers.map(ct => ({
+            id: ct.objectId!, // 使用 objectId 作为唯一标识
+            name: ct.name,
+            description: ct.description,
+            systemPrompt: ct.systemPrompt,
+            isCustom: true,  // 标记为自定义教师
+            objectId: ct.objectId
+          }));
+
+          // 获取预设教师(排除自定义教师入口)
+          const defaultTeachers = this.teachers.filter(t => t.id !== 'custom');
+          
+          // 添加自定义教师入口
+          const customTeacherEntry: Teacher = {
+            id: 'custom',
+            name: '自定义教师',
+            description: '创建您自己的AI教师',
+            systemPrompt: '',
+            isCustom: false
+          };
+
+          // 更新教师列表
+          this.teachers = [
+            ...defaultTeachers,        // 预设教师
+            ...customTeachersList,     // 自定义教师
+            customTeacherEntry         // 自定义教师入口
+          ];
+
+        }
 
-        // 获取预设教师(排除自定义教师入口)
-        const defaultTeachers = this.teachers.filter(t => t.id !== 'custom');
-        
-        // 添加自定义教师入口
-        const customTeacherEntry: Teacher = {
-          id: 'custom',
-          name: '自定义教师',
-          description: '创建您自己的AI教师',
-          systemPrompt: '',
-          isCustom: false
-        };
 
-        // 更新教师列表
-        this.teachers = [
-          ...defaultTeachers,        // 预设教师
-          ...customTeachersList,     // 自定义教师
-          customTeacherEntry         // 自定义教师入口
-        ];
       }
     } catch (error) {
       console.error('加载自定义教师失败:', error);

+ 16 - 16
AiStudy-app/src/app/tab3/tab3.page.ts

@@ -40,6 +40,7 @@ import {
   starOutline
 } from 'ionicons/icons';
 import Parse from 'parse';
+import { CloudUser } from 'src/lib/ncloud';
 
 @Component({
   selector: 'app-tab3',
@@ -116,7 +117,7 @@ export class Tab3Page implements OnInit {
 
   ngOnInit() {
     // 检查是否已登录
-    const currentUser = Parse.User.current();
+    const currentUser = new CloudUser();
     if (currentUser) {
       this.isLoggedIn = true;
       this.userName = currentUser.get('username');
@@ -144,10 +145,8 @@ export class Tab3Page implements OnInit {
       }
 
       // 执行登录
-      const user = await Parse.User.logIn(this.loginData.username, this.loginData.password);
-      
-      // 只保存 Parse token,不更改 AI token
-      localStorage.setItem('parseToken', user.getSessionToken());
+      let user = new CloudUser()
+      user.login(this.loginData.username, this.loginData.password);
       
       // 更新界面状态
       this.isLoggedIn = true;
@@ -239,19 +238,19 @@ export class Tab3Page implements OnInit {
       }
 
       // 创建新用户
-      const user = new Parse.User();
-      user.set('username', this.registerData.username.trim());
-      user.set('email', this.registerData.email.trim().toLowerCase());
-      user.set('password', this.registerData.password);
+      const user = new CloudUser();
+      user.set({'username': this.registerData.username.trim()});
+      user.set({'email': this.registerData.email.trim().toLowerCase()});
+      user.set({'password': this.registerData.password});
       
       // 设置初始用户信息
-      user.set('level', 'LV.1 新手学习者');
-      user.set('avatar', 'assets/default-avatar.png');
-      user.set('achievementCount', 0);
-      user.set('learningDays', 0);
-      user.set('completionRate', 0);
+      user.set({'level': 'LV.1 新手学习者'});
+      user.set({'avatar': 'assets/default-avatar.png'});
+      user.set({'achievementCount': 0});
+      user.set({'learningDays': 0});
+      user.set({'completionRate': 0});
 
-      await user.signUp();
+      user.signUp(this.registerData.username.trim(),this.registerData.password);
 
       // 注册成功后自动登录
       this.isLoggedIn = true;
@@ -324,7 +323,8 @@ export class Tab3Page implements OnInit {
           text: '确定',
           handler: async () => {
             try {
-              await Parse.User.logOut();
+              let user = new CloudUser();
+              user.logout();
               // 只清除 Parse token,保留 AI token
               localStorage.removeItem('parseToken');
               

+ 408 - 0
AiStudy-app/src/lib/ncloud.ts

@@ -0,0 +1,408 @@
+// CloudObject.ts
+export class CloudObject {
+    className: string;
+    id: string | null = null;
+    createdAt:any;
+    updatedAt:any;
+    data: Record<string, any> = {};
+
+    constructor(className: string) {
+        this.className = className;
+    }
+
+    toPointer() {
+        return { "__type": "Pointer", "className": this.className, "objectId": this.id };
+    }
+
+    set(json: Record<string, any>) {
+        Object.keys(json).forEach(key => {
+            if (["objectId", "id", "createdAt", "updatedAt"].indexOf(key) > -1) {
+                return;
+            }
+            this.data[key] = json[key];
+        });
+    }
+
+    get(key: string) {
+        return this.data[key] || null;
+    }
+
+    async save() {
+        let method = "POST";
+        let url = `https://dev.fmode.cn/parse/classes/${this.className}`;
+
+        // 更新
+        if (this.id) {
+            url += `/${this.id}`;
+            method = "PUT";
+        }
+
+        const body = JSON.stringify(this.data);
+        const response = await fetch(url, {
+            headers: {
+                "content-type": "application/json;charset=UTF-8",
+                "x-parse-application-id": "dev"
+            },
+            body: body,
+            method: method,
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+        }
+        if (result?.objectId) {
+            this.id = result?.objectId;
+        }
+        return this;
+    }
+
+    async destroy() {
+        if (!this.id) return;
+        const response = await fetch(`https://dev.fmode.cn/parse/classes/${this.className}/${this.id}`, {
+            headers: {
+                "x-parse-application-id": "dev"
+            },
+            body: null,
+            method: "DELETE",
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const result = await response?.json();
+        if (result) {
+            this.id = null;
+        }
+        return true;
+    }
+}
+
+// CloudQuery.ts
+export class CloudQuery {
+    className: string;
+    queryParams: Record<string, any> = {};
+
+    constructor(className: string) {
+        this.className = className;
+    }
+
+    include(...fileds:string[]) {
+        this.queryParams["include"] = fileds;
+    }
+    greaterThan(key: string, value: any) {
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$gt"] = value;
+    }
+
+    greaterThanAndEqualTo(key: string, value: any) {
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$gte"] = value;
+    }
+
+    lessThan(key: string, value: any) {
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$lt"] = value;
+    }
+
+    lessThanAndEqualTo(key: string, value: any) {
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$lte"] = value;
+    }
+
+    equalTo(key: string, value: any) {
+        if (!this.queryParams["where"]) this.queryParams["where"] = {};
+        this.queryParams["where"][key] = value;
+    }
+
+    async get(id: string) {
+        const url = `https://dev.fmode.cn/parse/classes/${this.className}/${id}?`;
+
+        const response = await fetch(url, {
+            headers: {
+                "if-none-match": "W/\"1f0-ghxH2EwTk6Blz0g89ivf2adBDKY\"",
+                "x-parse-application-id": "dev"
+            },
+            body: null,
+            method: "GET",
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const json = await response?.json();
+        return json || {};
+    }
+
+    async find():Promise<Array<CloudObject>> {
+        let url = `https://dev.fmode.cn/parse/classes/${this.className}?`;
+
+        let queryStr = ``
+        Object.keys(this.queryParams).forEach(key=>{
+            let paramStr = JSON.stringify(this.queryParams[key]);
+            if(key=="include"){
+                paramStr = this.queryParams[key]?.join(",")
+            }
+            if(queryStr) {
+                url += `${key}=${paramStr}`;
+            }else{
+                url += `&${key}=${paramStr}`;
+            }
+        })
+        // if (Object.keys(this.queryParams["where"]).length) {
+            
+        // }
+
+        const response = await fetch(url, {
+            headers: {
+                "if-none-match": "W/\"1f0-ghxH2EwTk6Blz0g89ivf2adBDKY\"",
+                "x-parse-application-id": "dev"
+            },
+            body: null,
+            method: "GET",
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const json = await response?.json();
+        let list = json?.results || []
+        let objList = list.map((item:any)=>this.dataToObj(item))
+        return objList || [];
+    }
+
+
+    async first() {
+        let url = `https://dev.fmode.cn/parse/classes/${this.className}?`;
+
+        if (Object.keys(this.queryParams["where"]).length) {
+            const whereStr = JSON.stringify(this.queryParams["where"]);
+            url += `where=${whereStr}`;
+        }
+
+        const response = await fetch(url, {
+            headers: {
+                "if-none-match": "W/\"1f0-ghxH2EwTk6Blz0g89ivf2adBDKY\"",
+                "x-parse-application-id": "dev"
+            },
+            body: null,
+            method: "GET",
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const json = await response?.json();
+        const exists = json?.results?.[0] || null;
+        if (exists) {
+            let existsObject = this.dataToObj(exists)
+            return existsObject;
+        }
+        return null
+    }
+
+    dataToObj(exists:any):CloudObject{
+        let existsObject = new CloudObject(this.className);
+        existsObject.set(exists);
+        existsObject.id = exists.objectId;
+        existsObject.createdAt = exists.createdAt;
+        existsObject.updatedAt = exists.updatedAt;
+        return existsObject;
+    }
+}
+
+// CloudUser.ts
+export class CloudUser extends CloudObject {
+    constructor() {
+        super("_User"); // 假设用户类在Parse中是"_User"
+        // 读取用户缓存信息
+        let userCacheStr = localStorage.getItem("NCloud/dev/User")
+        if(userCacheStr){
+            let userData = JSON.parse(userCacheStr)
+            // 设置用户信息
+            this.id = userData?.objectId;
+            this.sessionToken = userData?.sessionToken;
+            this.data = userData; // 保存用户数据
+        }
+    }
+
+    sessionToken:string|null = ""
+    /** 获取当前用户信息 */
+    async current() {
+        if (!this.sessionToken) {
+            console.error("用户未登录");
+            return null;
+        }
+        return this;
+        // const response = await fetch(`https://dev.fmode.cn/parse/users/me`, {
+        //     headers: {
+        //         "x-parse-application-id": "dev",
+        //         "x-parse-session-token": this.sessionToken // 使用sessionToken进行身份验证
+        //     },
+        //     method: "GET"
+        // });
+
+        // const result = await response?.json();
+        // if (result?.error) {
+        //     console.error(result?.error);
+        //     return null;
+        // }
+        // return result;
+    }
+
+    /** 登录 */
+    async login(username: string, password: string):Promise<CloudUser|null> {
+        const response = await fetch(`https://dev.fmode.cn/parse/login`, {
+            headers: {
+                "x-parse-application-id": "dev",
+                "Content-Type": "application/json"
+            },
+            body: JSON.stringify({ username, password }),
+            method: "POST"
+        });
+
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+            return null;
+        }
+        
+        // 设置用户信息
+        this.id = result?.objectId;
+        this.sessionToken = result?.sessionToken;
+        this.data = result; // 保存用户数据
+        // 缓存用户信息
+        console.log(result)
+        localStorage.setItem("NCloud/dev/User",JSON.stringify(result))
+        return this;
+    }
+
+    /** 登出 */
+    async logout() {
+        if (!this.sessionToken) {
+            console.error("用户未登录");
+            return;
+        }
+
+        const response = await fetch(`https://dev.fmode.cn/parse/logout`, {
+            headers: {
+                "x-parse-application-id": "dev",
+                "x-parse-session-token": this.sessionToken
+            },
+            method: "POST"
+        });
+
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+            return false;
+        }
+
+        // 清除用户信息
+        localStorage.removeItem("NCloud/dev/User")
+        this.id = null;
+        this.sessionToken = null;
+        this.data = {};
+        return true;
+    }
+
+    /** 注册 */
+    async signUp(username: string, password: string, additionalData: Record<string, any> = {}) {
+        const userData = {
+            username,
+            password,
+            ...additionalData // 合并额外的用户数据
+        };
+
+        const response = await fetch(`https://dev.fmode.cn/parse/users`, {
+            headers: {
+                "x-parse-application-id": "dev",
+                "Content-Type": "application/json"
+            },
+            body: JSON.stringify(userData),
+            method: "POST"
+        });
+
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+            return null;
+        }
+
+        // 设置用户信息
+        // 缓存用户信息
+        console.log(result)
+        localStorage.setItem("NCloud/dev/User",JSON.stringify(result))
+        this.id = result?.objectId;
+        this.sessionToken = result?.sessionToken;
+        this.data = result; // 保存用户数据
+        return this;
+    }
+
+    override async save() {
+        let method = "POST";
+        let url = `https://dev.fmode.cn/parse/users`;
+    
+        // 更新用户信息
+        if (this.id) {
+            url += `/${this.id}`;
+            method = "PUT";
+        }
+    
+        let data:any = JSON.parse(JSON.stringify(this.data))
+        delete data.createdAt
+        delete data.updatedAt
+        delete data.ACL
+        delete data.objectId
+        const body = JSON.stringify(data);
+        let headersOptions:any = {
+            "content-type": "application/json;charset=UTF-8",
+            "x-parse-application-id": "dev",
+            "x-parse-session-token": this.sessionToken, // 添加sessionToken以进行身份验证
+        }
+        const response = await fetch(url, {
+            headers: headersOptions,
+            body: body,
+            method: method,
+            mode: "cors",
+            credentials: "omit"
+        });
+    
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+        }
+        if (result?.objectId) {
+            this.id = result?.objectId;
+        }
+        localStorage.setItem("NCloud/dev/User",JSON.stringify(this.data))
+        return this;
+    }
+}
+
+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"
+        }
+        if(body||options?.body){
+            reqOpts.body = JSON.stringify(body || options?.body);
+            reqOpts.json = true;
+        }
+        let host = `https://dev.fmode.cn`
+        // host = `http://127.0.0.1:1337`
+        let url = `${host}/api/`+path
+        console.log(url,reqOpts)
+        const response = await fetch(url,reqOpts);
+        let json = await response.json();
+        return json
+    }
+}

+ 262 - 0
AiStudy-app/src/lib/story.ts

@@ -0,0 +1,262 @@
+// import pdf from 'pdf-parse';
+// import fs from 'fs';
+import { CloudApi, CloudObject, CloudQuery } from 'src/lib/ncloud';
+import mammoth from "mammoth";
+import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
+import { Document } from '@langchain/core/documents';
+
+import * as tf from '@tensorflow/tfjs-core';
+// import "@tensorflow/tfjs-backend-cpu";
+// import '@tensorflow/tfjs-backend-webgpu';
+import '@tensorflow/tfjs-backend-webgl';
+// import '@tensorflow/tfjs-backend-wasm';
+import { TensorFlowEmbeddings } from "@langchain/community/embeddings/tensorflow";
+
+export class AgentStory{
+
+    story:CloudObject|undefined
+    // 文件标题
+    title:string|undefined = ""
+    // 文档标签
+    tags:Array<string>|undefined
+    // 文件源地址
+    url:string|undefined = ""
+    // 文档完整纯文本内容
+    content:string|undefined = ""
+    // 文档hash唯一值
+    hash:string|undefined = ""
+    // 文档分割后的列表
+    docList:Array<Document|any> = []
+
+    constructor(metadata:{
+        url:string,
+        title?:string,
+        tags?:Array<string>
+    }){
+        this.url = metadata.url
+        this.title = metadata.title
+        this.tags = metadata.tags
+        setBackend()
+    }
+    async save(){
+        if(!this.hash){ return }
+        let query = new CloudQuery("Story");
+        query.equalTo("hash",this.hash);
+        let story = await query.first();
+        if(!story?.id){
+            story = new CloudObject("Story");
+        }
+        story.set({
+            title: this.title,
+            url: this.url,
+            content: this.content,
+            hash: this.hash,
+            tags:this.tags
+        })
+        this.story = await story.save();
+    }
+    async loader(url:string){
+        let api = new CloudApi();
+
+        let result;
+        if(url?.endsWith(".docx")){
+            result = await this.loadDocx(url)
+        }
+        if(!result){
+            result = await api.fetch("agent/loader",{url:url})
+        }
+        this.content = result?.data || null
+        if(this.content){
+            this.url = url
+        }
+        this.save();
+        return this.content
+    }
+
+    async loadDocx(url:string){
+        let data:any
+        const response = await fetch(url);
+
+        const arrayBuffer:any = await response.arrayBuffer();
+        
+        let text;
+        try {
+            text = await mammoth.extractRawText({arrayBuffer:arrayBuffer}); // 浏览器 直接传递 arrayBuffer
+        } catch (err) {
+            console.error(err);
+        }
+
+        this.hash = await arrayBufferToHASH(arrayBuffer)
+
+        // let html = mammoth.convertToHtml(buffer)
+        data = text?.value || "";
+        // 正则匹配所有 多个\n换行的字符 替换成一次换行
+        data = data.replaceAll(/\n+/g,"\n") // 剔除多余换行
+        return {data}
+    }
+    async splitter(options?:{
+        chunkSize:number,
+        chunkOverlap:number
+    }){
+        if(!this.content) return
+        // 默认:递归字符文本分割器
+        let splitter = new RecursiveCharacterTextSplitter({
+            chunkSize: options?.chunkSize || 500,
+            chunkOverlap: options?.chunkOverlap || 150,
+        });
+          
+        let docOutput = await splitter.splitDocuments([
+            new Document({ pageContent: this.content }),
+        ]);
+        console.log(docOutput)
+        this.docList = docOutput
+        return this.docList
+    }
+
+    /**
+     * 文本向量提取
+     * @see
+     * https://js.langchain.com/docs/integrations/text_embedding/tensorflow/
+     * @returns 
+     */
+    //  TensorFlow embedding vector(512) NOT NULL -- NOTE: 512 for Tensorflow
+    //  OpenAI embedding vector(1536) NOT NULL -- NOTE: 1536 for ChatGPT
+    async embedings(){
+        if(!this.docList?.length){return}
+        const embeddings = new TensorFlowEmbeddings();
+        let documentRes = await embeddings.embedDocuments(this.docList?.map(item=>item.pageContent));
+        console.log(documentRes);
+
+        // 向量持久化
+        documentRes.forEach(async (vector512:any,index)=>{
+            /**
+             * metadata
+             * pageContent
+             */
+            let document = this.docList[index]
+            this.docList[index].vector512 = vector512
+            let hash = await arrayBufferToHASH(stringToArrayBuffer(document?.pageContent))
+            let query = new CloudQuery("Document");
+            query.equalTo("hash",hash);
+            let docObj = await query.first()
+            if(!docObj?.id){
+                docObj = new CloudObject("Document");
+            }
+            docObj.set({
+                metadata:document?.metadata,
+                pageContent:document?.pageContent,
+                vector512:vector512,
+                hash:hash,
+                story:this.story?.toPointer(),
+            })
+            docObj.save();
+        })
+        return documentRes;
+    }
+    async destoryAllDocument(){
+        if(this.story?.id){
+            let query = new CloudQuery("Document");
+            query.equalTo("story",this.story?.id);
+            let docList = await query.find();
+            docList.forEach(doc=>{
+                doc.destroy();
+            })
+        }
+        
+    }
+}
+
+export async function fetchFileBuffer(url: string): Promise<Buffer> {
+    const response = await fetch(url);
+
+    if (!response.ok) {
+        throw new Error(`Failed to fetch file: ${response.status} ${response.statusText}`);
+    }
+
+    const arrayBuffer = await response.arrayBuffer();
+    return Buffer.from(arrayBuffer);
+}
+
+async function setBackend(){
+
+        let backend
+        let WebGPU = (navigator as any).gpu
+        if (WebGPU) {
+          // WebGPU is supported
+          // console.log(WebGPU)
+          backend = "webgpu"
+        } else {
+          // WebGPU is not supported
+        }
+        let glcanvas = document.createElement('canvas');
+        let WebGL = glcanvas.getContext('webgl') || glcanvas.getContext('experimental-webgl');
+        if (WebGL) {
+          // console.log(WebGL)
+          // WebGL is supported
+          if(!backend) backend = "webgl"
+        } else {
+          // WebGL is not supported
+        }
+
+        if (typeof WebAssembly === 'object' && typeof WebAssembly.instantiate === 'function') {
+          // WebAssembly is supported
+          // console.log(WebAssembly)
+          if(!backend) backend = "wasm"
+        } else {
+          // WebAssembly is not supported
+        }
+
+        backend&&await tf.setBackend(backend);
+        await tf.ready();
+        return
+  }
+
+  export async function arrayBufferToHASH(arrayBuffer:any) {
+    // 使用 SubtleCrypto API 计算哈希
+    const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer); // 使用 SHA-256 代替 MD5
+    const hashArray = Array.from(new Uint8Array(hashBuffer)); // 将缓冲区转换为字节数组
+    const hashHex = hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join(''); // 转换为十六进制字符串
+    return hashHex;
+}
+export function stringToArrayBuffer(str:string) {
+    // 创建一个与字符串长度相同的Uint8Array
+    const encoder = new TextEncoder();
+    return encoder.encode(str).buffer;
+}
+export async function EmbedQuery(str:any):Promise<Array<number>>{
+    const embeddings = new TensorFlowEmbeddings();
+    let documentRes = await embeddings.embedQuery(str);
+    return documentRes
+}
+
+/** 向量余弦相似度计算 */
+export function RetriveAllDocument(vector1: Array<number>, docList: Array<any>): Array<any> {
+    docList.forEach(doc => {
+        const vector512 = doc.vector512;
+        doc.similarity = cosineSimilarity(vector1, vector512); // 计算余弦相似度并存储
+    });
+
+    // 按照相似度排序,降序排列
+    docList.sort((a, b) => b.similarity - a.similarity);
+
+    return docList; // 返回排序后的docList
+}
+function dotProduct(vectorA: number[], vectorB: number[]): number {
+    return vectorA.reduce((sum, value, index) => sum + value * vectorB[index], 0);
+}
+
+function magnitude(vector: number[]): number {
+    return Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0));
+}
+
+function cosineSimilarity(vectorA: number[], vectorB: number[]): number {
+    const dotProd = dotProduct(vectorA, vectorB);
+    const magnitudeA = magnitude(vectorA);
+    const magnitudeB = magnitude(vectorB);
+
+    if (magnitudeA === 0 || magnitudeB === 0) {
+        throw new Error("One or both vectors are zero vectors, cannot compute cosine similarity.");
+    }
+
+    return dotProd / (magnitudeA * magnitudeB);
+}

+ 29 - 0
AiStudy-app/src/lib/user/modal-user-edit/modal-user-edit.component.html

@@ -0,0 +1,29 @@
+<!-- 用户登录状态 -->
+<ion-card>
+  <ion-card-header>
+    <ion-card-title>
+      用户名:{{currentUser?.get("username")}}
+    </ion-card-title>
+    <ion-card-subtitle>请输入您的详细资料</ion-card-subtitle>
+   </ion-card-header>
+ <ion-card-content>
+
+   <ion-item>
+     <ion-input [value]="userData['realname']" (ionChange)="userDataChange('realname',$event)" label="姓名" placeholder="请您输入真实姓名"></ion-input>
+   </ion-item>
+   <ion-item>
+     <ion-input type="number" [value]="userData['age']" (ionChange)="userDataChange('age',$event)" label="年龄" placeholder="请您输入年龄"></ion-input>
+    </ion-item>
+  <ion-item>
+     <ion-input [value]="userData['gender']" (ionChange)="userDataChange('gender',$event)" label="性别" placeholder="请您输入男/女"></ion-input>
+    </ion-item>
+    <ion-item>
+      <ion-input [value]="userData['avatar']" (ionChange)="userDataChange('avatar',$event)" label="头像" placeholder="请您输入头像地址"></ion-input>
+     </ion-item>
+
+   <ion-button expand="block" (click)="save()">保存</ion-button>
+   <ion-button expand="block" (click)="cancel()">取消</ion-button>
+ 
+
+</ion-card-content>
+</ion-card>

+ 0 - 0
AiStudy-app/src/lib/user/modal-user-edit/modal-user-edit.component.scss


+ 22 - 0
AiStudy-app/src/lib/user/modal-user-edit/modal-user-edit.component.spec.ts

@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+
+import { ModalUserEditComponent } from './modal-user-edit.component';
+
+describe('ModalUserEditComponent', () => {
+  let component: ModalUserEditComponent;
+  let fixture: ComponentFixture<ModalUserEditComponent>;
+
+  beforeEach(waitForAsync(() => {
+    TestBed.configureTestingModule({
+      imports: [ModalUserEditComponent],
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(ModalUserEditComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  }));
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 65 - 0
AiStudy-app/src/lib/user/modal-user-edit/modal-user-edit.component.ts

@@ -0,0 +1,65 @@
+import { Input, OnInit } from '@angular/core';
+import { Component } from '@angular/core';
+import { IonHeader, IonToolbar, IonTitle, IonContent, IonCard, IonCardContent, IonButton, IonCardHeader, IonCardTitle, IonCardSubtitle, ModalController, IonInput, IonItem, IonSegment, IonSegmentButton, IonLabel } from '@ionic/angular/standalone';
+import { CloudUser } from 'src/lib/ncloud';
+
+@Component({
+  selector: 'app-modal-user-edit',
+  templateUrl: './modal-user-edit.component.html',
+  styleUrls: ['./modal-user-edit.component.scss'],
+  standalone: true,
+  imports: [IonHeader, IonToolbar, IonTitle, IonContent, 
+    IonCard,IonCardContent,IonButton,IonCardHeader,IonCardTitle,IonCardSubtitle,
+    IonInput,IonItem,
+    IonSegment,IonSegmentButton,IonLabel
+  ],
+})
+export class ModalUserEditComponent  implements OnInit {
+
+  currentUser:CloudUser|undefined
+  userData:any = {}
+  userDataChange(key:string,ev:any){
+    let value = ev?.detail?.value
+    if(value){
+      this.userData[key] = value
+    }
+  }
+  constructor(private modalCtrl:ModalController) { 
+    this.currentUser = new CloudUser();
+    this.userData = this.currentUser.data;
+  }
+
+  ngOnInit() {}
+
+  async save(){
+    Object.keys(this.userData).forEach(key=>{
+      if(key=="age"){
+        this.userData[key] = Number(this.userData[key])
+      }
+    })
+
+    this.currentUser?.set(this.userData)
+    await this.currentUser?.save()
+    this.modalCtrl.dismiss(this.currentUser,"confirm")
+  }
+  cancel(){
+    this.modalCtrl.dismiss(null,"cancel")
+
+  }
+}
+
+export async function openUserEditModal(modalCtrl:ModalController):Promise<CloudUser|null>{
+  const modal = await modalCtrl.create({
+    component: ModalUserEditComponent,
+    breakpoints:[0.7,1.0],
+    initialBreakpoint:0.7
+  });
+  modal.present();
+
+  const { data, role } = await modal.onWillDismiss();
+
+  if (role === 'confirm') {
+    return data;
+  }
+  return null
+}

+ 36 - 0
AiStudy-app/src/lib/user/modal-user-login/modal-user-login.component.html

@@ -0,0 +1,36 @@
+<!-- 用户登录状态 -->
+<ion-card>
+  <ion-card-header>
+    <ion-card-title>
+      <ion-segment [value]="type" (ionChange)="typeChange($event)">
+        <ion-segment-button value="login">
+          <ion-label>登录</ion-label>
+        </ion-segment-button>
+        <ion-segment-button value="signup">
+          <ion-label>注册</ion-label>
+        </ion-segment-button>
+      </ion-segment>
+    </ion-card-title>
+    <ion-card-subtitle>请输入账号密码</ion-card-subtitle>
+  </ion-card-header>
+  <ion-card-content>
+    <ion-item>
+      <ion-input [value]="username" (ionChange)="usernameChange($event)" label="账号" placeholder="请您输入账号/手机号"></ion-input>
+    </ion-item>
+    <ion-item>
+      <ion-input [value]="password" (ionChange)="passwordChange($event)" label="密码" type="password" value="password"></ion-input>
+    </ion-item>
+
+    @if(type=="signup"){
+      <ion-item>
+        <ion-input [value]="password2" (ionChange)="password2Change($event)" label="密码二次" type="password" value="password"></ion-input>
+      </ion-item>
+    }
+    @if(type=="login"){
+      <ion-button expand="block" (click)="login()">登录</ion-button>
+    }
+    @if(type=="signup"){
+      <ion-button expand="block" (click)="signup()">注册</ion-button>
+    }
+  </ion-card-content>
+</ion-card>

+ 0 - 0
AiStudy-app/src/lib/user/modal-user-login/modal-user-login.component.scss


+ 22 - 0
AiStudy-app/src/lib/user/modal-user-login/modal-user-login.component.spec.ts

@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+
+import { ModalUserLoginComponent } from './modal-user-login.component';
+
+describe('ModalUserLoginComponent', () => {
+  let component: ModalUserLoginComponent;
+  let fixture: ComponentFixture<ModalUserLoginComponent>;
+
+  beforeEach(waitForAsync(() => {
+    TestBed.configureTestingModule({
+      imports: [ModalUserLoginComponent],
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(ModalUserLoginComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  }));
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 92 - 0
AiStudy-app/src/lib/user/modal-user-login/modal-user-login.component.ts

@@ -0,0 +1,92 @@
+import { Input, OnInit } from '@angular/core';
+import { Component } from '@angular/core';
+import { IonHeader, IonToolbar, IonTitle, IonContent, IonCard, IonCardContent, IonButton, IonCardHeader, IonCardTitle, IonCardSubtitle, ModalController, IonInput, IonItem, IonSegment, IonSegmentButton, IonLabel } from '@ionic/angular/standalone';
+import { CloudUser } from 'src/lib/ncloud';
+
+@Component({
+  selector: 'app-modal-user-login',
+  templateUrl: './modal-user-login.component.html',
+  styleUrls: ['./modal-user-login.component.scss'],
+  standalone: true,
+  imports: [IonHeader, IonToolbar, IonTitle, IonContent, 
+    IonCard,IonCardContent,IonButton,IonCardHeader,IonCardTitle,IonCardSubtitle,
+    IonInput,IonItem,
+    IonSegment,IonSegmentButton,IonLabel
+  ],
+})
+export class ModalUserLoginComponent  implements OnInit {
+  @Input()
+  type:"login"|"signup" = "login"
+  typeChange(ev:any){
+    this.type = ev?.detail?.value || ev?.value || 'login'
+  }
+  username:string = ""
+  usernameChange(ev:any){
+    console.log(ev)
+    this.username = ev?.detail?.value
+  }
+  password:string = ""
+  passwordChange(ev:any){
+    this.password = ev?.detail?.value
+  }
+  password2:string = ""
+  password2Change(ev:any){
+    this.password2 = ev?.detail?.value
+  }
+  constructor(private modalCtrl:ModalController) { }
+
+  ngOnInit() {}
+
+  async login(){
+    if(!this.username || !this.password){
+      console.log("请输入完整")
+      return
+    }
+    let user:any = new CloudUser();
+    user = await user.login(this.username,this.password);
+    if(user?.id){
+       this.modalCtrl.dismiss(user,"confirm")
+    }else{
+      console.log("登录失败")
+    }
+  }
+
+  async signup(){
+    if(!this.username || !this.password || !this.password2){
+      console.log("请输入完整")
+      return
+    }
+    if(this.password!=this.password2){
+      console.log("两次密码不符,请修改")
+      return
+    }
+
+    let user:any = new CloudUser();
+    user = await user.signUp(this.username,this.password);
+    if(user){
+      this.type = "login"
+      console.log("注册成功请登录")
+    }
+  }
+
+}
+
+
+export async function openUserLoginModal(modalCtrl:ModalController,type:"login"|"signup"="login"):Promise<CloudUser|null>{
+  const modal = await modalCtrl.create({
+    component: ModalUserLoginComponent,
+    componentProps:{
+      type:type
+    },
+    breakpoints:[0.5,0.7],
+    initialBreakpoint:0.5
+  });
+  modal.present();
+
+  const { data, role } = await modal.onWillDismiss();
+
+  if (role === 'confirm') {
+    return data;
+  }
+  return null
+}