interest-search.component.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. import { Component, OnInit, ViewChild } from '@angular/core';
  2. import {
  3. IonTextarea, IonCheckbox, IonList, IonButton, IonContent, IonHeader, IonInput, IonTitle,
  4. IonToolbar, IonItem, IonLabel, IonRadioGroup, IonRadio, IonDatetimeButton, IonDatetime,
  5. IonModal, IonAlert, IonBackButton, IonButtons
  6. } from '@ionic/angular/standalone';
  7. import { CloudQuery, CloudObject, Pointer } from '../../lib/ncloud'; // 确保路径正确
  8. import { CommonModule, DatePipe } from '@angular/common'; // 导入 CommonModule
  9. import { FormsModule } from '@angular/forms'; // 导入 FormsModule
  10. import { FmodeChatCompletion, MarkdownPreviewModule } from 'fmode-ng';
  11. import { AlertController } from '@ionic/angular';
  12. // 定义接口以确保类型安全
  13. interface Questionnaire {
  14. objectId: string;
  15. createdAt: string;
  16. QuestionnaireId: string;
  17. title: string;
  18. status: string;
  19. questions: string[]; // 修改为字符串数组
  20. }
  21. interface Question {
  22. objectId: string;
  23. createdAt: string;
  24. QuestionId: string;
  25. questionnaireId: string; // 修改为字符串
  26. questionText: string;
  27. options: string[]; // 修改为字符串数组
  28. }
  29. interface Option {
  30. objectId: string;
  31. createdAt: string;
  32. OptionId: string;
  33. questionId: string; // 修改为字符串
  34. optionText: string;
  35. isSelected: boolean;
  36. }
  37. interface QuestionnaireResult {
  38. objectId: string;
  39. createdAt: string;
  40. QuestionnaireResultId: string;
  41. userId: Pointer;
  42. questionnaireId: Pointer;
  43. answers: Pointer[];
  44. }
  45. interface UserInterestProfile {
  46. objectId: string;
  47. createdAt: string;
  48. userId: String;
  49. QuestionnaireId: String;
  50. interestTags: String[];
  51. content: String;
  52. }
  53. interface QuestionWithOptions extends Question {
  54. optionsData: Option[];
  55. }
  56. @Component({
  57. selector: 'app-interest-search',
  58. templateUrl: './interest-search.component.html',
  59. styleUrls: ['./interest-search.component.scss'],
  60. standalone: true,
  61. imports: [IonTextarea, IonCheckbox, IonList, IonButton, IonContent, IonHeader, IonInput,
  62. IonTitle, IonToolbar, IonItem, IonLabel, IonRadioGroup, IonRadio, IonDatetimeButton,
  63. IonDatetime, IonModal, CommonModule, FormsModule, IonDatetime, IonModal, IonAlert,
  64. IonBackButton, IonButtons, MarkdownPreviewModule
  65. ]
  66. })
  67. export class InterestSearchComponent implements OnInit {
  68. // 固定字段
  69. name: string = '';
  70. birthday: string = '';
  71. // 动态问卷数据
  72. questionnaire: Questionnaire | null = null;
  73. questionsWithOptions: QuestionWithOptions[] = [];
  74. answers: { [questionId: string]: string } = {}; // 存储用户答案
  75. //新增AI分析部分的变量
  76. aiAnalysisResult: { interestTags: string[], content: string } | null = null; // AI 分析结果
  77. isComplete: boolean = false; // 定义完成状态属性,用来标记是否补全完成
  78. @ViewChild(IonModal) modal!: IonModal; // 引入 IonModal 以控制其打开和关闭
  79. modalIsOpen: boolean = false; // 使用 isOpen 控制 Modal 的显示状态
  80. modalContent: string = ''; // 保存弹窗的内容
  81. constructor() { }
  82. // 定义方法,用于获取 <ion-datetime> 组件选择的值
  83. onDateTimeChange(event: any) {
  84. this.birthday = event.detail.value;
  85. // // 使用DatePipe进行日期格式化,只保留年、月、日
  86. // this.birthday = this.datePipe.transform(this.birthday, 'yyyy-MM-dd')!;
  87. console.log('选择的日期为:', this.birthday);
  88. }
  89. alertButtons = ['确定'];
  90. ngOnInit() {
  91. this.loadQuestionnaireData('q1'); // 使用 QuestionnaireId 'q1'
  92. //this.loadQuestionnaireData(this.getRandomQuestionnaire());
  93. }
  94. getRandomQuestionnaire() {
  95. const questionnaires = ['q1', 'q2', 'q3']; // List of your questionnaires
  96. const randomIndex = Math.floor(Math.random() * questionnaires.length); // Generate a random index
  97. return questionnaires[randomIndex]; // Return the randomly selected questionnaire ID
  98. }
  99. async loadQuestionnaireData(questionnaireId: string) {
  100. try {
  101. const questionnaireQuery = new CloudQuery("Questionnaire");
  102. questionnaireQuery.equalTo("QuestionnaireId", questionnaireId);
  103. const questionnaireObj = await questionnaireQuery.first();
  104. if (questionnaireObj) {
  105. const questionnaireData = questionnaireObj.data as Questionnaire;
  106. // 确保 objectId 存在且为字符串
  107. this.questionnaire = {
  108. ...questionnaireData,
  109. objectId: String(questionnaireObj.id)
  110. };
  111. console.log("加载到的问卷数据:", this.questionnaire);
  112. // 确保 questions 被正确传入
  113. if (this.questionnaire.questions) {
  114. await this.loadQuestions(this.questionnaire.questions);
  115. }
  116. } else {
  117. console.error(`未找到 QuestionnaireId 为 ${questionnaireId} 的问卷。`);
  118. }
  119. } catch (error) {
  120. console.error("加载问卷数据时出错:", error);
  121. }
  122. }
  123. async loadQuestions(questionIds: string[]) {
  124. this.questionsWithOptions = []; // 初始化问题列表
  125. for (const questionId of questionIds) {
  126. try {
  127. const questionQuery = new CloudQuery("Question");
  128. questionQuery.equalTo("QuestionId", questionId);
  129. const questionObj = await questionQuery.first();
  130. if (questionObj) {
  131. const question = questionObj.data as Question;
  132. // 异步加载选项并立即显示问题
  133. this.questionsWithOptions.push({ ...question, optionsData: [] });
  134. this.loadOptions(question.options).then((options) => {
  135. const index = this.questionsWithOptions.findIndex(
  136. (q) => q.QuestionId === question.QuestionId
  137. );
  138. if (index !== -1) {
  139. this.questionsWithOptions[index].optionsData = options;
  140. }
  141. });
  142. // 可选:每加载一个问题,立即触发渲染
  143. console.log("已加载问题:", question);
  144. }
  145. } catch (error) {
  146. console.error(`加载问题 ID ${questionId} 时出错:`, error);
  147. }
  148. }
  149. }
  150. async loadOptions(optionIds: string[]): Promise<Option[]> {
  151. try {
  152. if (!optionIds || optionIds.length === 0) return [];
  153. const optionQuery = new CloudQuery("Option");
  154. optionQuery.containedIn("OptionId", optionIds); // 批量查询
  155. const optionObjs = await optionQuery.find();
  156. return optionObjs.map((optionObj: any) => optionObj.data as Option);
  157. } catch (error) {
  158. console.error("加载选项时出错:", error);
  159. return [];
  160. }
  161. }
  162. // 保存功能(可选)
  163. async save() {
  164. try {
  165. // 实现保存逻辑,例如保存到本地存储或发送到后台
  166. console.log("保存的答案:", this.answers);
  167. console.log("姓名:", this.name);
  168. console.log("生日:", this.birthday);
  169. } catch (error) {
  170. console.error("保存答案时出错:", error);
  171. }
  172. }
  173. // 提交功能
  174. async submit() {
  175. try {
  176. if (!this.questionnaire) {
  177. console.error("未加载问卷数据。");
  178. return;
  179. }
  180. // 创建一个数组保存选中的 OptionId
  181. const answersArray: string[] = [];
  182. // 遍历每个问题,获取用户选择的选项
  183. for (const question of this.questionsWithOptions) {
  184. const selectedOptionId = this.answers[question.QuestionId];
  185. if (selectedOptionId) {
  186. // 将选中的 OptionId 存入 answersArray
  187. answersArray.push(selectedOptionId);
  188. }
  189. }
  190. // 创建一个新的 QuestionnaireResult 对象
  191. const questionnaireResult = new CloudObject("QuestionnaireResult");
  192. // 设置 QuestionnaireResult 的属性
  193. questionnaireResult.set({
  194. QuestionnaireResultId: `qr_${new Date().getTime()}`, // 生成唯一的 QuestionnaireResultId
  195. userId: { __type: "Pointer", className: "_User", objectId: "user1" }, // 替换为实际的用户ID
  196. // 使用 Pointer 类型的引用方式来设置 questionnaireId
  197. questionnaireId: { __type: "Pointer", className: "Questionnaire", objectId: this.questionnaire.objectId },
  198. answers: answersArray // 将选中的 OptionId 数组存入 answers 字段
  199. });
  200. // 保存 QuestionnaireResult 对象
  201. await questionnaireResult.save();
  202. console.log("问卷提交成功。");
  203. // 构建用于 AI 模型分析的提示词
  204. const aiPrompt = this.createAiPrompt(answersArray);
  205. // 调用 AI 模型分析,强制等待结果
  206. const aiResponse = await this.callAiModel(aiPrompt);
  207. // 如果 AI 响应有效,则执行以下逻辑
  208. if (aiResponse) {
  209. this.aiAnalysisResult = aiResponse; // 保存 AI 响应结果
  210. this.showAnalysisResult(); // 显示结果给用户
  211. await this.saveAnalysisResult(aiResponse); // 保存到数据库
  212. }
  213. } catch (error) {
  214. console.error("提交问卷时出错:", error);
  215. }
  216. }
  217. // 生成 AI 模型的提示词
  218. createAiPrompt(answersArray: string[]): string {
  219. const questionTexts = this.questionsWithOptions.map(q => q.questionText);
  220. const optionTexts = answersArray.map(optionId => {
  221. // 找到对应的 Option,并提取其 optionText
  222. const option = this.questionsWithOptions
  223. .flatMap(q => q.optionsData) // 从每个问题的 optionsData 获取 Option 对象
  224. .find(o => o.OptionId === optionId);
  225. return option ? option.optionText : ''; // 返回选项文本
  226. });
  227. return `
  228. 您是一名专业的兴趣分析师,请根据用户填写的问卷内容以及选项分析用户的兴趣并且生成以下格式的响应:
  229. {
  230. "interestTags": ["标签1", "标签2", "标签3", "标签4"], // 生成用户最感兴趣的四个标签(如:书法、绘画、摄影等)
  231. "content": "标签描述" // 针对每个标签生成简洁、生动的描述,帮助用户更清楚了解兴趣特点。描述可以包括用户行为、倾向和相关建议。
  232. }
  233. 请根据以下信息进行分析:
  234. 问题:${questionTexts.join(',')}
  235. 选项:${optionTexts.join(',')}
  236. 注意:
  237. - 仅选择用户**最感兴趣**的四个标签。
  238. - 生成的描述需要具体、生动,反映用户的兴趣深度或行为倾向。
  239. - 请忽略与用户兴趣无关的内容。
  240. - 标签和描述应通俗易懂,适合用户直接阅读。
  241. `;
  242. }
  243. async callAiModel(prompt: string): Promise<{ interestTags: string[], content: string }> {
  244. try {
  245. const completion = new FmodeChatCompletion([
  246. { role: "system", content: "您是一个专业的兴趣分析助手。" },
  247. { role: "user", content: prompt }
  248. ]);
  249. let fullContent = '';
  250. let count = 0;
  251. return new Promise((resolve, reject) => {
  252. completion.sendCompletion().subscribe({
  253. next: (message: any) => {
  254. if (message.content) {
  255. try {
  256. console.log('Received content:', message.content);
  257. fullContent = message.content;
  258. // 判断消息是否完成
  259. if (message?.complete) {
  260. this.isComplete = true;
  261. }
  262. // 如果消息完成且内容符合 JSON 格式,则解析
  263. if (this.isComplete) {
  264. const cleanedContent = fullContent.trim();
  265. // 检查是否是有效的 JSON 格式
  266. if (cleanedContent.startsWith('{') && cleanedContent.endsWith('}')) {
  267. try {
  268. // 清理掉换行符和多余空格
  269. let finalContent = cleanedContent.replace(/[\r\n]+/g, ''); // 去掉换行符
  270. finalContent = finalContent.replace(/\s+/g, ' '); // 去掉多余的空格
  271. console.log(finalContent);
  272. // 解析 JSON
  273. const parsedResponse = JSON.parse(finalContent);
  274. // 如果解析成功并且格式正确
  275. if (parsedResponse && parsedResponse.interestTags && parsedResponse.content) {
  276. const { interestTags, content } = parsedResponse;
  277. // 如果 content 是对象类型,转化成字符串
  278. let contentStr = '';
  279. if (typeof content === 'string') {
  280. contentStr = content; // 如果已经是字符串,直接使用
  281. } else if (typeof content === 'object') {
  282. // 如果是对象,转换为 JSON 字符串
  283. contentStr = JSON.stringify(content, null, 2); // 美化 JSON 字符串格式
  284. count = 0;
  285. this.isComplete = false;
  286. }
  287. resolve({
  288. interestTags: Array.isArray(interestTags) ? interestTags : [],
  289. content: contentStr
  290. });
  291. } else {
  292. reject(new Error("AI 返回的内容格式不正确"));
  293. }
  294. } catch (err) {
  295. console.log(fullContent);
  296. console.error("解析 AI 响应失败:", err);
  297. reject(new Error("解析 AI 响应失败"));
  298. }
  299. } else {
  300. reject(new Error("返回的内容不是有效的 JSON 格式"));
  301. }
  302. }
  303. } catch (err) {
  304. console.error("处理消息时出错:", err);
  305. reject(new Error("处理消息时出错"));
  306. }
  307. } else {
  308. if (count !== 0) {
  309. console.error("AI 返回的消息为空");
  310. reject(new Error("AI 返回的消息为空"));
  311. }
  312. count = 1;
  313. }
  314. },
  315. error: (err) => {
  316. console.error("AI 模型调用失败:", err);
  317. reject(new Error("AI 模型调用失败"));
  318. },
  319. complete: () => {
  320. // 可以在这里处理完成后的操作
  321. console.log("AI 请求完成");
  322. }
  323. });
  324. });
  325. } catch (error) {
  326. console.error("AI 模型调用失败:", error);
  327. throw new Error("AI 模型调用失败");
  328. }
  329. }
  330. /*
  331. // 显示分析结果
  332. showAnalysisResult() {
  333. if (this.aiAnalysisResult) {
  334. this.showAlert(this.aiAnalysisResult.content); // 弹窗显示 AI 分析的内容
  335. }
  336. }
  337. async showAlert(content: string) {
  338. //const formattedContent = this.formatContent(content); // 格式化内容
  339. const alert = await this.alertController.create({
  340. header: '兴趣分析结果',
  341. message: `${content}`, // 使用 pre 标签来保持格式
  342. buttons: ['确定']
  343. });
  344. await alert.present();
  345. }
  346. // 格式化 content 为可读的文本格式
  347. formatContent(content: string): string {
  348. try {
  349. // 尝试将 content 解析为 JSON 对象
  350. const contentObj = JSON.parse(content);
  351. // 构建格式化后的文本
  352. let formattedContent = '';
  353. // 遍历 JSON 对象,生成类似 "标签: 描述" 的格式
  354. for (const [tag, description] of Object.entries(contentObj)) {
  355. formattedContent += `${tag}: \n${description}\n\n`;
  356. }
  357. // 将换行符转换为 <br/>
  358. return formattedContent.replace(/\n/g, '<br/>');
  359. } catch (e) {
  360. console.error('格式化 AI 响应时出错:', e);
  361. return '分析结果格式错误。';
  362. }
  363. }
  364. *//*
  365. // 格式化 AI 响应内容
  366. formatContent(content: string): string {
  367. try {
  368. const contentObj = JSON.parse(content);
  369. return Object.entries(contentObj)
  370. .map(
  371. ([key, value]) =>
  372. `<strong>${key}:</strong><br>${value}<br><br>`
  373. )
  374. .join('');
  375. } catch (error) {
  376. console.error('格式化 AI 响应时出错:', error);
  377. return '分析结果格式错误。';
  378. }
  379. }
  380. */
  381. // 格式化 AI 响应内容
  382. formatContent(content: string): string {
  383. try {
  384. const contentObj = JSON.parse(content);
  385. console.log(contentObj)
  386. // 提取 interestTags 数组并格式化为一行展示
  387. const interestTags = contentObj.interestTags || [];
  388. const interestTagsFormatted = Array.isArray(interestTags)
  389. ? `${interestTags.join(',')}` // 标签用逗号分隔
  390. : '';
  391. // 提取 content 对象内容并格式化
  392. const contentDetails = contentObj.content || {};
  393. const contentFormatted = contentDetails
  394. .replace(/\"/g, '') // 移除转义字符如 \"
  395. .replace(/\n/g, '') // 移除换行符 \n
  396. .replace(/,/g, '') // 移除,
  397. .replace(/{/g, '') // 移除{
  398. .replace(/}/g, '') // 移除}
  399. .replace(/。/g, '。<br /><br />'); // 在每个句号 "。" 后插入换行 <br />
  400. // 冒号前加粗,描述部分保持普通
  401. // 拼接“兴趣描述”标题和换行
  402. const fullContent = `<strong class="fontsize">兴趣描述</strong>:<br />${contentFormatted}`;
  403. // 拼接最终输出
  404. return `
  405. <strong class="fontsize">兴趣标签:</strong><br> ${interestTagsFormatted}<br><br>
  406. ${fullContent}
  407. `;
  408. } catch (error) {
  409. console.error('格式化 AI 响应时出错:', error);
  410. return '分析结果格式错误。';
  411. }
  412. }
  413. // 显示分析结果
  414. showAnalysisResult() {
  415. if (this.aiAnalysisResult) {
  416. this.modalContent = this.formatContent(
  417. JSON.stringify({
  418. interestTags: this.aiAnalysisResult.interestTags,
  419. content: this.aiAnalysisResult.content
  420. })
  421. );
  422. this.modalIsOpen = true; // 打开 Modal
  423. }
  424. }
  425. // 关闭 Modal
  426. closeModal() {
  427. this.modalIsOpen = false; // 关闭 Modal
  428. }
  429. // 保存 AI 分析结果到数据库
  430. async saveAnalysisResult(aiResponse: { interestTags: string[], content: string }) {
  431. try {
  432. const userInterestProfile = new CloudObject("UserInterestProfile");
  433. userInterestProfile.set({
  434. userId: { __type: "Pointer", className: "_User", objectId: "user1" }, // 假设这是当前用户ID
  435. QuestionnaireId: this.questionnaire?.QuestionnaireId,
  436. interestTags: aiResponse.interestTags,
  437. content: aiResponse.content
  438. });
  439. await userInterestProfile.save();
  440. console.log("分析结果已保存");
  441. } catch (error) {
  442. console.error('保存分析结果时出错:', error);
  443. }
  444. }
  445. }