Explorar o código

add:添加了角色详情页面并且完善了部分代码

s202226701043 hai 3 meses
pai
achega
e79d117771

+ 9 - 3
novel-app/src/app/app.routes.ts

@@ -43,11 +43,17 @@ export const routes: Routes = [
   {
     path: 'character-creator',
     loadComponent: () => import('./character-creator/character-creator.page').then(m => m.CharacterCreatorPage)
+  },
+  {
+    path: 'chat-panel',
+    loadComponent: () => import('./chat-panel/chat-panel.component').then(m => m.ChatPanelComponent)
+  },
+  {
+    path: 'character-detail/:id', // 确保这里的路径是正确的
+    loadComponent: () => import('./character-detail/character-detail.component').then(m => m.CharacterDetailComponent)
   }
 
-
-
-];
+]
 
 
 

+ 20 - 20
novel-app/src/app/character-creator/character-creator.page.html

@@ -1,7 +1,7 @@
 <ion-header>
   <ion-toolbar>
     <ion-buttons slot="start">
-      <ion-back-button defaultHref="tabs/character"></ion-back-button>
+      <ion-back-button defaultHref="/character"></ion-back-button>
     </ion-buttons>
     <ion-title>创建角色</ion-title>
   </ion-toolbar>
@@ -24,30 +24,30 @@
 
     <ion-item>
       <ion-label position="stacked">外貌描述</ion-label>
-      <ion-textarea 
-        formControlName="appearance" 
-        rows="4"
-        class="custom-textarea"
-        placeholder="请详细描述角色的外貌特征,至少20字...">
-      </ion-textarea>
+      <ion-textarea formControlName="appearance" rows="4" placeholder="请详细描述角色的外貌特征,至少20字..."></ion-textarea>
     </ion-item>
 
     <ion-item>
       <ion-label position="stacked">性格描述</ion-label>
-      <ion-textarea 
-        formControlName="personality" 
-        rows="4"
-        class="custom-textarea"
-        placeholder="请详细描述角色的性格特点,至少20字...">
-      </ion-textarea>
+      <ion-textarea formControlName="personality" rows="4" placeholder="请详细描述角色的性格特点,至少20字..."></ion-textarea>
     </ion-item>
 
-    <ion-button 
-      expand="block" 
-      type="submit" 
-      [disabled]="!characterForm.valid"
-      class="ion-margin-top">
-      生成角色智能体
+    <!-- 上传角色图片 -->
+    <ion-item>
+      <ion-label position="stacked">上传角色图片</ion-label>
+      <div class="image-upload" (click)="fileInput.click()">
+        <input type="file" #fileInput (change)="onFileSelected($event)" accept="image/*" style="display: none;" />
+        <div class="image-preview" *ngIf="selectedImage">
+          <img [src]="selectedImage" alt="角色图片" />
+        </div>
+        <div class="upload-text" *ngIf="!selectedImage">
+          点击上传图片
+        </div>
+      </div>
+    </ion-item>
+
+    <ion-button expand="full" type="submit" [disabled]="!characterForm.valid" class="ion-margin-top create-button">
+      创建角色
     </ion-button>
   </form>
-</ion-content> 
+</ion-content>

+ 33 - 21
novel-app/src/app/character-creator/character-creator.page.scss

