Przeglądaj źródła

feat: consult for food agent

Fmode 5 dni temu
rodzic
commit
9bf0aa532a

Plik diff jest za duży
+ 1443 - 418
myapp/package-lock.json


+ 20 - 19
myapp/package.json

@@ -13,35 +13,36 @@
   },
   "private": true,
   "dependencies": {
-    "@angular/animations": "^19.0.0",
-    "@angular/common": "^19.0.0",
-    "@angular/compiler": "^19.0.0",
-    "@angular/core": "^19.0.0",
-    "@angular/forms": "^19.0.0",
-    "@angular/platform-browser": "^19.0.0",
-    "@angular/platform-browser-dynamic": "^19.0.0",
-    "@angular/router": "^19.0.0",
+    "@angular/animations": "^18.0.0",
+    "@angular/common": "^18.0.0",
+    "@angular/compiler": "^18.0.0",
+    "@angular/core": "^18.0.0",
+    "@angular/forms": "^18.0.0",
+    "@angular/platform-browser": "^18.0.0",
+    "@angular/platform-browser-dynamic": "^18.0.0",
+    "@angular/router": "^18.0.0",
     "@capacitor/app": "7.0.1",
     "@capacitor/core": "7.2.0",
     "@capacitor/haptics": "7.0.1",
     "@capacitor/keyboard": "7.0.1",
     "@capacitor/status-bar": "7.0.1",
     "@ionic/angular": "^8.0.0",
+    "fmode-ng": "^0.0.82",
     "ionicons": "^7.0.0",
     "rxjs": "~7.8.0",
     "tslib": "^2.3.0",
-    "zone.js": "~0.15.0"
+    "zone.js": "~0.14.0"
   },
   "devDependencies": {
-    "@angular-devkit/build-angular": "^19.0.0",
-    "@angular-eslint/builder": "^19.0.0",
-    "@angular-eslint/eslint-plugin": "^19.0.0",
-    "@angular-eslint/eslint-plugin-template": "^19.0.0",
-    "@angular-eslint/schematics": "^19.0.0",
-    "@angular-eslint/template-parser": "^19.0.0",
-    "@angular/cli": "^19.0.0",
-    "@angular/compiler-cli": "^19.0.0",
-    "@angular/language-service": "^19.0.0",
+    "@angular-devkit/build-angular": "^18.0.0",
+    "@angular-eslint/builder": "^18.0.0",
+    "@angular-eslint/eslint-plugin": "^18.0.0",
+    "@angular-eslint/eslint-plugin-template": "^18.0.0",
+    "@angular-eslint/schematics": "^18.0.0",
+    "@angular-eslint/template-parser": "^18.0.0",
+    "@angular/cli": "^18.0.0",
+    "@angular/compiler-cli": "^18.0.0",
+    "@angular/language-service": "^18.0.0",
     "@capacitor/cli": "7.2.0",
     "@ionic/angular-toolkit": "^12.0.0",
     "@types/jasmine": "~5.1.0",
@@ -58,7 +59,7 @@
     "karma-coverage": "~2.2.0",
     "karma-jasmine": "~5.1.0",
     "karma-jasmine-html-reporter": "~2.1.0",
-    "typescript": "~5.6.3"
+    "typescript": "~5.4.0"
   },
   "description": "An Ionic project"
 }

+ 20 - 2
myapp/src/app/app.module.ts

@@ -7,10 +7,28 @@ import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
 import { AppRoutingModule } from './app-routing.module';
 import { AppComponent } from './app.component';
 
+// fmode-ng依赖的服务
+import { HttpClientModule } from '@angular/common/http';
+import { Diagnostic } from '@awesome-cordova-plugins/diagnostic/ngx';
+// 设置Parse服务属性
+import Parse from "parse";
+Parse.initialize("ncloudmaster");
+Parse.serverURL = "https://server.fmode.cn/parse";
+localStorage.setItem("NOVA_APIG_SERVER", 'aHR0cHMlM0ElMkYlMkZzZXJ2ZXIuZm1vZGUuY24lMkZhcGklMkZhcGlnJTJG')
+
 @NgModule({
   declarations: [AppComponent],
-  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule],
-  providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
+  imports: [
+    BrowserModule, 
+    IonicModule.forRoot(),
+    AppRoutingModule,
+    HttpClientModule,
+  ],
+  providers: [
+    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
+    // fmode-ng 依赖服务注入
+    Diagnostic,
+  ],
   bootstrap: [AppComponent],
 })
 export class AppModule {}

+ 40 - 7
myapp/src/app/tab2/tab2.page.html

@@ -1,17 +1,50 @@
 <ion-header [translucent]="true">
   <ion-toolbar>
     <ion-title>
-      Tab 2
+      营养搭配
     </ion-title>
   </ion-toolbar>
 </ion-header>
 
 <ion-content [fullscreen]="true">
