Просмотр исходного кода

feat: training-plan & training consult

未来全栈 13 часов назад
Родитель
Сommit
3488d08cff

+ 19 - 19
src/agent/tasks/inquiry/2.inquiry-doctor-question.ts

@@ -52,25 +52,25 @@ export function TaskInqueryDoctorQuestion(options:{
                 let completion = new FmodeChatCompletion([
                     {role:"system",content:""},
                     {role:"user",content:PromptTemplate}
-                    ])
-                    completion.sendCompletion().subscribe((message:any)=>{
-                        if(task1.progress < 0.5){
-                            task1.progress += 0.1
-                        }
-                        if(task1.progress >= 0.5 && task1.progress <= 0.9){
-                            task1.progress += 0.01
-                        }
-                        if(task1.progress >= 0.7){
-                            task1.progress += 0.001
-                        }
-                        // 打印消息体
-                        console.log(message.content)
-                        // 赋值消息内容给组件内属性
-                        if(message.complete){ // 判断message为完成状态,则设置isComplete为完成
-                            options.shareData.userStory.questionList = extactAndParseJsonFromString(message.content).questionList || []
-                            task1.progress = 1
-                            resolve(true)
-                        }
+                ])
+                completion.sendCompletion().subscribe((message:any)=>{
+                    if(task1.progress < 0.5){
+                        task1.progress += 0.1
+                    }
+                    if(task1.progress >= 0.5 && task1.progress <= 0.9){
+                        task1.progress += 0.01
+                    }
+                    if(task1.progress >= 0.7){
+                        task1.progress += 0.001
+                    }
+                    // 打印消息体
+                    console.log(message.content)
+                    // 赋值消息内容给组件内属性
+                    if(message.complete){ // 判断message为完成状态,则设置isComplete为完成
+                        options.shareData.userStory.questionList = extactAndParseJsonFromString(message.content).questionList || []
+                        task1.progress = 1
+                        resolve(true)
+                    }
                 })
             })
 

+ 56 - 10
src/app/tab2/tab2.page.html

@@ -16,6 +16,7 @@
       </ion-card-header>
       <ion-card-content>
         <p>我们的智能助手可以为您提供多种专业领域的咨询服务,点击下方按钮开始体验。</p>
+        <ion-button (click)="goTraining()">文本补全:指定训练计划</ion-button>
       </ion-card-content>
     </ion-card>
   </div>
@@ -79,20 +80,65 @@
     </ion-row>
 
     <ion-row>
-      <ion-col size="12">
+      <ion-col size="12" size-md="8" offset-md="2">
         <ion-card class="special-feature" button>
-          <ion-card-header>
-            <ion-icon name="medical" color="danger" class="feature-icon"></ion-icon>
-            <ion-card-title>运动健身服务</ion-card-title>
-            <ion-card-subtitle>专业健身咨询</ion-card-subtitle>
+          <ion-card-header class="ion-text-center">
+            <ion-icon name="barbell" color="primary" class="feature-icon" style="font-size: 48px;"></ion-icon>
+            <ion-card-title class="ion-padding-top">运动健身服务</ion-card-title>
+            <ion-card-subtitle>专业健身咨询 - 打造完美身材</ion-card-subtitle>
           </ion-card-header>
+
           <ion-card-content>
-            请联系我们私教咨询。
+            <ion-text color="medium" class="ion-text-center ion-margin-bottom">
+              <p>我们的专业教练团队随时为您提供个性化健身指导</p>
+              <ion-button fill="clear" size="small" (click)="loadConsult()">
+                <ion-icon slot="icon-only" name="refresh"></ion-icon>
+              </ion-button>
+            </ion-text>
+
+            @if(consultList?.length){
+            <ion-list lines="none" class="consult-history">
+              <ion-list-header>
+                <ion-label>历史咨询记录</ion-label>
+                <ion-button fill="clear" size="small" (click)="loadConsult()">
+                  <ion-icon slot="icon-only" name="refresh"></ion-icon>
+                </ion-button>
+              </ion-list-header>
+
+              @for(consult of consultList; track consult){
+              <ion-item button detail (click)="openConsult(consult.get('chatId'))" class="consult-item">
+                @if(consult.get("avatar")){
+                <ion-avatar slot="start">
+                  <img [src]="consult.get('avatar')" alt="教练头像">
+                </ion-avatar>
+                } @else {
+                <ion-avatar slot="start">
+                  <ion-icon name="person-circle" style="font-size: 40px;"></ion-icon>
+                </ion-avatar>
+                }
+
+                <ion-label>
+                  <h3>{{consult.get("name") || '未命名咨询'}}</h3>
+                  <p>{{consult.updatedAt | date:'yyyy-MM-dd HH:mm'}}</p>
+                </ion-label>
+
+                <ion-badge slot="end" color="light">
+                  <ion-icon name="chevron-forward" color="medium"></ion-icon>
+                </ion-badge>
+              </ion-item>
+              }
+            </ion-list>
+            }
           </ion-card-content>
-          <ion-button (click)="openConsult()" fill="clear" expand="block" color="danger">
-            开始咨询
-            <ion-icon slot="end" name="medkit"></ion-icon>
-          </ion-button>
+
+          <ion-footer class="ion-no-border">
+            <ion-toolbar>
+              <ion-button (click)="openConsult()" expand="block" color="primary" shape="round">
+                <ion-icon slot="start" name="chatbubbles"></ion-icon>
+                开始新咨询
+              </ion-button>
+            </ion-toolbar>
+          </ion-footer>
         </ion-card>
       </ion-col>
     </ion-row>

+ 105 - 51
src/app/tab2/tab2.page.scss

@@ -1,65 +1,119 @@
 /* 页面整体样式 */
 ion-content {
-    --background: #f5f5f5;
+  --background: #f5f5f5;
+}
+
+/* 欢迎卡片样式 */
+.welcome-card {
+  text-align: center;
+  margin: 20px auto;
+  max-width: 800px;
+  border-radius: 15px;
+  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
+
+  ion-card-header {
+    padding-bottom: 0;
   }
-  
-  /* 欢迎卡片样式 */
-  .welcome-card {
+}
+
+/* 功能卡片样式 */
+.features-grid {
+  max-width: 1200px;
+  margin: 0 auto;
+}
+
+.feature-card {
+  height: 100%;
+  border-radius: 12px;
+  transition: transform 0.3s ease, box-shadow 0.3s ease;
+
+  &:hover {
+    transform: translateY(-5px);
+    box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
+  }
+
+  ion-card-header {
     text-align: center;
-    margin: 20px auto;
-    max-width: 800px;
-    border-radius: 15px;
-    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
-    ion-card-header {
-      padding-bottom: 0;
-    }
+    padding-bottom: 0;
   }
-  
-  /* 功能卡片样式 */
-  .features-grid {
-    max-width: 1200px;
-    margin: 0 auto;
+
+  ion-card-content {
+    min-height: 80px;
   }
-  
-  .feature-card {
-    height: 100%;
-    border-radius: 12px;
-    transition: transform 0.3s ease, box-shadow 0.3s ease;
-    &:hover {
-      transform: translateY(-5px);
-      box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
-    }
-    ion-card-header {
-      text-align: center;
-      padding-bottom: 0;
-    }
-    ion-card-content {
-      min-height: 80px;
-    }
+}
+
+.feature-icon {
+  font-size: 48px;
+  margin-bottom: 16px;
+}
+
+/* 特殊功能卡片样式 */
+.special-feature {
+  margin-top: 20px;
+  border-radius: 12px;
+  background: linear-gradient(135deg, #fff8f8, #ffffff);
+  border-left: 4px solid var(--ion-color-danger);
+
+  ion-card-header {
+    text-align: center;
   }
-  
-  .feature-icon {
-    font-size: 48px;
-    margin-bottom: 16px;
+
+  ion-card-content {
+    min-height: 80px;
   }
-  
-  /* 特殊功能卡片样式 */
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+
+  .feature-card,
   .special-feature {
-    margin-top: 20px;
+    margin-bottom: 20px;
+  }
+}
+
+.special-feature {
+  border-radius: 16px;
+  overflow: hidden;
+  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
+
+  .feature-icon {
+    margin-bottom: 8px;
+  }
+
+  .consult-history {
+    background: var(--ion-color-light);
     border-radius: 12px;
-    background: linear-gradient(135deg, #fff8f8, #ffffff);
-    border-left: 4px solid var(--ion-color-danger);
-    ion-card-header {
-      text-align: center;
+    margin: 16px 0;
+
+    ion-list-header {
+      --background: transparent;
+      --color: var(--ion-color-medium);
     }
-    ion-card-content {
-      min-height: 80px;
+
+    .consult-item {
+      --border-radius: 8px;
+      --padding-start: 8px;
+      --padding-end: 8px;
+      margin-bottom: 8px;
+
+      ion-avatar {
+        width: 40px;
+        height: 40px;
+      }
+
+      &:hover {
+        --background: rgba(var(--ion-color-primary-rgb), 0.08);
+      }
     }
   }
-  
-  /* 响应式调整 */
-  @media (max-width: 768px) {
-    .feature-card, .special-feature {
-      margin-bottom: 20px;
+
+  ion-footer {
+    ion-toolbar {
+      --padding-top: 0;
+      --padding-bottom: 0;
+      --padding-start: 16px;
+      --padding-end: 16px;
     }
-  }
+  }
+}

+ 184 - 161
src/app/tab2/tab2.page.ts

@@ -1,38 +1,32 @@
+import { CommonModule } from '@angular/common';
 import { Component } from '@angular/core';
 import { Router } from '@angular/router';
-import { IonHeader, IonToolbar, IonTitle, IonContent, ModalController, IonButton } from '@ionic/angular/standalone';
-import { ExploreContainerComponent } from '../explore-container/explore-container.component';
+import {
+  ModalController,
+  IonHeader, IonTitle, IonContent,
+  IonButton, IonList, IonItem, IonLabel, IonIcon, IonCardContent, IonCardSubtitle, IonCardTitle,
+  IonCardHeader, IonCard, IonRow, IonCol, IonGrid,
+  IonAvatar, IonBadge, IonText, IonListHeader, IonToolbar, IonFooter
+} from '@ionic/angular/standalone';
+
+import { NavController } from '@ionic/angular/standalone';
 import { ChatPanelOptions, FmChatModalInput, FmodeChat, FmodeChatMessage, openChatPanelModal } from 'fmode-ng';
 // import { ModalAudioMessageComponent } from 'fmode-ng/lib/aigc/chat/chat-modal-input/modal-audio-message/modal-audio-message.component';
 import Parse from "parse";
-import { 
-  IonCard,
-  IonCardHeader,
-  IonCardTitle,
-  IonCardSubtitle,
-  IonCardContent,
-  IonGrid,
-  IonRow,
-  IonCol,
-  IonIcon
-} from '@ionic/angular/standalone';
+
+import { CloudObject, CloudQuery } from 'src/lib/ncloud';
 @Component({
   selector: 'app-tab2',
   templateUrl: 'tab2.page.html',
   styleUrls: ['tab2.page.scss'],
   standalone: true,
   imports: [
-    IonHeader, IonToolbar, IonTitle, IonContent, ExploreContainerComponent,
-    IonButton,
-    IonCard,
-    IonCardHeader,
-    IonCardTitle,
-    IonCardSubtitle,
-    IonCardContent,
-    IonGrid,
-    IonRow,
-    IonCol,
-    IonIcon,
+    CommonModule,
+    IonHeader, IonTitle, IonContent,
+    IonButton, IonList, IonItem, IonLabel, IonIcon, IonAvatar,
+    IonCardContent, IonCardSubtitle, IonCardTitle,
+    IonCardHeader, IonCard, IonRow, IonCol, IonGrid,
+    IonAvatar, IonBadge, IonText, IonListHeader, IonToolbar, IonFooter,
     // ASR语音输入模块
     FmChatModalInput,
     // ModalAudioMessageComponent
@@ -41,34 +35,39 @@ import {
 export class Tab2Page {
 
   constructor(
-    private modalCtrl:ModalController,
-    private router:Router,
-    ) {
+    private modalCtrl: ModalController,
+    private router: Router,
+    private navCtrl: NavController,
+  ) {
 
   }
-  title:string = "123"
+  goTraining() {
+    this.navCtrl.navigateRoot(['tabs', 'demo', 'training'])
+  }
+
+  title: string = "123"
   /** 示例:问诊ChatPanel面板 */
-  openInquiry(chatId?:string){
-    localStorage.setItem("company","E4KpGvTEto")
-    let options:ChatPanelOptions = {
-      roleId:"2DXJkRsjXK", // 预设,无需更改
-      chatId:chatId, // 若存在,则恢复会话。若不存在,则开启新会话
-      onChatInit:(chat:FmodeChat)=>{
+  openInquiry(chatId?: string) {
+    localStorage.setItem("company", "E4KpGvTEto")
+    let options: ChatPanelOptions = {
+      roleId: "2DXJkRsjXK", // 预设,无需更改
+      chatId: chatId, // 若存在,则恢复会话。若不存在,则开启新会话
+      onChatInit: (chat: FmodeChat) => {
         console.log("onChatInit");
-        console.log("Chat类",chat);
-        console.log("预设角色",chat.role);
+        console.log("Chat类", chat);
+        console.log("预设角色", chat.role);
         // 角色名称
-        chat.role.set("name","晓晓");
+        chat.role.set("name", "晓晓");
         // 角色称号
-        chat.role.set("title","全科医生");
+        chat.role.set("title", "全科医生");
         // 角色描述
-        chat.role.set("desc","一名亲切和蔼的门诊全科主任医生,晓晓,年龄36岁");
+        chat.role.set("desc", "一名亲切和蔼的门诊全科主任医生,晓晓,年龄36岁");
         // 角色标签
-        chat.role.set("tags",["全科","门诊"]);
+        chat.role.set("tags", ["全科", "门诊"]);
         // 角色头像
-        chat.role.set("avatar","https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/aigc/imagine/Q4Zif7fTbK-0.png")
+        chat.role.set("avatar", "https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/aigc/imagine/Q4Zif7fTbK-0.png")
         // 角色提示词
-        chat.role.set("prompt",`
+        chat.role.set("prompt", `
 # 角色设定
 您是一名亲切和蔼的专业的全科医生,晓晓,年龄36岁,需要完成一次完整的门诊服务。
 
@@ -125,26 +124,26 @@ export class Tab2Page {
           }
         ]
         setTimeout(() => {
-          chat.role.set("promptCates",promptCates)
+          chat.role.set("promptCates", promptCates)
         }, 500);
         // 对话灵感列表
         let promptList = [
           {
-            cate:"外科123",img:"https://file-cloud.fmode.cn/UP2cStyjuk/20231211/r1ltv1023812146.png",
-            messageList:["局部疼痛或肿胀","伤口出血或感染","关节活动受限","体表肿块或结节","外伤后活动障碍","皮肤溃疡不愈合","异物刺入或嵌顿","术后并发症复查","肢体麻木或无力","运动损伤疼痛"]
+            cate: "外科123", img: "https://file-cloud.fmode.cn/UP2cStyjuk/20231211/r1ltv1023812146.png",
+            messageList: ["局部疼痛或肿胀", "伤口出血或感染", "关节活动受限", "体表肿块或结节", "外伤后活动障碍", "皮肤溃疡不愈合", "异物刺入或嵌顿", "术后并发症复查", "肢体麻木或无力", "运动损伤疼痛"]
           },
           {
-            cate:"内科",img:"https://file-cloud.fmode.cn/UP2cStyjuk/20231211/fo81fg034154259.png",
-            messageList:["反复发热或低热","持续咳嗽咳痰","胸闷气短心悸","慢性腹痛腹泻","头晕头痛乏力","体重骤增或骤减","食欲异常或消化不良","尿频尿急尿痛","睡眠障碍易醒","异常出汗或怕冷"]
+            cate: "内科", img: "https://file-cloud.fmode.cn/UP2cStyjuk/20231211/fo81fg034154259.png",
+            messageList: ["反复发热或低热", "持续咳嗽咳痰", "胸闷气短心悸", "慢性腹痛腹泻", "头晕头痛乏力", "体重骤增或骤减", "食欲异常或消化不良", "尿频尿急尿痛", "睡眠障碍易醒", "异常出汗或怕冷"]
           },
           {
-            cate:"心理",img:"https://file-cloud.fmode.cn/UP2cStyjuk/20231211/fc1nqi034201098.png",
-            messageList:["持续情绪低落","焦虑紧张不安","失眠或睡眠过多","注意力难以集中","社交恐惧回避","强迫思维或行为","记忆减退疑虑","躯体无器质性疼痛","自杀倾向念头","现实感丧失体验"]
+            cate: "心理", img: "https://file-cloud.fmode.cn/UP2cStyjuk/20231211/fc1nqi034201098.png",
+            messageList: ["持续情绪低落", "焦虑紧张不安", "失眠或睡眠过多", "注意力难以集中", "社交恐惧回避", "强迫思维或行为", "记忆减退疑虑", "躯体无器质性疼痛", "自杀倾向念头", "现实感丧失体验"]
           },
         ]
         let ChatPrompt = Parse.Object.extend("ChatPrompt");
         setTimeout(() => {
-          chat.promptList = promptList.map(item=>{
+          chat.promptList = promptList.map(item => {
             let prompt = new ChatPrompt();
             prompt.set(item);
             prompt.img = item.img;
@@ -155,35 +154,35 @@ export class Tab2Page {
         // 功能按钮区域预设
         chat.leftButtons = [
           { // 提示 当角色配置预设提示词时 显示
-           title:"话题灵感", // 按钮标题
-           showTitle:true, // 是否显示标题文字
-           icon:"color-wand-outline", // 标题icon图标
-           onClick:()=>{ // 按钮点击事件
-               chat.isPromptModalOpen = true
-           },
-           show:()=>{ // 按钮显示条件
-             return chat?.promptList?.length // 存在话题提示词时显示
-           }
-         },
-         { // 总结 结束并归档本次对话
-            title:"门诊归档",
-            showTitle:true,
-            icon:"archive-outline",
-            onClick:()=>{
-                // 门诊归档,记录用户门诊咨询,并进行过程评价
-                console.log(chat?.chatSession) // 本次会话内容数据
-              },
-            show:()=>{ 
+            title: "话题灵感", // 按钮标题
+            showTitle: true, // 是否显示标题文字
+            icon: "color-wand-outline", // 标题icon图标
+            onClick: () => { // 按钮点击事件
+              chat.isPromptModalOpen = true
+            },
+            show: () => { // 按钮显示条件
+              return chat?.promptList?.length // 存在话题提示词时显示
+            }
+          },
+          { // 总结 结束并归档本次对话
+            title: "门诊归档",
+            showTitle: true,
+            icon: "archive-outline",
+            onClick: () => {
+              // 门诊归档,记录用户门诊咨询,并进行过程评价
+              console.log(chat?.chatSession) // 本次会话内容数据
+            },
+            show: () => {
               return true // 一直显示
             }
-        }
-      ]
+          }
+        ]
 
       },
-      onMessage:(chat:FmodeChat,message:FmodeChatMessage)=>{
-        console.log("onMessage",message)
-        let content:any = message?.content
-        if(typeof content == "string"){
+      onMessage: (chat: FmodeChat, message: FmodeChatMessage) => {
+        console.log("onMessage", message)
+        let content: any = message?.content
+        if (typeof content == "string") {
           // 根据阶段标记判断下一步处理过程
           if (content.includes('[导诊完成]')) {
             // 进入问诊环节
@@ -200,36 +199,37 @@ export class Tab2Page {
           }
         }
       },
-      onChatSaved:(chat:FmodeChat)=>{
+      onChatSaved: (chat: FmodeChat) => {
         // chat?.chatSession?.id 本次会话的 chatId
-        console.log("onChatSaved",chat,chat?.chatSession,chat?.chatSession?.id)
+        console.log("onChatSaved", chat, chat?.chatSession, chat?.chatSession?.id)
       }
     }
-    openChatPanelModal(this.modalCtrl,options)
+    openChatPanelModal(this.modalCtrl, options)
   }
-openConsult(chatId?:string){
-    localStorage.setItem("company","E4KpGvTEto")
-    let options:ChatPanelOptions = {
-      roleId:"2DXJkRsjXK", // 预设,无需更改
-      // chatId:chatId, // 若存在,则恢复会话。若不存在,则开启新会话
-      onChatInit:(chat:FmodeChat)=>{
+  openConsult(chatId?: string) {
+    localStorage.setItem("company", "E4KpGvTEto")
+    let options: ChatPanelOptions = {
+      roleId: "2DXJkRsjXK", // 预设,无需更改
+      chatId: chatId, // 若存在,则恢复会话。若不存在,则开启新会话
+      onChatInit: (chat: FmodeChat) => {
         console.log("onChatInit");
-        console.log("Chat类",chat);
-        console.log("预设角色",chat.role);
+        console.log("Chat类", chat);
+        console.log("预设角色", chat.role);
         // 角色名称
-        chat.role.set("name","宋珀尔");
+        chat.role.set("name", "宋珀尔");
         // 角色称号
-        chat.role.set("title","专业教练");
+        chat.role.set("title", "专业教练");
         // 角色描述
-        chat.role.set("desc","一名亲切和蔼的健身教练,宋珀尔,年龄26岁");
+        chat.role.set("desc", "一名亲切和蔼的健身教练,宋珀尔,年龄26岁");
         // 角色标签
-        chat.role.set("tags",['跑步', '动感单车']);
+        chat.role.set("tags", ['跑步', '动感单车']);
         // 角色头像
-        chat.role.set("avatar","/assets/avatars/jiaolian1.jpg")
+        chat.role.set("avatar", "/assets/avatars/jiaolian1.jpg")
         // 角色提示词
-        chat.role.set("prompt",`
+        chat.role.set("prompt", `
 # 角色设定
 您是一名亲切和蔼的健身教练,宋珀尔,年龄26岁,需要您解答用户健身方面的专业问题。
+请您模仿一个正常教练对话,语言简短不需要长篇大论。
 `);
         // 对话灵感分类
         let promptCates = [
@@ -247,59 +247,59 @@ openConsult(chatId?:string){
           }
         ]
         setTimeout(() => {
-          chat.role.set("promptCates",promptCates)
+          chat.role.set("promptCates", promptCates)
         }, 500);
         // 对话灵感列表
         let promptList = [
           {
-            cate:"有氧",img:"/assets/icon/yy.jpg",
-            messageList:[
-            "有氧运动多久才能有效减脂?",  
-            "跑步和游泳哪个减肥效果更好?",  
-            "空腹有氧真的更燃脂吗?",  
-            "有氧运动会不会掉肌肉?",  
-            "心率控制在多少才能高效燃脂?",  
-            "每天做有氧运动会不会过度疲劳?",  
-            "有氧运动前要不要吃东西?",  
-            "椭圆机和跑步机哪个更适合新手?",  
-            "跳绳会不会伤膝盖?",  
-            "有氧运动后怎么补充能量?"  
-          ]
+            cate: "有氧", img: "/assets/icon/yy.jpg",
+            messageList: [
+              "有氧运动多久才能有效减脂?",
+              "跑步和游泳哪个减肥效果更好?",
+              "空腹有氧真的更燃脂吗?",
+              "有氧运动会不会掉肌肉?",
+              "心率控制在多少才能高效燃脂?",
+              "每天做有氧运动会不会过度疲劳?",
+              "有氧运动前要不要吃东西?",
+              "椭圆机和跑步机哪个更适合新手?",
+              "跳绳会不会伤膝盖?",
+              "有氧运动后怎么补充能量?"
+            ]
           },
           {
-            cate:"减脂",img:"/assets/icon/jz.jpg",
-            messageList:[
-              "减脂一定要做有氧吗?",  
-              "为什么体重没变但看起来瘦了?",  
-              "局部减脂(如瘦肚子)真的存在吗?",  
-              "减脂期每天应该吃多少热量?",  
-              "低碳饮食和低脂饮食哪个更适合减脂?",  
-              "为什么运动后体重反而增加了?",  
-              "减脂期可以吃零食吗?",  
-              "平台期怎么突破?",  
-              "晚上吃东西会不会更容易长胖?",  
-              "减脂期要不要计算蛋白质摄入?"  
+            cate: "减脂", img: "/assets/icon/jz.jpg",
+            messageList: [
+              "减脂一定要做有氧吗?",
+              "为什么体重没变但看起来瘦了?",
+              "局部减脂(如瘦肚子)真的存在吗?",
+              "减脂期每天应该吃多少热量?",
+              "低碳饮食和低脂饮食哪个更适合减脂?",
+              "为什么运动后体重反而增加了?",
+              "减脂期可以吃零食吗?",
+              "平台期怎么突破?",
+              "晚上吃东西会不会更容易长胖?",
+              "减脂期要不要计算蛋白质摄入?"
             ]
           },
           {
-            cate:"增肌",img:"/assets/icon/zj.jpg",
+            cate: "增肌", img: "/assets/icon/zj.jpg",
             messageList: [
-            "增肌一定要喝蛋白粉吗?",  
-            "为什么练了很久肌肉不长?",  
-            "增肌期可以同时减脂吗?",  
-            "训练后多久补充蛋白质最有效?",  
-            "增肌需要每天练同一个部位吗?",  
-            "徒手训练(如俯卧撑)能有效增肌吗?",  
-            "增肌期体重不增长是怎么回事?",  
-            "肌肉酸痛还能继续练吗?",  
-            "增肌训练每组做多少次最合适?",  
-            "睡眠对增肌的影响有多大?"  
-          ]
+              "增肌一定要喝蛋白粉吗?",
+              "为什么练了很久肌肉不长?",
+              "增肌期可以同时减脂吗?",
+              "训练后多久补充蛋白质最有效?",
+              "增肌需要每天练同一个部位吗?",
+              "徒手训练(如俯卧撑)能有效增肌吗?",
+              "增肌期体重不增长是怎么回事?",
+              "肌肉酸痛还能继续练吗?",
+              "增肌训练每组做多少次最合适?",
+              "睡眠对增肌的影响有多大?"
+            ]
           },
         ]
         let ChatPrompt = Parse.Object.extend("ChatPrompt");
         setTimeout(() => {
-          chat.promptList = promptList.map(item=>{
+          chat.promptList = promptList.map(item => {
             let prompt = new ChatPrompt();
             prompt.set(item);
             prompt.img = item.img;
@@ -310,23 +310,23 @@ openConsult(chatId?:string){
         // 功能按钮区域预设
         chat.leftButtons = [
           { // 提示 当角色配置预设提示词时 显示
-           title:"话题灵感", // 按钮标题
-           showTitle:true, // 是否显示标题文字
-           icon:"color-wand-outline", // 标题icon图标
-           onClick:()=>{ // 按钮点击事件
-               chat.isPromptModalOpen = true
-           },
-           show:()=>{ // 按钮显示条件
-             return chat?.promptList?.length // 存在话题提示词时显示
-           }
-         },
-      ]
+            title: "话题灵感", // 按钮标题
+            showTitle: true, // 是否显示标题文字
+            icon: "color-wand-outline", // 标题icon图标
+            onClick: () => { // 按钮点击事件
+              chat.isPromptModalOpen = true
+            },
+            show: () => { // 按钮显示条件
+              return chat?.promptList?.length // 存在话题提示词时显示
+            }
+          },
+        ]
 
       },
-      onMessage:(chat:FmodeChat,message:FmodeChatMessage)=>{
-        console.log("onMessage",message)
-        let content:any = message?.content
-        if(typeof content == "string"){
+      onMessage: (chat: FmodeChat, message: FmodeChatMessage) => {
+        console.log("onMessage", message)
+        let content: any = message?.content
+        if (typeof content == "string") {
           // 根据阶段标记判断下一步处理过程
           if (content.includes('[导诊完成]')) {
             // 进入问诊环节
@@ -343,39 +343,62 @@ openConsult(chatId?:string){
           }
         }
       },
-      onChatSaved:(chat:FmodeChat)=>{
+      onChatSaved: async (chat: FmodeChat) => {
         // chat?.chatSession?.id 本次会话的 chatId
-        console.log("onChatSaved",chat,chat?.chatSession,chat?.chatSession?.id)
+        console.log("onChatSaved", chat, chat?.chatSession, chat?.chatSession?.id)
+        let chatId = chat?.chatSession?.id;
+        console.log("chatId", chatId)
+        let query = new CloudQuery("TrainingConsult");
+        let trainingConsult = await query.get(chatId);
+        console.log("trainingConsult", trainingConsult);
+        if (!trainingConsult?.id) { // 若无重复记录,则实例化一个新的咨询记录
+          trainingConsult = new CloudObject("TrainingConsult")
+        }
+        trainingConsult.set({
+          "chatId": chatId,
+          "messageList": chat.messageList,
+          "name": chat.role.get("name"),
+          "avatar": chat.role.get("avatar")
+        })
+        console.log("trainingConsult", trainingConsult)
+        trainingConsult.save();
       }
     }
-    openChatPanelModal(this.modalCtrl,options)
+    openChatPanelModal(this.modalCtrl, options)
+  }
+  // 加载健康咨询记录
+  consultList: Array<CloudObject> = []
+  async loadConsult() {
+    let query = new CloudQuery("TrainingConsult");
+    this.consultList = await query.find()
+    console.log(this.consultList)
   }
   /**
    * 开始聊天
    */
-  openChat(){
-    let options:ChatPanelOptions = {
-      roleId:"2DXJkRsjXK",
-      onChatSaved:(chat:FmodeChat)=>{
+  openChat() {
+    let options: ChatPanelOptions = {
+      roleId: "2DXJkRsjXK",
+      onChatSaved: (chat: FmodeChat) => {
         // chat?.chatSession?.id 本次会话的 chatId
-        console.log("onChatSaved",chat,chat?.chatSession,chat?.chatSession?.id)
+        console.log("onChatSaved", chat, chat?.chatSession, chat?.chatSession?.id)
       },
     }
-    openChatPanelModal(this.modalCtrl,options)
+    openChatPanelModal(this.modalCtrl, options)
   }
   /**
    * 恢复聊天
    * @chatId 从onChatSaved生命周期中,获取chat?.chatSession?.id
    */
-  restoreChat(chatId:string){
-    let options:ChatPanelOptions = {
-      roleId:"2DXJkRsjXK",
-      chatId:chatId
+  restoreChat(chatId: string) {
+    let options: ChatPanelOptions = {
+      roleId: "2DXJkRsjXK",
+      chatId: chatId
     }
-    openChatPanelModal(this.modalCtrl,options)
+    openChatPanelModal(this.modalCtrl, options)
   }
 
-  goChat(){
+  goChat() {
     this.router.navigateByUrl("/chat/session/role/2DXJkRsjXK")
   }
 

+ 10 - 4
src/app/tabs/tabs.routes.ts

@@ -10,20 +10,20 @@ export const routes: Routes = [
     children: [
       {
         path: 'tab1',
-        canActivate:[TokenGuard],
+        canActivate: [TokenGuard],
         loadComponent: () =>
           import('../../modules/flow/page-flow-test/page-flow-test.component').then((m) => m.PageFlowTestComponent),
-          // import('../tab1/tab1.page').then((m) => m.Tab1Page),
+        // import('../tab1/tab1.page').then((m) => m.Tab1Page),
       },
       {
         path: 'tab2',
-        canActivate:[TokenGuard,TestGuard],
+        canActivate: [TokenGuard],
         loadComponent: () =>
           import('../tab2/tab2.page').then((m) => m.Tab2Page),
       },
       {
         path: 'tab3',
-        canActivate:[TokenGuard],
+        canActivate: [TokenGuard],
         loadComponent: () =>
           import('../tab3/tab3.page').then((m) => m.Tab3Page),
       },
@@ -32,6 +32,12 @@ export const routes: Routes = [
         redirectTo: '/tabs/tab3',
         pathMatch: 'full',
       },
+      {
+        path: 'demo/training',
+        canActivate: [TokenGuard],
+        loadComponent: () =>
+          import('../../modules/demo/page-training-plan/page-training-plan.component').then((m) => m.PageTrainingPlanComponent),
+      },
     ],
   },
   {

+ 40 - 40
src/lib/ncloud.ts

@@ -1,15 +1,15 @@
 // CloudObject.ts
 
 let serverURL = `https://dev.fmode.cn/parse`;
-if(location.protocol=="http:"){
-   serverURL = `http://dev.fmode.cn:1337/parse`;
+if (location.protocol == "http:") {
+    serverURL = `http://dev.fmode.cn:1337/parse`;
 }
 
 export class CloudObject {
     className: string;
-    id: string | null = null;
-    createdAt:any;
-    updatedAt:any;
+    id: string | undefined = undefined;
+    createdAt: any;
+    updatedAt: any;
     data: Record<string, any> = {};
 
     constructor(className: string) {
@@ -79,7 +79,7 @@ export class CloudObject {
 
         const result = await response?.json();
         if (result) {
-            this.id = null;
+            this.id = undefined;
         }
         return true;
     }
@@ -88,13 +88,13 @@ export class CloudObject {
 // CloudQuery.ts
 export class CloudQuery {
     className: string;
-    queryParams: Record<string, any> = {};
+    queryParams: Record<string, any> = { where: {} };
 
     constructor(className: string) {
         this.className = className;
     }
 
-    include(...fileds:string[]) {
+    include(...fileds: string[]) {
         this.queryParams["include"] = fileds;
     }
     greaterThan(key: string, value: any) {
@@ -144,18 +144,18 @@ export class CloudQuery {
         return null
     }
 
-    async find():Promise<Array<CloudObject>> {
+    async find(): Promise<Array<CloudObject>> {
         let url = serverURL + `/classes/${this.className}?`;
 
         let queryStr = ``
-        Object.keys(this.queryParams).forEach(key=>{
+        Object.keys(this.queryParams).forEach(key => {
             let paramStr = JSON.stringify(this.queryParams[key]);
-            if(key=="include"){
+            if (key == "include") {
                 paramStr = this.queryParams[key]?.join(",")
             }
-            if(queryStr) {
+            if (queryStr) {
                 url += `${key}=${paramStr}`;
-            }else{
+            } else {
                 url += `&${key}=${paramStr}`;
             }
         })
@@ -176,7 +176,7 @@ export class CloudQuery {
 
         const json = await response?.json();
         let list = json?.results || []
-        let objList = list.map((item:any)=>this.dataToObj(item))
+        let objList = list.map((item: any) => this.dataToObj(item))
         return objList || [];
     }
 
@@ -209,12 +209,12 @@ export class CloudQuery {
         return null
     }
 
-    dataToObj(exists:any):CloudObject{
+    dataToObj(exists: any): CloudObject {
         let existsObject = new CloudObject(this.className);
-        Object.keys(exists).forEach(key=>{
-          if(exists[key]?.__type =="Object"){
-            exists[key] = this.dataToObj(exists[key])
-          }
+        Object.keys(exists).forEach(key => {
+            if (exists[key]?.__type == "Object") {
+                exists[key] = this.dataToObj(exists[key])
+            }
         })
         existsObject.set(exists);
         existsObject.id = exists.objectId;
@@ -230,7 +230,7 @@ export class CloudUser extends CloudObject {
         super("_User"); // 假设用户类在Parse中是"_User"
         // 读取用户缓存信息
         let userCacheStr = localStorage.getItem("NCloud/dev/User")
-        if(userCacheStr){
+        if (userCacheStr) {
             let userData = JSON.parse(userCacheStr)
             // 设置用户信息
             this.id = userData?.objectId;
@@ -239,7 +239,7 @@ export class CloudUser extends CloudObject {
         }
     }
 
-    sessionToken:string|null = ""
+    sessionToken: string | null = ""
     /** 获取当前用户信息 */
     async current() {
         if (!this.sessionToken) {
@@ -264,7 +264,7 @@ export class CloudUser extends CloudObject {
     }
 
     /** 登录 */
-    async login(username: string, password: string):Promise<CloudUser|null> {
+    async login(username: string, password: string): Promise<CloudUser | null> {
         const response = await fetch(serverURL + `/login`, {
             headers: {
                 "x-parse-application-id": "dev",
@@ -286,7 +286,7 @@ export class CloudUser extends CloudObject {
         this.data = result; // 保存用户数据
         // 缓存用户信息
         console.log(result)
-        localStorage.setItem("NCloud/dev/User",JSON.stringify(result))
+        localStorage.setItem("NCloud/dev/User", JSON.stringify(result))
         return this;
     }
 
@@ -309,7 +309,7 @@ export class CloudUser extends CloudObject {
 
         if (result?.error) {
             console.error(result?.error);
-            if(result?.error=="Invalid session token"){
+            if (result?.error == "Invalid session token") {
                 this.clearUserCache()
                 return true;
             }
@@ -319,10 +319,10 @@ export class CloudUser extends CloudObject {
         this.clearUserCache()
         return true;
     }
-    clearUserCache(){
+    clearUserCache() {
         // 清除用户信息
         localStorage.removeItem("NCloud/dev/User")
-        this.id = null;
+        this.id = undefined;
         this.sessionToken = null;
         this.data = {};
     }
@@ -353,7 +353,7 @@ export class CloudUser extends CloudObject {
         // 设置用户信息
         // 缓存用户信息
         console.log(result)
-        localStorage.setItem("NCloud/dev/User",JSON.stringify(result))
+        localStorage.setItem("NCloud/dev/User", JSON.stringify(result))
         this.id = result?.objectId;
         this.sessionToken = result?.sessionToken;
         this.data = result; // 保存用户数据
@@ -370,13 +370,13 @@ export class CloudUser extends CloudObject {
             method = "PUT";
         }
 
-        let data:any = JSON.parse(JSON.stringify(this.data))
+        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 = {
+        let headersOptions: any = {
             "content-type": "application/json;charset=UTF-8",
             "x-parse-application-id": "dev",
             "x-parse-session-token": this.sessionToken, // 添加sessionToken以进行身份验证
@@ -396,18 +396,18 @@ export class CloudUser extends CloudObject {
         if (result?.objectId) {
             this.id = result?.objectId;
         }
-        localStorage.setItem("NCloud/dev/User",JSON.stringify(this.data))
+        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
-    }){
+export class CloudApi {
+    async fetch(path: string, body: any, options?: {
+        method: string
+        body: any
+    }) {
 
-        let reqOpts:any =  {
+        let reqOpts: any = {
             headers: {
                 "x-parse-application-id": "dev",
                 "Content-Type": "application/json"
@@ -416,15 +416,15 @@ export class CloudApi{
             mode: "cors",
             credentials: "omit"
         }
-        if(body||options?.body){
+        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 url = `${host}/api/` + path
+        console.log(url, reqOpts)
+        const response = await fetch(url, reqOpts);
         let json = await response.json();
         return json
     }

+ 61 - 0
src/modules/demo/page-training-plan/page-training-plan.component.html

@@ -0,0 +1,61 @@
+<ion-header>
+  <ion-toolbar color="primary">
+    <ion-title>个性化训练计划生成器</ion-title>
+    <ion-buttons slot="end">
+      <ion-button (click)="resetForm()">
+        <ion-icon slot="icon-only" name="refresh"></ion-icon>
+      </ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content class="ion-padding">
+  <div class="form-container">
+    <ion-item>
+      <ion-textarea label="您的训练目标" labelPlacement="floating" placeholder="例如: 增肌、减脂、提高耐力等" [(ngModel)]="userGoal"
+        [disabled]="isLoading" rows="3" autoGrow="true"></ion-textarea>
+    </ion-item>
+
+    <ion-item>
+      <ion-select label="您的健身水平" [(ngModel)]="fitnessLevel" [disabled]="isLoading">
+        <ion-select-option *ngFor="let level of fitnessLevels" [value]="level.value">
+          {{level.label}}
+        </ion-select-option>
+      </ion-select>
+    </ion-item>
+
+    <ion-item>
+      <ion-range label="每周可训练天数: {{availableDays}}天" [(ngModel)]="availableDays" [disabled]="isLoading" min="1" max="7"
+        snaps="true"></ion-range>
+    </ion-item>
+
+    <ion-item>
+      <ion-range label="每次训练时间: {{availableTime}}分钟" [(ngModel)]="availableTime" [disabled]="isLoading" min="15"
+        max="120" step="5"></ion-range>
+    </ion-item>
+
+    <ion-button expand="block" color="primary" (click)="generatePlan()" [disabled]="isLoading || !userGoal.trim()">
+      <ion-spinner *ngIf="isLoading" name="lines"></ion-spinner>
+      <span *ngIf="!isLoading">生成训练计划</span>
+    </ion-button>
+  </div>
+
+  <ion-card class="ai-response-card" *ngIf="aiContent">
+    <ion-card-header>
+      <ion-card-title>
+        <ion-icon name="barbell-outline"></ion-icon>
+        您的个性化训练计划
+      </ion-card-title>
+    </ion-card-header>
+    <ion-card-content>
+      <div class="ai-content">
+        @if(!isPlanGenerated){
+        <div [innerHTML]="aiContent"></div>
+        }
+        @if(isPlanGenerated){
+        <fm-markdown-preview class="content-style" [content]="aiContent"></fm-markdown-preview>
+        }
+      </div>
+    </ion-card-content>
+  </ion-card>
+</ion-content>

+ 23 - 0
src/modules/demo/page-training-plan/page-training-plan.component.scss

@@ -0,0 +1,23 @@
+.form-container {
+    max-width: 800px;
+    margin: 0 auto;
+}
+
+.ai-response-card {
+    margin-top: 20px;
+    background: var(--ion-color-light);
+
+    ion-card-header {
+        background: var(--ion-color-primary);
+        color: white;
+    }
+
+    .ai-content {
+        white-space: pre-line;
+        line-height: 1.6;
+    }
+}
+
+ion-spinner {
+    margin-right: 8px;
+}

+ 22 - 0
src/modules/demo/page-training-plan/page-training-plan.component.spec.ts

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

+ 95 - 0
src/modules/demo/page-training-plan/page-training-plan.component.ts

@@ -0,0 +1,95 @@
+import { Component, OnInit } from '@angular/core';
+import { IonicModule } from '@ionic/angular';
+import { FormsModule } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+import { FmodeChatCompletion, MarkdownPreviewModule } from 'fmode-ng';
+
+@Component({
+  selector: 'app-page-training-plan',
+  templateUrl: './page-training-plan.component.html',
+  styleUrls: ['./page-training-plan.component.scss'],
+  standalone: true,
+  imports: [IonicModule, FormsModule, CommonModule, MarkdownPreviewModule]
+})
+export class PageTrainingPlanComponent implements OnInit {
+  // 用户输入
+  userGoal: string = '';
+  fitnessLevel: string = 'beginner';
+  availableDays: number = 3;
+  availableTime: number = 30;
+
+  // AI响应
+  aiContent: string = '请填写您的训练目标,然后点击"生成计划"按钮';
+  isLoading: boolean = false;
+  isPlanGenerated: boolean = false;
+
+  // 训练水平选项
+  fitnessLevels = [
+    { value: 'beginner', label: '初级' },
+    { value: 'intermediate', label: '中级' },
+    { value: 'advanced', label: '高级' }
+  ];
+
+  constructor() { }
+
+  ngOnInit() { }
+
+  generatePlan() {
+    if (!this.userGoal.trim()) {
+      this.aiContent = '请输入您的训练目标';
+      return;
+    }
+
+    this.isLoading = true;
+    this.aiContent = '正在为您生成个性化训练计划...';
+    this.isPlanGenerated = false;
+
+    const now = new Date();
+    const timeParams = `当前时间:${now.toLocaleString()}`;
+
+    const prompt = `你是一名专业的健身教练,请根据以下信息为用户制定个性化的训练计划:
+    
+    用户目标: ${this.userGoal}
+    健身水平: ${this.fitnessLevel}
+    每周可训练天数: ${this.availableDays}天
+    每次训练时间: ${this.availableTime}分钟
+    
+    请生成一个详细、科学的训练计划,包括:
+    1. 训练目标概述
+    2. 每周训练安排
+    3. 每次训练的具体内容(动作名称、组数、次数、休息时间)
+    4. 注意事项和建议
+    
+    使用清晰易读的格式,适当使用标题和分段。`;
+
+    const completion = new FmodeChatCompletion([
+      { role: "system", content: timeParams },
+      { role: "user", content: prompt }
+    ]);
+
+    completion.sendCompletion().subscribe({
+      next: (message: any) => {
+        this.aiContent = message.content;
+        if (message.complete) {
+          this.isLoading = false;
+          this.isPlanGenerated = true;
+        }
+      },
+      error: (err) => {
+        this.aiContent = '生成计划时出错,请重试';
+        this.isLoading = false;
+        console.error(err);
+      }
+    });
+  }
+
+  resetForm() {
+    this.userGoal = '';
+    this.fitnessLevel = 'beginner';
+    this.availableDays = 3;
+    this.availableTime = 30;
+    this.aiContent = '请填写您的训练目标,然后点击"生成计划"按钮';
+    this.isPlanGenerated = false;
+  }
+
+}