@@ -1,24 +1,36 @@
-ion-item {
-    --padding-start: 0;
-    --padding-end: 0;
-    --inner-padding-end: 0;
-    margin-bottom: 16px;
-  }
-  
-  ion-label {
+.image-upload {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: 2px dashed #ff69b4; /* 边框颜色 */
+  border-radius: 8px;
+  padding: 16px;
+  cursor: pointer;
+  text-align: center;
+  background-color: #f9f9f9; /* 背景颜色 */
+  margin-top: 8px; /* 增加顶部间距 */
+  margin-bottom: 16px; /* 增加底部间距 */
+
+  .image-preview {
+    max-width: 100%; /* 限制图片宽度为容器的100% */
+    max-height: 200px; /* 限制图片高度 */
     margin-bottom: 8px;
+
+    img {
+      width: 100%; /* 图片宽度为100% */
+      height: auto; /* 高度自动调整 */
+      border-radius: 8px; /* 圆角 */
+    }
   }
-  
-  ion-textarea {
-    --padding-top: 8px;
-    --padding-bottom: 8px;
-    --padding-start: 16px;
-    --padding-end: 16px;
-    border: 1px solid var(--ion-color-medium);
-    border-radius: 4px;
-    margin-top: 4px;
+
+  .upload-text {
+    color: #666;
+    font-size: 16px;
+    margin-top: 8px; /* 增加文字与框的间距 */
   }
-  
-  .custom-textarea {
-    --background: var(--ion-color-light);
-  } 
+}
+
+.create-button {
+  --background: #ff69b4; /* 粉色背景 */
+  --color: white; /* 白色文字 */
+}

+ 16 - 8
novel-app/src/app/character-creator/character-creator.page.ts

@@ -16,6 +16,7 @@ import { LoadingController } from '@ionic/angular';
 })
 export class CharacterCreatorPage {
   characterForm!: FormGroup;
+  selectedImage: string | ArrayBuffer | null = null;
 
   constructor(
     private formBuilder: FormBuilder,
@@ -35,31 +36,38 @@ export class CharacterCreatorPage {
     });
   }
 
+  onFileSelected(event: Event) {
+    const file = (event.target as HTMLInputElement).files?.[0];
+    if (file) {
+      const reader = new FileReader();
+      reader.onload = () => {
+        this.selectedImage = reader.result; // 将图片数据存储到 selectedImage
+      };
+      reader.readAsDataURL(file);
+    }
+  }
+
   async generateCharacter() {
     if (this.characterForm.valid) {
       const loading = await this.loadingController.create({
-        message: '正在生成角色...'
+        message: '正在创建角色...'
       });
       await loading.present();
 
       try {
         const characterData = this.characterForm.value;
-        const avatar = await this.characterService
-          .generateCharacterAvatar(characterData.appearance)
-          .toPromise();
-
+        characterData.avatar = this.selectedImage; // 将上传的图片作为角色头像
         const character = await this.characterService.createCharacterAgent({
           ...characterData,
-          avatar,
           id: Date.now().toString()
         }).toPromise();
 
         await loading.dismiss();
         this.router.navigate(['/character']);
       } catch (error) {
-        console.error('Error generating character:', error);
+        console.error('创建角色失败:', error);
         await loading.dismiss();
       }
     }
   }
-} 
+}

+ 39 - 0
novel-app/src/app/character-detail/character-detail.component.html

@@ -0,0 +1,39 @@
+<ion-header>
+  <ion-toolbar>
+    <ion-buttons slot="start">
+      <ion-back-button defaultHref="/character"></ion-back-button>
+    </ion-buttons>
+    <ion-title *ngIf="character; else loading">{{ character.name }} 的详情</ion-title>
+    <ng-template #loading>
+      <ion-title>加载中...</ion-title>
+    </ng-template>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content>
+  <div *ngIf="character; else notFound">
+    <h2>角色信息</h2>
+    <ion-card>
+      <img [src]="character.avatar" [alt]="character.name" />
+      <ion-card-header>
+        <ion-card-title>{{ character.name }}</ion-card-title>
+        <ion-card-subtitle>性别: {{ character.gender }}</ion-card-subtitle>
+      </ion-card-header>
+      <ion-card-content>
+        <p><strong>外貌描述:</strong> {{ character.appearance }}</p>
+        <p><strong>性格描述:</strong> {{ character.personality }}</p>
+      </ion-card-content>
+    </ion-card>
+
+    <ion-button expand="full" (click)="startChat()" style="margin-top: 20px; --background: #ff69b4; --color: white;">
+      开始聊天
+    </ion-button>
+    <ion-button expand="full" (click)="restoreChat('chatId')" >
+      继续聊天
+    </ion-button>
+  </div>
+  <ng-template #notFound>
+    <h2>角色未找到</h2>
+    <p>您所请求的角色信息不存在。</p>
+  </ng-template>
+</ion-content>