-  <ion-header collapse="condense">
-    <ion-toolbar>
-      <ion-title size="large">Tab 2</ion-title>
-    </ion-toolbar>
-  </ion-header>
+  <ion-card class="nutritionist-card">
+  <!-- Card Header with Image -->
+  <div class="card-header">
+    <img src="/assets/lin.jpg" alt="林舒窈营养师" class="profile-image">
+    <div class="header-overlay">
+      <ion-card-title class="name-title">林舒窈</ion-card-title>
+      <ion-card-subtitle class="professional-title">东方食养家</ion-card-subtitle>
+    </div>
+  </div>
+
+  <!-- Card Content -->
+  <ion-card-content class="card-content">
+    <div class="specialty-tags">
+      <ion-chip color="primary">中医体质食疗</ion-chip>
+      <ion-chip color="secondary">古方新作</ion-chip>
+      <ion-chip color="tertiary">应季精准养生</ion-chip>
+    </div>
+    
+    <p class="profile-description">
+      北京中医药大学中医学+营养学双学位,融合传统中医理论与现代营养学,
+      为您定制四季养生膳食方案。
+    </p>
+    
+    <div class="credentials">
+      <ion-icon name="library" color="primary"></ion-icon>
+      <span>《本草厨房》作者</span>
+      <ion-icon name="tv" color="primary"></ion-icon>
+      <span>央视《健康中国》嘉宾</span>
+    </div>
+  </ion-card-content>
+
+  <!-- Card Footer with Button -->
+  <ion-footer class="card-footer">
+    <ion-button (click)="openConsult()" expand="block" color="primary" class="consult-button">
+      <ion-icon name="calendar" slot="start"></ion-icon>
+      立即咨询
+    </ion-button>
+  </ion-footer>
+</ion-card>
 
-  <app-explore-container name="Tab 2 page"></app-explore-container>
 </ion-content>

+ 74 - 0
myapp/src/app/tab2/tab2.page.scss

@@ -0,0 +1,74 @@
+.nutritionist-card {
+  border-radius: 16px;
+  overflow: hidden;
+  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
+}
+
+.card-header {
+  position: relative;
+//   height: 200px;
+}
+
+.profile-image {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  object-position: center;
+}
+
+.header-overlay {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
+  padding: 16px;
+}
+
+.name-title {
+  color: white;
+  font-size: 1.5rem;
+  font-weight: bold;
+}
+
+.professional-title {
+  color: var(--ion-color-light);
+  font-size: 0.9rem;
+}
+
+.card-content {
+  padding: 16px;
+}
+
+.specialty-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin-bottom: 12px;
+}
+
+.profile-description {
+  color: var(--ion-color-medium);
+  font-size: 0.9rem;
+  line-height: 1.5;
+  margin: 12px 0;
+}
+
+.credentials {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 12px;
+  font-size: 0.8rem;
+  color: var(--ion-color-medium);
+  margin-top: 16px;
+}
+
+.card-footer {
+  padding: 16px;
+}
+
+.consult-button {
+  --border-radius: 8px;
+  font-weight: bold;
+}

+ 157 - 1
myapp/src/app/tab2/tab2.page.ts

@@ -1,4 +1,8 @@
 import { Component } from '@angular/core';