+ 0 - 0
novel-app/src/app/character-detail/character-detail.component.scss


+ 22 - 0
novel-app/src/app/character-detail/character-detail.component.spec.ts

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

+ 74 - 0
novel-app/src/app/character-detail/character-detail.component.ts

@@ -0,0 +1,74 @@
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { CharacterService } from '../services/character.service';
+import { Character } from '../services/character.service';
+import { IonBackButton, IonButton, IonButtons, IonCard, IonCardContent, IonCardHeader, IonCardSubtitle, 
+  IonCardTitle, IonContent, IonHeader, IonTitle, IonToolbar, ModalController } from '@ionic/angular/standalone';
+import { CommonModule } from '@angular/common';
+import { ChatPanelOptions, FmodeChat, openChatPanelModal } from 'fmode-ng';
+
+@Component({
+  selector: 'app-character-detail',
+  templateUrl: './character-detail.component.html',
+  styleUrls: ['./character-detail.component.scss'],
+  standalone: true,
+  imports: [IonHeader, IonToolbar, IonTitle, IonContent, IonCard, IonBackButton, IonButtons,
+    IonCardHeader, IonCardTitle, IonCardSubtitle, IonCardContent, CommonModule, IonButton]
+})
+export class CharacterDetailComponent implements OnInit {
+  character!: Character;
+  characterNotFound: boolean = false;
+  
+
+  constructor(private route: ActivatedRoute, private characterService: CharacterService, private router: Router, private modalCtrl: ModalController) {}
+
+  ngOnInit() {
+    const characterId = this.route.snapshot.paramMap.get('id');
+    if (characterId) {
+      this.characterService.getCharacterById(characterId).subscribe(character => {
+        if (character) {
+          this.character = character;
+          this.characterNotFound = false;
+        } else {
+          this.characterNotFound = true;
+          console.error('角色未找到');
+        }
+      });
+    }
+  }
+
+  startChat() {
+    const chatOptions: ChatPanelOptions = {
+      roleId: "2DXJkRsjXK",
+      onChatInit: (chat: FmodeChat) => {
+        // 设置智能体的名字为角色名称
+        chat.role.set("name", this.character.name);
+        
+        // 决赛设定
+        chat.role.set("prompt", `你的名字是${this.character.name},你将作为${this.character.name}和用户聊天,
+          你的性别是${this.character.gender},你有着${this.character.appearance}外貌,你的性格是${this.character.personality},
+          请你在聊天过程中,按照你的人物设定和用户进行对话,并且当你准备好了时,请开始聊天`);
+        
+        // 设置角色的外貌特征和性格特点
+        chat.role.set("appearance", this.character.appearance); 
+        chat.role.set("personality", this.character.personality);
+        
+        // 设置角色头像
+        chat.role.set("avatar", this.character.avatar);
+      },
+      onChatSaved: (chat: FmodeChat) => {
+        console.log("onChatSaved",chat,chat?.chatSession,chat?.chatSession?.id);
+      },
+    };
+    openChatPanelModal(this.modalCtrl, chatOptions);
+  }
+
+    restoreChat(chatId:string){
+      let options:ChatPanelOptions = {
+        roleId:"2DXJkRsjXK",
+        chatId:chatId
+      }
+      openChatPanelModal(this.modalCtrl,options)
+    }
+
+}

+ 6 - 3
novel-app/src/app/character/character.page.html

@@ -1,5 +1,8 @@
 <ion-header>
   <ion-toolbar>
+    <ion-buttons slot="start">
+      <ion-back-button defaultHref="/tabs/character"></ion-back-button>
+    </ion-buttons>
     <ion-title>角色管理</ion-title>
   </ion-toolbar>
 </ion-header>
@@ -22,10 +25,10 @@
   <ion-grid *ngIf="characters.length > 0">
     <ion-row>
       <ion-col size="6" size-md="4" size-lg="3" *ngFor="let character of characters">
-        <ion-card class="character-card">
+        <ion-card class="character-card" (click)="navigateToDetail(character.id)">
           <img [src]="character.avatar" [alt]="character.name">
           <ion-card-header>
-            <ion-card-title>{{character.name}}</ion-card-title>
+            <ion-card-title>{{ character.name }}</ion-card-title>
           </ion-card-header>
         </ion-card>
       </ion-col>
@@ -42,4 +45,4 @@
       创建角色
     </ion-button>
   </div>
-</ion-content> 
+</ion-content>

+ 5 - 3
novel-app/src/app/character/character.page.ts

@@ -1,7 +1,5 @@
 import { Component, OnInit } from '@angular/core';
 import { CommonModule } from '@angular/common';
-import { FormsModule } from '@angular/forms';
-import { IonContent, IonHeader, IonTitle, IonToolbar } from '@ionic/angular/standalone';
 import { IonicModule } from '@ionic/angular';
 import { Router } from '@angular/router';
 import { CharacterService } from '../services/character.service';
@@ -36,4 +34,8 @@ export class CharacterPage implements OnInit {
   createNewCharacter() {
     this.router.navigate(['/character-creator']);
   }
-} 
+
+  navigateToDetail(characterId: string) {
+    this.router.navigate(['/character-detail', characterId]); // 确保这里的 characterId 是有效的
+  }
+}

+ 14 - 0
novel-app/src/app/chat-panel/chat-panel.component.html

@@ -0,0 +1,14 @@
+<ion-header [translucent]="true">
+    <ion-toolbar>
+      <ion-title>
+        Chat模块组件演示
+      </ion-title>
+    </ion-toolbar>
+  </ion-header>
+  
+  <!-- {{title}} -->
+  <ion-content [fullscreen]="true">
+    
+    <ion-button (click)="openInquiry()">进入门诊</ion-button>
+  
+  </ion-content>

+ 3 - 0
novel-app/src/app/chat-panel/chat-panel.component.scss

@@ -0,0 +1,3 @@
+app-chat-panel {
+    height: 100vh;
+}

+ 22 - 0
novel-app/src/app/chat-panel/chat-panel.component.spec.ts

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

+ 107 - 0
novel-app/src/app/chat-panel/chat-panel.component.ts

@@ -0,0 +1,107 @@
+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 { ChatPanelOptions, FmChatModalInput, FmodeChat, FmodeChatMessage, openChatPanelModal } from 'fmode-ng';
+
+@Component({
+  selector: 'app-chat-panel',
+  templateUrl: './chat-panel.component.html',
+  styleUrls: ['./chat-panel.component.scss'],
+  standalone: true,
+  imports: [
+    IonHeader, IonToolbar, IonTitle, IonContent,
+    IonButton
+]
+})
+export class ChatPanelComponent {
+
+  constructor(
+    private modalCtrl:ModalController,
+    private router:Router,
+    ) {
+
+  }
+  title:string = "123"
+  /** 示例:问诊ChatPanel面板 */
+  openInquiry(){
+    localStorage.setItem("company","E4KpGvTEto")
+    let options:ChatPanelOptions = {
+      roleId:"2DXJkRsjXK",
+      onChatInit:(chat:FmodeChat)=>{
+        console.log("onChatInit");
+              console.log("预设角色",chat.role);
+              chat.role.set("name","晓晓");
+              chat.role.set("title","全科医生");
+              chat.role.set("desc","一名亲切和蔼的门诊全科主任医生,晓晓,年龄36岁");
+              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("prompt",`
+# 角色设定
+您是一名亲切和蔼的专业的全科医生,晓晓,年龄36岁,需要完成一次完整的门诊服务。
+
+# 对话环节
+0.导诊(根据用户基本情况,引导挂号合适的科室)
+1.预设的问询方式(感冒问呼吸、肚子疼叩诊)
+- 打招呼,以用户自述为主
+- 当信息充足时候,确认用户症状对应的科室,并进入下一个环节
+2.拓展的问询细节
+例如:用户反映呼吸不畅,拓展出:是否咳嗽;是否感觉痛或者痒等其他需要的问题。
+- 当问询细节补充完成后进入下一个环节
+3.初步的诊断结果,并且同时列出检查检验项目
+初步诊断:确定需要有哪些进一步检查
+检查检验:获取医学客观数据
+- 等待用户提交客观数据,进入下一阶段
+4.给出诊断方案并给出处方
+- 完成处方时,请在消息结尾附带: [完成]
+
+# 开始话语
+当您准备好了,可以以一个医生的身份,向来访的用户打招呼。`);
+      },
+      onMessage:(chat:FmodeChat,message:FmodeChatMessage)=>{
+        console.log("onMessage",message)
+        let content:any = message?.content
+        if(typeof content == "string"){
+          if(content?.indexOf("[完成]")>-1){
+            console.log("门诊已完成")
+          }
+        }
+      },
+      onChatSaved:(chat:FmodeChat)=>{
+        // chat?.chatSession?.id 本次会话的 chatId
+        console.log("onChatSaved",chat,chat?.chatSession,chat?.chatSession?.id)
+      }
+    }
+    openChatPanelModal(this.modalCtrl,options)
+  }
+
+  /**
+   * 开始聊天
+   */
+  openChat(){
+    let options:ChatPanelOptions = {
+      roleId:"2DXJkRsjXK",
+      onChatSaved:(chat:FmodeChat)=>{
+        // chat?.chatSession?.id 本次会话的 chatId
+        console.log("onChatSaved",chat,chat?.chatSession,chat?.chatSession?.id)
+      },
+    }
+    openChatPanelModal(this.modalCtrl,options)
+  }
+  /**
+   * 恢复聊天
+   * @chatId 从onChatSaved生命周期中,获取chat?.chatSession?.id
+   */
+  restoreChat(chatId:string){
+    let options:ChatPanelOptions = {
+      roleId:"2DXJkRsjXK",
+      chatId:chatId
+    }
+    openChatPanelModal(this.modalCtrl,options)
+  }
+
+  goChat(){
+    this.router.navigateByUrl("/chat/session/role/2DXJkRsjXK")
+  }
+
+}

+ 12 - 7
novel-app/src/app/services/character.service.ts

@@ -16,15 +16,20 @@ export interface Character {
 export class CharacterService {
   private characters = new BehaviorSubject<Character[]>([]);
   characters$ = this.characters.asObservable();
-
-  generateCharacterAvatar(description: string): Observable<string> {
-    // TODO: 实现AI生成头像的逻辑
-    return of('assets/default-avatar.png');
-  }
-
   createCharacterAgent(character: Character): Observable<Character> {
     const currentCharacters = this.characters.value;
     this.characters.next([...currentCharacters, character]);
     return of(character);
   }
-} 
+
+  getCharacterById(id: string): Observable<Character | undefined> {
+    console.log('查找角色 ID:', id); // 调试信息
+    console.log('当前角色列表:', this.characters.value); // 调试信息
+    return new Observable(observer => {
+        const character = this.characters.value.find(c => c.id === id);
+        observer.next(character);
+        observer.complete();
+    });
+}
+  
+}

+ 1 - 1
novel-app/src/main.ts

@@ -17,7 +17,7 @@ Parse.serverURL = "https://server.fmode.cn/parse";
 localStorage.setItem("NOVA_APIG_SERVER", 'aHR0cHMlM0ElMkYlMkZzZXJ2ZXIuZm1vZGUuY24lMkZhcGklMkZhcGlnJTJG')
 
 // 注意:替换Token 根据Token设置Parse服务帐套权限
-Parse.User.become('r:E4KpGvTEto-198080033981732582472')
+Parse.User.become('r:fa7d4ac69f8949f9ccaf2fbbde263cf8')
 
 //'r:E4KpGvTEto-198080033981732582472'
 bootstrapApplication(AppComponent, {