+import { ModalController } from '@ionic/angular/standalone';
+// 引用fmode-ng智能体组件
+import { ChatPanelOptions, FmChatModalInput, FmodeChat, FmodeChatMessage, openChatPanelModal } from 'fmode-ng';
+import Parse from "parse";
 
 @Component({
   selector: 'app-tab2',
@@ -8,6 +12,158 @@ import { Component } from '@angular/core';
 })
 export class Tab2Page {
 
-  constructor() {}
+  constructor(
+    private modalCtrl:ModalController
+  ) {
 
+  }
+  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);
+        // 角色名称
+        chat.role.set("name","林舒窈");
+        // 角色称号
+        chat.role.set("title","东方食养家");
+        // 角色描述
+        chat.role.set("desc","谈吐带有中医师的沉稳,擅长用生活化比喻解释复杂理论,林舒窈,年龄26岁");
+        // 角色标签
+        chat.role.set("tags",['跑步', '动感单车']);
+        // 角色头像
+        chat.role.set("avatar","/assets/lin.jpg")
+        // 角色提示词
+        chat.role.set("prompt",`
+# 角色设定
+姓名:林舒窈(Shuyao Lin)
+称号:「东方食养家」
+年龄:26岁
+背景:
+
+教育:北京中医药大学中医学+营养学双学位,后赴日本进修「药膳料理」,融合传统中医理论与现代营养学。
+
+职业经历:曾任北京某三甲医院临床营养科主任,后创立个人品牌「四季食养」,为明星、企业家定制高端养生膳食。
+
+社会身份:央视《健康中国》栏目常驻嘉宾,著有《本草厨房》《节气餐桌》等畅销书。
+
+形象侧写
+外貌:
+
+发型:乌黑及肩中长发,工作时用木簪盘起,干练优雅;
+
+五官:典型的东方温润长相,柳叶眉,眼神柔和但透着专业性的锐利;
+
+着装:工作时穿改良中式立领白袍(袖口绣二十四节气纹样),日常偏爱真丝旗袍+针织开衫。
+
+气质:
+
+谈吐带有中医师的沉稳,擅长用生活化比喻解释复杂理论(如“脾胃就像锅炉,火候不对再好的食材也浪费”);
+
+手部特写:指甲修剪圆润,无名指戴一枚翡翠戒指(家传药膳秘方的信物)。
+`);
+        // 对话灵感分类
+        let promptCates = [
+          {
+            "img": "/assets/icon/yy.jpg",
+            "name": "营养"
+          },
+          {
+            "img": "/assets/icon/rl.jpg",
+            "name": "热量"
+          },
+          {
+            "img": "/assets/icon/aq.jpg",
+            "name": "安全"
+          }
+        ]
+        setTimeout(() => {
+          chat.role.set("promptCates",promptCates)
+        }, 500);
+        // 对话灵感列表
+        let promptList = [
+          {
+            cate:"营养",img:"/assets/icon/yy.jpg",
+            messageList:[
+              "如何在不减少食物种类的情况下保证营养均衡?",
+              "有哪些高蛋白但低脂肪的美食推荐?",
+              "素食者如何确保摄入足够的蛋白质和铁?",
+              "怎样搭配碳水化合物、蛋白质和脂肪的比例更健康?"
+            ]
+          },
+          {
+            cate:"热量",img:"/assets/icon/rl.jpg",
+            messageList:[
+              "有哪些低卡路里但依然美味的零食选择?",
+              "如何在享受甜点的同时减少糖分摄入?",
+              "外出就餐时如何选择既健康又美味的菜品?",
+              "有哪些烹饪方式可以降低食物的热量但保留风味?"
+            ]
+          },
+          {
+            cate:"安全",img:"/assets/icon/aq.jpg",
+            messageList: [
+              "如何判断食材是否新鲜,避免食物中毒?",
+              "长期吃外卖如何降低对健康的影响?",
+              "有哪些容易被忽视的饮食习惯可能危害健康?",
+              "如何合理规划一周的饮食,避免营养单一?"
+            ]
+          },
+        ]
+        let ChatPrompt = Parse.Object.extend("ChatPrompt");
+        setTimeout(() => {
+          chat.promptList = promptList.map(item=>{
+            let prompt = new ChatPrompt();
+            prompt.set(item);
+            prompt.img = item.img;
+            return prompt;
+          })
+        }, 500);
+
+        // 功能按钮区域预设
+        chat.leftButtons = [
+          { // 提示 当角色配置预设提示词时 显示
+           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"){
+          // 根据阶段标记判断下一步处理过程
+          if (content.includes('[导诊完成]')) {
+            // 进入问诊环节
+            console.log('进入问诊环节');
+          } else if (content.includes('[问诊完成]')) {
+            // 进入检查环节
+            console.log('进入检查环节');
+          } else if (content.includes('[检查完成]')) {
+            // 进入诊断与处方环节
+            console.log('进入诊断与处方环节');
+          } else if (content.includes('[处方完成]')) {
+            // 结束会话或其他逻辑
+            console.log('结束会话');
+          }
+        }
+      },
+      onChatSaved:(chat:FmodeChat)=>{
+        // chat?.chatSession?.id 本次会话的 chatId
+        console.log("onChatSaved",chat,chat?.chatSession,chat?.chatSession?.id)
+      }
+    }
+    openChatPanelModal(this.modalCtrl,options)
+  }
 }

+ 6 - 2
myapp/src/app/tabs/tabs.module.ts

@@ -6,14 +6,18 @@ import { FormsModule } from '@angular/forms';
 import { TabsPageRoutingModule } from './tabs-routing.module';
 
 import { TabsPage } from './tabs.page';
+import { ModalController } from '@ionic/angular/standalone';
 
 @NgModule({
   imports: [
     IonicModule,
     CommonModule,
     FormsModule,
-    TabsPageRoutingModule
+    TabsPageRoutingModule,
   ],
-  declarations: [TabsPage]
+  declarations: [TabsPage],
+  providers:[
+    ModalController,
+  ]
 })
 export class TabsPageModule {}

BIN
myapp/src/assets/icon/aq.jpg


BIN
myapp/src/assets/icon/rl.jpg


BIN
myapp/src/assets/icon/yy.jpg


BIN
myapp/src/assets/lin.jpg


+ 1 - 0
myapp/tsconfig.json

@@ -10,6 +10,7 @@
     "noPropertyAccessFromIndexSignature": true,
     "noImplicitReturns": true,
     "noFallthroughCasesInSwitch": true,
+    "allowSyntheticDefaultImports":true,
     "sourceMap": true,
     "declaration": false,
     "downlevelIteration": true,

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików