fmode-ng 0.0.243 → 0.0.245

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1131,8 +1131,9 @@ class FmodeObject {
1131
1131
  if (typeof this.updatedAt == "string") {
1132
1132
  this.updatedAt = new Date(this.updatedAt);
1133
1133
  }
1134
- console.log(this.updatedAt);
1135
- json.updatedAt = { __type: 'Date', iso: this.updatedAt.toISOString() };
1134
+ if (this.updatedAt != "Invalid Date") {
1135
+ json.updatedAt = { __type: 'Date', iso: this.updatedAt.toISOString() };
1136
+ }
1136
1137
  }
1137
1138
  }
1138
1139
  catch (err) { }
@@ -2019,7 +2020,8 @@ class FmodeCloud {
2019
2020
  }
2020
2021
  async function(params = {}, options = {}) {
2021
2022
  const config = this.currentConfig;
2022
- const url = `${config.serverURL}/../api/functions`;
2023
+ let u = new URL(config.serverURL);
2024
+ let url = u.origin + "/api/functions";
2023
2025
  const requestBody = {
2024
2026
  ...params,
2025
2027
  _ApplicationId: config.appId,
@@ -2047,7 +2049,20 @@ class FmodeCloud {
2047
2049
  if (!response.ok) {
2048
2050
  throw new Error(`HTTP error! status: ${response.status}`);
2049
2051
  }
2050
- const data = await response.json();
2052
+ // const data = await response.json();
2053
+ // 【关键】直接从 body 获取 reader,只读取一次
2054
+ const reader = response.body?.getReader();
2055
+ if (!reader) {
2056
+ throw new Error('No body reader available');
2057
+ }
2058
+ // 只读取一次!
2059
+ const { value, done } = await reader.read();
2060
+ if (!value) {
2061
+ throw new Error('Empty response body');
2062
+ }
2063
+ const decoder = new TextDecoder();
2064
+ const text = decoder.decode(value);
2065
+ const data = JSON.parse(text);
2051
2066
  if (data.error) {
2052
2067
  throw new Error(data.error.message || 'Cloud function error');
2053
2068
  }
@@ -4117,10 +4132,14 @@ function chunkToJson(chunk) {
4117
4132
  function RequestFmodeChatApi(apipath, body, method = "POST") {
4118
4133
  return new Observable((observer) => {
4119
4134
  let url = API_BASE + apipath;
4120
- let API_TOKEN = localStorage.getItem("FMODE_AI_TOKEN") || Parse$I.User.current()?.getSessionToken();
4135
+ let API_TOKEN = localStorage.getItem("FMODE_AI_TOKEN") || Parse$I.User.current()?.getSessionToken() || localStorage.getItem("COMPANY_PUBLIC_TOKEN");
4121
4136
  // 通过body传递token参数,避免no-cors模式下Authoriztion头部无效
4122
4137
  let AUTH_TOKEN = `Bearer ${API_TOKEN}`;
4123
4138
  body.token = AUTH_TOKEN;
4139
+ let company = localStorage.getItem("company");
4140
+ if (company) {
4141
+ body.company = body.company || company;
4142
+ }
4124
4143
  if (body)
4125
4144
  body = JSON.stringify(body);
4126
4145
  console.log(url, body);
@@ -4778,8 +4797,17 @@ class MessageManager {
4778
4797
  constructor(options) {
4779
4798
  this._messageList = []; // 统一的消息列表
4780
4799
  this._isLoaded = false;
4800
+ options.limitHistory = options?.limitHistory || 10;
4781
4801
  this.options = options;
4782
4802
  }
4803
+ getCompanyPointer() {
4804
+ let cid = this.options?.company?.objectId || this.options?.company?.id || localStorage.getItem("company");
4805
+ return {
4806
+ __type: "Pointer",
4807
+ className: "Company",
4808
+ objectId: cid
4809
+ };
4810
+ }
4783
4811
  /**
4784
4812
  * 获取统一的消息列表
4785
4813
  */
@@ -4824,43 +4852,34 @@ class MessageManager {
4824
4852
  messageObj.set("status", "sent");
4825
4853
  messageObj.set("createdAt", message.createdAt || new Date());
4826
4854
  messageObj.set("complete", message.complete || false);
4827
- // 设置内容
4828
- if (message.content) {
4829
- if (typeof message.content === "string") {
4830
- messageObj.set("content", {
4831
- type: "text",
4832
- text: message.content
4833
- });
4834
- }
4835
- else if (Array.isArray(message.content)) {
4836
- messageObj.set("content", {
4837
- type: "mixed",
4838
- items: message.content
4839
- });
4840
- }
4841
- else {
4842
- messageObj.set("content", message.content);
4843
- }
4844
- }
4845
- // 设置角色
4855
+ // 设置消息角色(user/assistant/system)- 这是消息的发送者角色
4846
4856
  if (message.role) {
4847
- messageObj.set("role", message.role);
4848
- }
4849
- // 设置其他属性
4850
- if (message.json)
4851
- messageObj.set("json", message.json);
4852
- if (message.hidden)
4853
- messageObj.set("hidden", message.hidden);
4854
- if (message.voice)
4855
- messageObj.set("voice", message.voice);
4856
- if (message.cid)
4857
- messageObj.set("cid", message.cid);
4857
+ messageObj.set("role", this.options.role.toPointer());
4858
+ }
4859
+ // 设置内容 - 保持原始格式兼容(string 或 Array)
4860
+ // 同时扩展存储格式,支持更多元数据
4861
+ if (message.content !== undefined) {
4862
+ messageObj.set("content", message);
4863
+ }
4858
4864
  // 根据场景设置关联对象
4865
+ // avatarRole:指向 AvatarRole 表的指针(智能体角色)
4859
4866
  if (targetScene === MessageScene.ROLE && this.options.role) {
4860
- messageObj.set("role", this.options.role.toPointer());
4867
+ messageObj.set("avatarRole", this.options.role.toPointer());
4861
4868
  }
4869
+ // company:指向 Company 表的指针
4862
4870
  if (this.options.company) {
4863
- messageObj.set("company", this.options.company.toPointer());
4871
+ messageObj.set("company", this.getCompanyPointer());
4872
+ }
4873
+ // 关键:如果chatSession已存在且有id,立即关联
4874
+ // 如果chatSession还没有id(新会话),标记为待关联
4875
+ if (this.options.chatSession) {
4876
+ if (this.options.chatSession.id) {
4877
+ messageObj.set("session", this.options.chatSession.toPointer());
4878
+ }
4879
+ else {
4880
+ // 新会话,标记待关联,等待session保存后再关联
4881
+ messageObj.set("_pendingSession", true);
4882
+ }
4864
4883
  }
4865
4884
  // 直接添加到列表末尾
4866
4885
  this._messageList.push(messageObj);
@@ -4873,50 +4892,90 @@ class MessageManager {
4873
4892
  }
4874
4893
  /**
4875
4894
  * 异步保存消息对象
4895
+ * 增强版:处理session关联和延迟保存
4876
4896
  */
4877
4897
  async saveMessage(messageObj) {
4878
- if (!messageObj.id) {
4879
- try {
4880
- await messageObj.save();
4898
+ if (!messageObj)
4899
+ return;
4900
+ try {
4901
+ // 检查是否需要关联session(对于新会话的消息)
4902
+ if (!messageObj.get("session") && this.options.chatSession?.id) {
4903
+ messageObj.set("session", this.options.chatSession.toPointer());
4904
+ }
4905
+ // 确保消息有关联的session才保存到Message表
4906
+ // 如果没有session且不是老消息转换的,先不保存(只存在于内存)
4907
+ if (!messageObj.get("session") && !messageObj.get("isLegacy")) {
4908
+ // 没有session的消息暂时不保存到Message表
4909
+ // 等session创建后会再次调用saveMessage
4910
+ console.log("消息等待session创建后保存");
4911
+ return;
4881
4912
  }
4882
- catch (error) {
4883
- console.error("保存消息对象失败:", error);
4913
+ await messageObj.save();
4914
+ }
4915
+ catch (error) {
4916
+ console.error("保存消息对象失败:", error);
4917
+ }
4918
+ }
4919
+ /**
4920
+ * 在session创建/更新后,保存所有待关联的消息
4921
+ * 关键方法:在chatSession保存成功后调用
4922
+ */
4923
+ async savePendingMessages() {
4924
+ if (!this.options.chatSession?.id) {
4925
+ console.warn("session未保存,无法保存待处理消息");
4926
+ return;
4927
+ }
4928
+ const savePromises = [];
4929
+ for (const messageObj of this._messageList) {
4930
+ // 处理待关联的消息
4931
+ // if (messageObj.get("_pendingSession")) {
4932
+ messageObj.set("session", messageObj?.get("session") || this.options.chatSession.toPointer());
4933
+ // messageObj.set("_pendingSession", undefined);
4934
+ // }
4935
+ // 保存所有没有id的消息(新消息)
4936
+ if (!messageObj.id) {
4937
+ if (!messageObj?.get("hidden")) {
4938
+ savePromises.push(this.saveMessage(messageObj));
4939
+ }
4884
4940
  }
4885
4941
  }
4942
+ if (savePromises.length > 0) {
4943
+ await Promise.all(savePromises);
4944
+ console.log(`保存了 ${savePromises.length} 条待处理消息`);
4945
+ }
4946
+ }
4947
+ /**
4948
+ * 更新chatSession引用(当新会话第一次保存后)
4949
+ */
4950
+ updateChatSession(chatSession) {
4951
+ this.options.chatSession = chatSession;
4886
4952
  }
4887
4953
  /**
4888
4954
  * 更新消息(优化版)
4889
- * 直接通过下标更新消息对象属性
4955
+ * 直接通过下标更新消息对象属性,返回是否成功
4890
4956
  */
4891
4957
  updateMessage(index, message) {
4892
4958
  if (index < 0 || index >= this._messageList.length)
4893
- return;
4959
+ return null;
4894
4960
  const messageObj = this._messageList[index];
4895
4961
  if (!messageObj)
4896
- return;
4897
- // 更新内容
4962
+ return null;
4963
+ // 更新消息角色
4964
+ if (message.role) {
4965
+ messageObj.set("role", this.options.role.toPointer());
4966
+ }
4967
+ // 更新内容 - 保持原始格式兼容
4968
+ // 从现有 content 中获取完整的 FmodeChatMessage 对象,然后更新字段
4898
4969
  if (message.content !== undefined) {
4899
- if (typeof message.content === "string") {
4900
- messageObj.set("content", {
4901
- type: "text",
4902
- text: message.content
4903
- });
4904
- }
4905
- else if (Array.isArray(message.content)) {
4906
- messageObj.set("content", {
4907
- type: "mixed",
4908
- items: message.content
4909
- });
4910
- }
4911
- else {
4912
- messageObj.set("content", message.content);
4913
- }
4970
+ const existingContent = messageObj.get("content") || {};
4971
+ messageObj.set("content", {
4972
+ ...existingContent,
4973
+ content: message.content
4974
+ });
4914
4975
  }
4915
4976
  // 更新其他属性
4916
4977
  if (message.complete !== undefined)
4917
4978
  messageObj.set("complete", message.complete);
4918
- if (message.role)
4919
- messageObj.set("role", message.role);
4920
4979
  if (message.json)
4921
4980
  messageObj.set("json", message.json);
4922
4981
  if (message.hidden)
@@ -4925,6 +4984,16 @@ class MessageManager {
4925
4984
  messageObj.set("voice", message.voice);
4926
4985
  if (message.cid)
4927
4986
  messageObj.set("cid", message.cid);
4987
+ return messageObj;
4988
+ }
4989
+ /**
4990
+ * 更新消息并异步保存
4991
+ */
4992
+ async updateMessageAndSave(index, message) {
4993
+ const messageObj = this.updateMessage(index, message);
4994
+ if (!messageObj?.get("hidden")) {
4995
+ await this.saveMessage(messageObj);
4996
+ }
4928
4997
  }
4929
4998
  /**
4930
4999
  * 重新加载消息
@@ -4947,20 +5016,35 @@ class MessageManager {
4947
5016
  const MessageClass = Parse$G.Object.extend("Message");
4948
5017
  const messageObj = new MessageClass();
4949
5018
  // 复制老版消息属性
4950
- messageObj.set("role", legacyMsg.role);
5019
+ messageObj.set("role", this.options.role.toPointer());
4951
5020
  messageObj.set("content", legacyMsg.content);
4952
5021
  messageObj.set("complete", legacyMsg.complete);
4953
5022
  messageObj.set("createdAt", legacyMsg.createdAt);
4954
5023
  messageObj.set("voice", legacyMsg.voice);
4955
5024
  messageObj.set("cid", legacyMsg.cid);
4956
5025
  messageObj.set("isLegacy", true);
5026
+ // 为老消息同步创建 contentData,保持格式统一
5027
+ if (typeof legacyMsg.content === "string") {
5028
+ messageObj.set("contentData", {
5029
+ type: "text",
5030
+ text: legacyMsg.content,
5031
+ role: legacyMsg.role
5032
+ });
5033
+ }
5034
+ else if (Array.isArray(legacyMsg.content)) {
5035
+ messageObj.set("contentData", {
5036
+ type: "mixed",
5037
+ items: legacyMsg.content,
5038
+ role: legacyMsg.role
5039
+ });
5040
+ }
4957
5041
  this._messageList.push(messageObj);
4958
5042
  }
4959
5043
  // 2. 加载新版消息
4960
5044
  let newMessages = [];
4961
5045
  switch (this.options.scene) {
4962
5046
  case MessageScene.ROLE:
4963
- if (this.options.chatSession) {
5047
+ if (this.options.role) {
4964
5048
  newMessages = await this.loadRoleMessages();
4965
5049
  }
4966
5050
  break;
@@ -4993,10 +5077,11 @@ class MessageManager {
4993
5077
  * 加载角色对话消息
4994
5078
  */
4995
5079
  async loadRoleMessages() {
4996
- if (!this.options.chatSession)
5080
+ if (!this.options.role?.id)
4997
5081
  return [];
4998
5082
  const query = new Parse$G.Query("Message");
4999
- query.equalTo("session", this.options.chatSession);
5083
+ query.exists("content");
5084
+ query.equalTo("role", this.options.role?.id);
5000
5085
  query.addAscending("createdAt");
5001
5086
  query.limit(1000); // 限制数量避免性能问题
5002
5087
  return await query.find();
@@ -5008,6 +5093,7 @@ class MessageManager {
5008
5093
  if (!this.options.group)
5009
5094
  return [];
5010
5095
  const query = new Parse$G.Query("Message");
5096
+ query.exists("content");
5011
5097
  query.equalTo("group", this.options.group);
5012
5098
  query.addAscending("createdAt");
5013
5099
  query.limit(1000);
@@ -5020,6 +5106,7 @@ class MessageManager {
5020
5106
  if (!this.options.userFrom || !this.options.userTo)
5021
5107
  return [];
5022
5108
  const query = new Parse$G.Query("Message");
5109
+ query.exists("content");
5023
5110
  query.containedIn("userFrom", [this.options.userFrom, this.options.userTo]);
5024
5111
  query.containedIn("userTo", [this.options.userFrom, this.options.userTo]);
5025
5112
  query.addAscending("createdAt");
@@ -5033,6 +5120,7 @@ class MessageManager {
5033
5120
  if (!this.options.contact)
5034
5121
  return [];
5035
5122
  const query = new Parse$G.Query("Message");
5123
+ query.exists("content");
5036
5124
  query.equalTo("contact", this.options.contact);
5037
5125
  query.addAscending("createdAt");
5038
5126
  query.limit(1000);
@@ -5102,12 +5190,12 @@ function getMessageContentText(content) {
5102
5190
  if (typeof content == "string")
5103
5191
  text = content;
5104
5192
  if (typeof content == "object")
5105
- text = content?.content?.text || "";
5193
+ text = content?.content || "";
5106
5194
  return text;
5107
5195
  }
5108
5196
  function getMessageImageUrl(content) {
5109
5197
  if (typeof content == "object")
5110
- return content?.content?.image_url?.url || "";
5198
+ return content?.image_url?.url || "";
5111
5199
  return null;
5112
5200
  }
5113
5201
  /**
@@ -5268,20 +5356,17 @@ class FmodeChat {
5268
5356
  this.navCtrl = navCtrl;
5269
5357
  this.ncloud = ncloud;
5270
5358
  this.storage = storage;
5271
- // 初始化企业信息
5359
+ // 初始化企业信息(异步,不阻塞)
5272
5360
  this.initCompany();
5273
5361
  if (chatSession?.id) {
5274
5362
  this.chatSession = chatSession;
5275
5363
  this.sessionId = chatSession?.id;
5276
- // 初始化消息管理器
5277
- this.initMessageManager();
5278
- }
5279
- else {
5280
- // 即使没有chatSession,也尝试初始化消息管理器(用于新会话)
5281
- setTimeout(() => {
5282
- this.initMessageManager();
5283
- }, 100);
5284
5364
  }
5365
+ // 延迟初始化消息管理器(懒加载模式)
5366
+ // 确保company先初始化完成
5367
+ setTimeout(() => {
5368
+ this.ensureMessageManager();
5369
+ }, 0);
5285
5370
  if (this.role?.id) {
5286
5371
  this.voiceConfig = this.role?.get("voiceConfig");
5287
5372
  if (this.voiceConfig?.autoTalk) {
@@ -5352,6 +5437,8 @@ class FmodeChat {
5352
5437
  * 获取消息列表(兼容老版本)
5353
5438
  */
5354
5439
  async getMessageList() {
5440
+ // 确保 messageManager 已初始化
5441
+ await this.ensureMessageManager();
5355
5442
  if (!this.messageManager) {
5356
5443
  return [];
5357
5444
  }
@@ -5386,17 +5473,33 @@ class FmodeChat {
5386
5473
  }
5387
5474
  /**
5388
5475
  * 初始化消息管理器
5476
+ * 关键:即使没有chatSession.id也能初始化(新会话场景)
5389
5477
  */
5390
- initMessageManager() {
5391
- if (this.chatSession && this.role && this.company) {
5478
+ async initMessageManager() {
5479
+ // 确保 company 已初始化
5480
+ if (!this.company) {
5481
+ await this.initCompany();
5482
+ }
5483
+ if (this.role && this.company) {
5392
5484
  // 使用智能体对话场景的消息管理器
5393
- this.messageManager = MessageManager.createForRole(this.chatSession, this.role, this.company);
5485
+ // chatSession可能暂时没有id(新会话),但manager仍然可以工作
5486
+ this.messageManager = MessageManager.createForRole(this.chatSession || new Parse$F.Object("ChatSession"), this.role, this.company);
5487
+ }
5488
+ }
5489
+ /**
5490
+ * 确保消息管理器已初始化(懒加载)
5491
+ */
5492
+ async ensureMessageManager() {
5493
+ if (!this.messageManager) {
5494
+ await this.initMessageManager();
5394
5495
  }
5395
5496
  }
5396
5497
  /**
5397
5498
  * 初始化企业信息
5398
5499
  */
5399
5500
  async initCompany() {
5501
+ if (this.company)
5502
+ return; // 已经初始化
5400
5503
  const companyId = localStorage.getItem("company");
5401
5504
  if (companyId) {
5402
5505
  this.company = {
@@ -5411,8 +5514,10 @@ class FmodeChat {
5411
5514
  * 直接添加到MessageManager的_messageList,延迟保存
5412
5515
  */
5413
5516
  async addMessage(content, scene = MessageScene.ROLE) {
5517
+ // 确保 messageManager 已初始化
5518
+ await this.ensureMessageManager();
5414
5519
  if (!this.messageManager) {
5415
- console.warn("MessageManager未初始化,无法添加消息");
5520
+ console.warn("MessageManager初始化失败,无法添加消息");
5416
5521
  return;
5417
5522
  }
5418
5523
  try {
@@ -5420,8 +5525,9 @@ class FmodeChat {
5420
5525
  const messageObj = this.messageManager.addMessage(content, scene);
5421
5526
  // 异步保存,不阻塞主流程
5422
5527
  setTimeout(() => {
5423
- this.messageManager.saveMessage(messageObj);
5528
+ this.messageManager?.saveMessage(messageObj);
5424
5529
  }, 0);
5530
+ return messageObj;
5425
5531
  }
5426
5532
  catch (error) {
5427
5533
  console.error("添加消息失败:", error);
@@ -5431,16 +5537,18 @@ class FmodeChat {
5431
5537
  * 更新消息(优化版)
5432
5538
  * 直接通过下标更新MessageManager中的消息
5433
5539
  */
5434
- updateMessage(index, content) {
5540
+ updateMessage(messageObj, content) {
5435
5541
  if (!this.messageManager) {
5436
5542
  console.warn("MessageManager未初始化,无法更新消息");
5437
5543
  return;
5438
5544
  }
5439
5545
  try {
5440
5546
  // 直接更新MessageManager中的消息
5441
- this.messageManager.updateMessage(index, content);
5442
- // 异步保存,不阻塞主流程
5443
- const messageObj = this.messageManager.getMessage(index);
5547
+ content = {
5548
+ ...messageObj.get("content"),
5549
+ ...content
5550
+ };
5551
+ messageObj.set("content", content);
5444
5552
  if (messageObj) {
5445
5553
  setTimeout(() => {
5446
5554
  this.messageManager.saveMessage(messageObj);
@@ -5555,37 +5663,25 @@ class FmodeChat {
5555
5663
  // 提示词已经存在
5556
5664
  return;
5557
5665
  }
5558
- // 补全提示词
5559
- let systemIndex = this.messageList?.findIndex(item => this.getMessageProperty(item, 'role') == "system");
5560
- let insertIndex = systemIndex + 1;
5561
- let msgObj = new Parse$F.Object("Message");
5562
- msgObj.set("content", promptMsg);
5563
- this.messageList.splice(insertIndex, 0, msgObj);
5666
+ // 补全提示词 - 使用 addMessage 确保正确保存
5667
+ this.addMessage(promptMsg, MessageScene.ROLE);
5564
5668
  return;
5565
5669
  }
5566
5670
  /**
5567
5671
  * 角色提示词
5568
5672
  * @returns
5569
5673
  */
5570
- loadRolePrompt() {
5674
+ async loadRolePrompt() {
5675
+ // 确保 messageManager 已初始化
5676
+ await this.ensureMessageManager();
5571
5677
  // 角色提示
5572
5678
  let prompt = this.role?.get("prompt");
5573
- let promptMsg = { role: "user", content: prompt, hidden: true };
5574
5679
  if (!prompt)
5575
5680
  return; // 无提示词无需添加
5576
- // 内容检查
5577
- let content = this.messageList?.map(item => this.getMessageProperty(item, 'content')).join();
5578
- if (content.indexOf(prompt) > -1) {
5579
- // 提示词已经存在
5580
- return;
5581
- }
5582
- // 补全提示词
5583
- let systemIndex = this.messageList?.findIndex(item => this.getMessageProperty(item, 'role') == "system");
5584
- let insertIndex = systemIndex + 1;
5585
- let msgObj = new Parse$F.Object("Message");
5586
- msgObj.set("content", promptMsg);
5587
- this.messageList.splice(insertIndex, 0, msgObj);
5588
- // console.log(this.messageList)
5681
+ this.rolePrompt = prompt;
5682
+ // 使用 addMessage 添加提示词,确保正确保存
5683
+ // let promptMsg:FmodeChatMessage = {role:"user",content:prompt,hidden:true}
5684
+ // await this.addMessage(promptMsg, MessageScene.ROLE);
5589
5685
  }
5590
5686
  /**
5591
5687
  * 发送消息
@@ -5593,12 +5689,14 @@ class FmodeChat {
5593
5689
  * @param imageUrl
5594
5690
  */
5595
5691
  async sendMessage(message = "FmodeAiTest测试问题", imageUrl, onComplete, eventMap, voice) {
5692
+ // 确保 messageManager 已初始化
5693
+ await this.ensureMessageManager();
5596
5694
  // 发消息自动置底
5597
5695
  this.scrollToBottom && this.scrollToBottom();
5598
5696
  // 为消息列表补全提示词
5599
5697
  // await this.loadTalkSystemPrompt(this.role);
5600
5698
  this.isPromptMessageAreaShow = false; // 发送第一条消息后,关闭提示看板
5601
- this.loadRolePrompt();
5699
+ await this.loadRolePrompt();
5602
5700
  // 构建用户消息
5603
5701
  let userMessage;
5604
5702
  if (!imageUrl) { // 纯文本
@@ -5633,11 +5731,14 @@ class FmodeChat {
5633
5731
  }
5634
5732
  }
5635
5733
  // 添加用户消息
5636
- await this.addMessage(userMessage, MessageScene.ROLE);
5734
+ this.addMessage(userMessage, MessageScene.ROLE);
5637
5735
  // 获取最新消息列表用于补全
5638
5736
  const messages = await this.getMessageList();
5639
5737
  // 创建并发起一条新的消息补全
5640
- let completion = new FmodeChatCompletion(this.fixMessageList(this.convertMessages(messages)), {
5738
+ let completion = new FmodeChatCompletion([
5739
+ { role: "user", content: this.rolePrompt }, // 角色提示词 不在消息列表
5740
+ ...this.fixMessageList(this.convertMessages(messages)) // 列表形式,确保灵活的图片消息格式接收
5741
+ ], {
5641
5742
  model: this.currentModel?.get?.("code") || "fmode-1.6-cn"
5642
5743
  });
5643
5744
  // 生命周期:消息获取完成
@@ -5661,9 +5762,7 @@ class FmodeChat {
5661
5762
  createdAt: new Date()
5662
5763
  };
5663
5764
  // 添加AI回复消息占位符
5664
- await this.addMessage(aiMessagePlaceholder, MessageScene.ROLE);
5665
- const currentMessages = await this.getMessageList();
5666
- const aiMessageIndex = currentMessages.length - 1;
5765
+ let newAiMessage = await this.addMessage(aiMessagePlaceholder, MessageScene.ROLE);
5667
5766
  // 生命周期:用户发送消息回调
5668
5767
  if (this.onUserSend) {
5669
5768
  let sendResult = await this.onUserSend(this, userMessage);
@@ -5674,29 +5773,26 @@ class FmodeChat {
5674
5773
  isDirect: isDirect,
5675
5774
  onComplete: onComplete || null
5676
5775
  }).pipe(finalize(async () => {
5677
- let currentMessage = currentMessages[aiMessageIndex];
5678
5776
  // 标记消息完成
5679
- this.setMessageProperty(currentMessage, 'complete', true);
5680
- await this.updateMessage(aiMessageIndex, currentMessage);
5777
+ this.updateMessage(newAiMessage, { complete: true });
5681
5778
  // 在消息完成后生成语音(TalkMode)
5682
5779
  if (this.isTalkMode) {
5683
- let content = currentMessage?.get("content");
5780
+ let content = newAiMessage?.get("content");
5684
5781
  if (this.hasValidContent(content)) {
5685
5782
  try {
5686
5783
  let voice = await this.getVoiceByContentText(content, eventMap);
5687
5784
  if (voice && voice.id) {
5688
- currentMessage.set("voice", voice);
5689
- await this.updateMessage(aiMessageIndex, currentMessage);
5785
+ // 更新消息的voice属性
5786
+ this.updateMessage(newAiMessage, { voice: voice });
5690
5787
  // 通知SSML完成
5691
5788
  eventMap?.onSSMLComplete && eventMap?.onSSMLComplete(voice);
5692
5789
  // 播放语音
5693
5790
  this.playChatVoice(this.voiceMap[voice?.id], {
5694
5791
  onResult: (result) => {
5695
5792
  if (result?.duration) {
5696
- let voice = currentMessage.get("voice");
5697
- voice.duration = result?.duration;
5698
- currentMessage.set("voice", voice);
5699
- this.updateMessage(aiMessageIndex, currentMessage);
5793
+ // 更新语音时长
5794
+ const updatedVoice = { ...voice, duration: result?.duration };
5795
+ this.updateMessage(newAiMessage, { voice: updatedVoice });
5700
5796
  }
5701
5797
  }
5702
5798
  });
@@ -5708,27 +5804,24 @@ class FmodeChat {
5708
5804
  }
5709
5805
  }
5710
5806
  // 生命周期:消息获取完成
5711
- const finalMsgs = await this.getMessageList();
5712
- if (finalMsgs?.length > 0) {
5713
- this.onMessage && this.onMessage(this, finalMsgs[finalMsgs?.length - 1]?.get("content"));
5807
+ if (newAiMessage?.id) {
5808
+ this.onMessage && this.onMessage(this, newAiMessage?.get("content"));
5809
+ }
5810
+ // 保存AI消息到数据库(必须在session保存之后)
5811
+ if (newAiMessage) {
5812
+ await this.messageManager?.saveMessage(newAiMessage);
5714
5813
  }
5715
5814
  })).subscribe(async (message) => {
5716
- // 实时更新消息内容
5717
- await this.updateMessage(aiMessageIndex, message);
5815
+ let content = message.content;
5718
5816
  // 更新最新回复内容
5719
- this.latestAIResponse = this.getContentText(message?.content);
5817
+ this.latestAIResponse = this.getContentText(content);
5720
5818
  // 生命周期:消息更新回调(实时更新)
5721
5819
  if (this.onMessage && !message?.complete) {
5722
- const currentMsgs = await this.getMessageList();
5723
- if (currentMsgs?.length > 0) {
5724
- this.onMessage(this, currentMsgs[currentMsgs?.length - 1]?.get("content"));
5725
- }
5820
+ newAiMessage.set("content", message?.content);
5821
+ this.onMessage(this, message);
5726
5822
  }
5727
- // 保存聊天会话(实时保存)
5728
- const currentMsgs = await this.getMessageList();
5729
- let savedList = this.chatSession?.get("messageList")?.length;
5730
- if (currentMsgs?.length > savedList) {
5731
- await this.saveChatSession();
5823
+ if (message?.complete) {
5824
+ this.updateMessage(newAiMessage, content);
5732
5825
  }
5733
5826
  // 滚动到底部
5734
5827
  this.scrollToBottom && this.scrollToBottom();
@@ -5967,9 +6060,11 @@ class FmodeChat {
5967
6060
  }
5968
6061
  /**
5969
6062
  * 保存单次会话
6063
+ * 关键:在session保存后,立即更新消息管理器中的session引用并保存所有待处理消息
5970
6064
  */
5971
6065
  async saveChatSession() {
5972
- if (this.sessionId == "new") {
6066
+ const isNewSession = this.sessionId == "new";
6067
+ if (isNewSession) {
5973
6068
  this.chatSession = new this.ChatSession();
5974
6069
  }
5975
6070
  this.chatSession.set("title", this.genTitle());
@@ -5983,8 +6078,18 @@ class FmodeChat {
5983
6078
  this.chatSession.set("messageList", []);
5984
6079
  }
5985
6080
  this.chatSession = await this.chatSession.save();
5986
- this.onChatSaved && this.onChatSaved(this);
6081
+ // 关键:更新sessionId
6082
+ const oldSessionId = this.sessionId;
5987
6083
  this.sessionId = this.chatSession?.id;
6084
+ // 关键:更新MessageManager中的session引用
6085
+ if (this.messageManager) {
6086
+ this.messageManager.updateChatSession(this.chatSession);
6087
+ // 保存所有待处理的消息(新会话的消息或之前因缺少session而未保存的消息)
6088
+ // if (this.sessionId) {
6089
+ await this.messageManager.savePendingMessages();
6090
+ // }
6091
+ }
6092
+ this.onChatSaved && this.onChatSaved(this);
5988
6093
  if (this.sessionId) {
5989
6094
  // 修改URL地址为sessionId,方便分享或切换 角色页面 => 会话页面
5990
6095
  let newHref = `${window.location.origin}/chat/pro/chat/${this.sessionId}`;
@@ -6000,11 +6105,16 @@ class FmodeChat {
6000
6105
  const messages = await this.getMessageList();
6001
6106
  const lastMessage = messages[messages?.length - 1];
6002
6107
  let messagePreview = "";
6003
- if (typeof lastMessage.get("content") === "string") {
6004
- messagePreview = lastMessage.get("content").slice(0, 20);
6108
+ // 处理不同格式的content
6109
+ const content = lastMessage?.get("content");
6110
+ if (typeof content === "string") {
6111
+ messagePreview = content.slice(0, 20);
6005
6112
  }
6006
- else if (Array.isArray(lastMessage.get("content"))) {
6007
- const textItem = lastMessage.get("content").find(item => item?.text);
6113
+ else if (content?.text) {
6114
+ messagePreview = content.text.slice(0, 20);
6115
+ }
6116
+ else if (Array.isArray(content)) {
6117
+ const textItem = content.find(item => item?.text);
6008
6118
  messagePreview = textItem?.text?.slice(0, 20) || "";
6009
6119
  }
6010
6120
  let newChat = {
@@ -6016,13 +6126,18 @@ class FmodeChat {
6016
6126
  };
6017
6127
  if (this.chatServ && !this.chatServ?.chatList?.length)
6018
6128
  this.chatServ.chatList = [];
6019
- let index = this.chatServ?.chatList?.find(item => item?.sid == newChat?.sid);
6129
+ let index = this.chatServ?.chatList?.findIndex(item => item?.sid == newChat?.sid);
6020
6130
  if (index > -1) {
6021
6131
  this.chatServ.chatList[index] = newChat;
6022
6132
  }
6023
6133
  else {
6024
6134
  this.chatServ?.chatList.unshift(newChat);
6025
6135
  }
6136
+ // 更新chatServ中的chatMap(将new的key替换为真实sessionId)
6137
+ if (isNewSession && oldSessionId === "new") {
6138
+ this.chatServ.chatMap[this.sessionId] = this;
6139
+ delete this.chatServ.chatMap["new"];
6140
+ }
6026
6141
  }
6027
6142
  }
6028
6143
  getInviteUrl(url) {
@@ -6046,7 +6161,9 @@ class FmodeChat {
6046
6161
  return this.title;
6047
6162
  }
6048
6163
  fixMessageList(messages) {
6049
- return messages.map(msg => { return { role: msg.role, content: msg.content }; });
6164
+ let list = messages.filter((msg) => { return typeof msg.content.role == "string" && msg.content.content; }).map((msg) => { return { role: msg.content.role, content: msg.content.content }; });
6165
+ console.log("messages", messages, list);
6166
+ return list;
6050
6167
  }
6051
6168
  nowStr() {
6052
6169
  let now = new Date();
@@ -11484,14 +11601,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
11484
11601
 
11485
11602
  const Parse$w = defaultExport.with("nova");
11486
11603
  class ChatService {
11487
- constructor(router, ncloud, platform, alertCtrl, navCtrl, cross, storage) {
11604
+ constructor(router, ncloud, platform, alertCtrl, navCtrl, cross) {
11488
11605
  this.router = router;
11489
11606
  this.ncloud = ncloud;
11490
11607
  this.platform = platform;
11491
11608
  this.alertCtrl = alertCtrl;
11492
11609
  this.navCtrl = navCtrl;
11493
11610
  this.cross = cross;
11494
- this.storage = storage;
11495
11611
  // 已加载聊天信息临时存储
11496
11612
  this.chatMap = {};
11497
11613
  this.isCapacitor = false;
@@ -11500,6 +11616,10 @@ class ChatService {
11500
11616
  "mobile": "移动端",
11501
11617
  };
11502
11618
  this.isCapacitor = this.platform.is("capacitor");
11619
+ this.initStorage();
11620
+ }
11621
+ async initStorage() {
11622
+ this.storage = await NovaStorage.withCid(localStorage.getItem("company"));
11503
11623
  }
11504
11624
  async doButtonAction(button) {
11505
11625
  let type = this.cross.navMenuType;
@@ -11636,7 +11756,7 @@ class ChatService {
11636
11756
  type: "phone"
11637
11757
  }]);
11638
11758
  }
11639
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ChatService, deps: [{ token: i1$1.Router }, { token: NovaCloudService }, { token: i3.Platform }, { token: i3.AlertController }, { token: i3.NavController }, { token: CrossService }, { token: NovaStorage }], target: i0.ɵɵFactoryTarget.Injectable }); }
11759
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ChatService, deps: [{ token: i1$1.Router }, { token: NovaCloudService }, { token: i3.Platform }, { token: i3.AlertController }, { token: i3.NavController }, { token: CrossService }], target: i0.ɵɵFactoryTarget.Injectable }); }
11640
11760
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ChatService, providedIn: 'root' }); }
11641
11761
  }
11642
11762
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ChatService, decorators: [{
@@ -11644,7 +11764,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
11644
11764
  args: [{
11645
11765
  providedIn: 'root'
11646
11766
  }]
11647
- }], ctorParameters: () => [{ type: i1$1.Router }, { type: NovaCloudService }, { type: i3.Platform }, { type: i3.AlertController }, { type: i3.NavController }, { type: CrossService }, { type: NovaStorage }] });
11767
+ }], ctorParameters: () => [{ type: i1$1.Router }, { type: NovaCloudService }, { type: i3.Platform }, { type: i3.AlertController }, { type: i3.NavController }, { type: CrossService }] });
11648
11768
 
11649
11769
  const Parse$v = defaultExport.with("nova");
11650
11770
  /**
@@ -13340,7 +13460,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
13340
13460
  class ChatContentPipe {
13341
13461
  transform(content, ...args) {
13342
13462
  // console.log(args);
13343
- let arg = args?.[0] || "text";
13463
+ let arg = args?.[0] || content?.type || "text";
13344
13464
  if (arg == "text") {
13345
13465
  return getMessageContentText(content);
13346
13466
  }
@@ -16457,7 +16577,7 @@ class FmChatMessageCard {
16457
16577
  this.copyServ.copyToClipboard(getMessageContentText(this.message?.get('content')));
16458
16578
  }
16459
16579
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FmChatMessageCard, deps: [{ token: ClipboardService }], target: i0.ɵɵFactoryTarget.Component }); }
16460
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: FmChatMessageCard, isStandalone: true, selector: "fm-chat-message-card", inputs: { index: "index", message: "message", role: "role", chat: "chat" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"message-card\" [class.right]=\"message?.get('role')=='user'\" [class.center]=\"message?.get('role')=='system'\">\r\n <!-- \u7528\u6237\u53CA\u64CD\u4F5C\u533A -->\r\n <div class=\"item-row user\" *ngIf=\"message?.get('role')!='system'\"> <!-- \u7CFB\u7EDF\u6D88\u606F\u4E0D\u663E\u793A\u5934\u50CF -->\r\n <div class=\"avatar-row\">\r\n <div class=\"actions\">\r\n <!-- \u5237\u65B0 -->\r\n <!-- <ion-button fill=\"outline\" slot=\"start\">\r\n <ion-icon name=\"refresh-outline\"></ion-icon>\r\n </ion-button> -->\r\n <!-- \u590D\u5236 -->\r\n <ion-button size=\"small\" fill=\"outline\" slot=\"start\" (click)=\"copy()\">\r\n <ion-icon name=\"copy-outline\"></ion-icon>\r\n </ion-button>\r\n <!-- \u7F16\u8F91 -->\r\n <!-- <ion-button fill=\"outline\" slot=\"start\">\r\n <ion-icon name=\"create-outline\"></ion-icon>\r\n </ion-button> -->\r\n </div>\r\n <!-- \u66F4\u65B0\u97F3\u9891\u6D88\u606F\u533A\u57DF -->\r\n <div *ngIf=\"((message?.get('role')=='assistant' && chat?.role?.get('voiceConfig')?.voice) || (message?.get('role')=='user'&&message?.get('voice')))\"\r\n class=\"play-voice\"\r\n (click)=\"!isLoadingText && toggleVoicePlay()\"\r\n [class.loading-voice]=\"chat?.isTalkMode && isLoadingText\">\r\n\r\n <div class=\"voice-button\">\r\n <!-- \u52A0\u8F7D\u65F6\u663E\u793Aspinner\uFF0C\u5426\u5219\u663E\u793Awifi\u56FE\u6807 -->\r\n <ion-spinner *ngIf=\"chat?.isTalkMode && isLoadingText\" name=\"lines\" class=\"loading-spinner\"></ion-spinner>\r\n <ion-icon *ngIf=\"!(chat?.isTalkMode && isLoadingText)\" name=\"wifi-outline\"\r\n [style.transform]=\"message?.get('role')=='user'?'rotate(-90deg)':'rotate(90deg)'\"\r\n class=\"audio-icon\"\r\n [class.play-voice-playing]=\"tts?.isPlaying\"></ion-icon>\r\n </div>\r\n <div class=\"voice-info\">\r\n <span *ngIf=\"message?.get('voice')?.duration && !isLoadingText\">\r\n {{((message?.get('voice')?.duration||0)/1000) | durationStr}}\r\n </span>\r\n </div>\r\n </div>\r\n <!-- \u5934\u50CF\u533A\u57DF -->\r\n <img class=\"avatar\" *ngIf=\"message?.get('role')!='user'\" [src]=\"(chat?.role?.get('avatar') || chat?.role?.get('thumb') || 'https://file-cloud.fmode.cn/E4KpGvTEto/20230930/l413e6090731854.png')+'?'+'x-image-process=image/resize,m_fixed,w_100'+'&imageView2/1/w/100/h/100'\" >\r\n <app-comp-user-avatar [user]=\"user\" *ngIf=\"message?.get('role')=='user'\"></app-comp-user-avatar>\r\n </div>\r\n </div>\r\n <!-- \u9644\u4EF6\uFF1A\u56FE\u7247 -->\r\n <div class=\"item-row images\" *ngIf=\"message?.get('content') | chatContent:'image_url'\">\r\n <img [src]=\"message?.get('content') | chatContent:'image_url'\" alt=\"\">\r\n </div>\r\n <!-- \u804A\u5929\u6C14\u6CE1 -->\r\n <!-- Replace the bubble section with this: -->\r\n <div class=\"item-row bubble\" [style.fontSize]=\"role?.get('uiConfig')?.msg?.bubble?.fontSize || '0.8rem'\">\r\n <!-- \u8BF4\u8BDD\u6A21\u5F0F\uFF1A\u5C55\u793A\u52A0\u8F7D\u72B6\u6001 Show loading state for talk mode when message is not complete -->\r\n\r\n <!-- Show normal content for non-talk mode or when loading is complete -->\r\n <ng-container *ngIf=\"!chat?.isTalkMode || message?.get('role') !== 'assistant' || !isLoadingText\">\r\n <fm-markdown-preview *ngIf=\"!message?.get('complete')\" class=\"content-style\"\r\n [content]=\"message?.get('content') | chatContent\" [render]=\"false\"></fm-markdown-preview>\r\n <fm-markdown-preview *ngIf=\"message?.get('complete')\"\r\n [content]=\"message?.get('content') | chatContent\"></fm-markdown-preview>\r\n </ng-container>\r\n </div>\r\n <!-- \u65F6\u95F4\u663E\u793A -->\r\n <div class=\"item-row loading\" *ngIf=\"message?.get('role')!='system' && !message?.get('complete')\">\r\n \u6B63\u5728\u8F93\u5165<ion-spinner name=\"dots\"></ion-spinner>\r\n </div>\r\n\r\n <div class=\"item-row created\" *ngIf=\"message?.get('createdAt')\">\r\n <span>{{message?.get('createdAt') | date:\"dd/MM/yy HH:mm\"}}</span>\r\n </div>\r\n</div>", styles: ["@charset \"UTF-8\";:host-context(body.dark) .message-card .actions .item-native{background:none!important}:host-context(body.dark) .message-card .bubble{color:#0e101d}:host-context(body.dark) .message-card .bubble .content-style{filter:invert(1)!important}:host-context(body.dark) .message-card .bubble fm-markdown-preview{filter:invert(1)!important}:host-context(body.dark) .message-card .play-voice{background-color:#0e101d}:host-context(body.dark) .message-card .play-voice .voice-info{color:#fff}:host-context(body.dark) .message-card .right .bubble{color:#921f8a!important;background:#921f8a!important}:host-context(body.dark) .message-card .right .play-voice{background:#921f8a!important}:host-context(body.dark) .message-card .created span{color:#fff}@media screen and (max-width: 800px){.message-card:focus .actions{opacity:1!important}}.message-card:hover .actions{opacity:1;transition:opacity .3s ease-in-out}.message-card{display:flex;flex-wrap:wrap;justify-content:start;align-items:flex-start}.message-card .avatar-row{width:300px;height:32px;display:flex;flex-direction:row-reverse;justify-content:start;align-items:center}.message-card .actions{display:flex;opacity:0;padding-left:10px;padding-right:10px}.message-card .item-row{display:flex;flex:100%;justify-content:start;margin-bottom:5px}.message-card .images img{max-width:300px}.message-card .bubble.loading{color:var(--gray-secondary)}.message-card .bubble.loading .content-style{filter:none}.message-card .bubble{display:flex;justify-content:center;max-width:100%;padding:.5rem .5rem 0rem;color:#333;flex:none;border-radius:0 1.5em 1.5em/0em 1.5em 1.5em;color:#fff;background-color:currentColor}.message-card .bubble .content-style{filter:grayscale(1) contrast(999) invert(1)}.message-card .loading{text-align:right;color:#101010}.message-card .created{display:flex}.message-card .created span{font-size:12px;opacity:.4;white-space:nowrap;transition:all .6s ease;color:var(--black);text-align:center;width:100%;box-sizing:border-box;padding-right:10px;pointer-events:none;z-index:1}.right{justify-content:end;align-items:flex-end}.right .avatar-row{flex-direction:row;justify-content:end;width:auto}.right .actions{position:relative;margin-left:0}.right .item-row{justify-content:end}.right .bubble{color:#bbdefb;border-top-left-radius:1.5em;border-top-right-radius:0}.right .play-voice{flex-direction:row-reverse;background-color:#bbdefb}.center{justify-content:center;align-items:center}.center .item-row{justify-content:center}.center .bubble{color:var(--gray-secondary);border-top-left-radius:1.5em;border-top-right-radius:1.5em;font-size:12px;font-weight:100;padding:5px 20px}.play-voice{min-width:100px;height:32px;display:flex;justify-content:space-around;align-items:center;background-color:#fff;border-radius:7px}.play-voice .voice-button{width:32px;height:32px;display:flex;justify-content:center;align-items:center}.play-voice .voice-button span{overflow:hidden;font-size:18px;color:#ff69b4}.play-voice .voice-info{height:32px;display:flex;padding:0 10px;justify-content:end;align-items:center;color:#333}.avatar{border-radius:50%;width:32px;height:32px;object-fit:cover}.audio-icon{color:#ff69b4;font-size:18px}.play-voice-playing{animation:play-voice-animation 1s infinite}@keyframes play-voice-animation{0%{width:0}to{width:32px}}.content-style.loading-text{color:#666;font-style:italic}.play-voice{transition:opacity .3s ease}.play-voice.loading-voice{opacity:.8;cursor:not-allowed}.play-voice.loading-voice .loading-spinner{width:18px;height:18px;color:var(--gray-secondary)}.play-voice.loading-voice .loading-text{font-size:.8em;color:var(--gray-secondary);margin-left:5px}.play-voice .voice-button{display:flex;align-items:center;justify-content:center;width:24px;height:24px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i2.DatePipe, name: "date" }, { kind: "component", type: CompUserAvatarComponent, selector: "app-comp-user-avatar", inputs: ["user"] }, { kind: "ngmodule", type: MarkdownPreviewModule }, { kind: "component", type: MarkdownPreviewComponent, selector: "fm-markdown-preview", inputs: ["content", "render"] }, { kind: "pipe", type: ChatContentPipe, name: "chatContent" }, { kind: "pipe", type: DurationStrPipe, name: "durationStr" }] }); }
16580
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: FmChatMessageCard, isStandalone: true, selector: "fm-chat-message-card", inputs: { index: "index", message: "message", role: "role", chat: "chat" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"message-card\" [class.right]=\"message?.get('content')?.role=='user'\" [class.center]=\"message?.get('content')?.role=='system'\">\r\n <!-- \u7528\u6237\u53CA\u64CD\u4F5C\u533A -->\r\n <div class=\"item-row user\" *ngIf=\"message?.get('content')?.role!='system'\"> <!-- \u7CFB\u7EDF\u6D88\u606F\u4E0D\u663E\u793A\u5934\u50CF -->\r\n <div class=\"avatar-row\">\r\n <div class=\"actions\">\r\n <!-- \u5237\u65B0 -->\r\n <!-- <ion-button fill=\"outline\" slot=\"start\">\r\n <ion-icon name=\"refresh-outline\"></ion-icon>\r\n </ion-button> -->\r\n <!-- \u590D\u5236 -->\r\n <ion-button size=\"small\" fill=\"outline\" slot=\"start\" (click)=\"copy()\">\r\n <ion-icon name=\"copy-outline\"></ion-icon>\r\n </ion-button>\r\n <!-- \u7F16\u8F91 -->\r\n <!-- <ion-button fill=\"outline\" slot=\"start\">\r\n <ion-icon name=\"create-outline\"></ion-icon>\r\n </ion-button> -->\r\n </div>\r\n <!-- \u66F4\u65B0\u97F3\u9891\u6D88\u606F\u533A\u57DF -->\r\n <div *ngIf=\"((message?.get('content')?.role=='assistant' && chat?.role?.get('voiceConfig')?.voice) || (message?.get('content')?.role=='user'&&message?.get('voice')))\"\r\n class=\"play-voice\"\r\n (click)=\"!isLoadingText && toggleVoicePlay()\"\r\n [class.loading-voice]=\"chat?.isTalkMode && isLoadingText\">\r\n\r\n <div class=\"voice-button\">\r\n <!-- \u52A0\u8F7D\u65F6\u663E\u793Aspinner\uFF0C\u5426\u5219\u663E\u793Awifi\u56FE\u6807 -->\r\n <ion-spinner *ngIf=\"chat?.isTalkMode && isLoadingText\" name=\"lines\" class=\"loading-spinner\"></ion-spinner>\r\n <ion-icon *ngIf=\"!(chat?.isTalkMode && isLoadingText)\" name=\"wifi-outline\"\r\n [style.transform]=\"message?.get('content')?.role=='user'?'rotate(-90deg)':'rotate(90deg)'\"\r\n class=\"audio-icon\"\r\n [class.play-voice-playing]=\"tts?.isPlaying\"></ion-icon>\r\n </div>\r\n <div class=\"voice-info\">\r\n <span *ngIf=\"message?.get('voice')?.duration && !isLoadingText\">\r\n {{((message?.get('voice')?.duration||0)/1000) | durationStr}}\r\n </span>\r\n </div>\r\n </div>\r\n <!-- \u5934\u50CF\u533A\u57DF -->\r\n <img class=\"avatar\" *ngIf=\"message?.get('content')?.role!='user'\" [src]=\"(chat?.role?.get('avatar') || chat?.role?.get('thumb') || 'https://file-cloud.fmode.cn/E4KpGvTEto/20230930/l413e6090731854.png')+'?'+'x-image-process=image/resize,m_fixed,w_100'+'&imageView2/1/w/100/h/100'\" >\r\n <app-comp-user-avatar [user]=\"user\" *ngIf=\"message?.get('content')?.role=='user'\"></app-comp-user-avatar>\r\n </div>\r\n </div>\r\n <!-- \u9644\u4EF6\uFF1A\u56FE\u7247 -->\r\n <div class=\"item-row images\" *ngIf=\"message?.get('content') | chatContent:'image_url'\">\r\n <img [src]=\"message?.get('content') | chatContent:'image_url'\" alt=\"\">\r\n </div>\r\n <!-- \u804A\u5929\u6C14\u6CE1 -->\r\n <!-- Replace the bubble section with this: -->\r\n <div class=\"item-row bubble\" [style.fontSize]=\"role?.get('uiConfig')?.msg?.bubble?.fontSize || '0.8rem'\">\r\n <!-- \u8BF4\u8BDD\u6A21\u5F0F\uFF1A\u5C55\u793A\u52A0\u8F7D\u72B6\u6001 Show loading state for talk mode when message is not complete -->\r\n\r\n <!-- Show normal content for non-talk mode or when loading is complete -->\r\n <ng-container *ngIf=\"!chat?.isTalkMode || message?.get('content')?.role !== 'assistant' || !isLoadingText\">\r\n <fm-markdown-preview *ngIf=\"!message?.get('complete')\" class=\"content-style\"\r\n [content]=\"message?.get('content') | chatContent\" [render]=\"false\"></fm-markdown-preview>\r\n <fm-markdown-preview *ngIf=\"message?.get('complete')\"\r\n [content]=\"message?.get('content') | chatContent\"></fm-markdown-preview>\r\n </ng-container>\r\n </div>\r\n <!-- \u65F6\u95F4\u663E\u793A -->\r\n <div class=\"item-row loading\" *ngIf=\"message?.get('content')?.role!='system' && !message?.get('complete')\">\r\n \u6B63\u5728\u8F93\u5165<ion-spinner name=\"dots\"></ion-spinner>\r\n </div>\r\n\r\n <div class=\"item-row created\" *ngIf=\"message?.get('createdAt')\">\r\n <span>{{message?.get('createdAt') | date:\"dd/MM/yy HH:mm\"}}</span>\r\n </div>\r\n</div>", styles: ["@charset \"UTF-8\";:host-context(body.dark) .message-card .actions .item-native{background:none!important}:host-context(body.dark) .message-card .bubble{color:#0e101d}:host-context(body.dark) .message-card .bubble .content-style{filter:invert(1)!important}:host-context(body.dark) .message-card .bubble fm-markdown-preview{filter:invert(1)!important}:host-context(body.dark) .message-card .play-voice{background-color:#0e101d}:host-context(body.dark) .message-card .play-voice .voice-info{color:#fff}:host-context(body.dark) .message-card .right .bubble{color:#921f8a!important;background:#921f8a!important}:host-context(body.dark) .message-card .right .play-voice{background:#921f8a!important}:host-context(body.dark) .message-card .created span{color:#fff}@media screen and (max-width: 800px){.message-card:focus .actions{opacity:1!important}}.message-card:hover .actions{opacity:1;transition:opacity .3s ease-in-out}.message-card{display:flex;flex-wrap:wrap;justify-content:start;align-items:flex-start}.message-card .avatar-row{width:300px;height:32px;display:flex;flex-direction:row-reverse;justify-content:start;align-items:center}.message-card .actions{display:flex;opacity:0;padding-left:10px;padding-right:10px}.message-card .item-row{display:flex;flex:100%;justify-content:start;margin-bottom:5px}.message-card .images img{max-width:300px}.message-card .bubble.loading{color:var(--gray-secondary)}.message-card .bubble.loading .content-style{filter:none}.message-card .bubble{display:flex;justify-content:center;max-width:100%;padding:.5rem .5rem 0rem;color:#333;flex:none;border-radius:0 1.5em 1.5em/0em 1.5em 1.5em;color:#fff;background-color:currentColor}.message-card .bubble .content-style{filter:grayscale(1) contrast(999) invert(1)}.message-card .loading{text-align:right;color:#101010}.message-card .created{display:flex}.message-card .created span{font-size:12px;opacity:.4;white-space:nowrap;transition:all .6s ease;color:var(--black);text-align:center;width:100%;box-sizing:border-box;padding-right:10px;pointer-events:none;z-index:1}.right{justify-content:end;align-items:flex-end}.right .avatar-row{flex-direction:row;justify-content:end;width:auto}.right .actions{position:relative;margin-left:0}.right .item-row{justify-content:end}.right .bubble{color:#bbdefb;border-top-left-radius:1.5em;border-top-right-radius:0}.right .play-voice{flex-direction:row-reverse;background-color:#bbdefb}.center{justify-content:center;align-items:center}.center .item-row{justify-content:center}.center .bubble{color:var(--gray-secondary);border-top-left-radius:1.5em;border-top-right-radius:1.5em;font-size:12px;font-weight:100;padding:5px 20px}.play-voice{min-width:100px;height:32px;display:flex;justify-content:space-around;align-items:center;background-color:#fff;border-radius:7px}.play-voice .voice-button{width:32px;height:32px;display:flex;justify-content:center;align-items:center}.play-voice .voice-button span{overflow:hidden;font-size:18px;color:#ff69b4}.play-voice .voice-info{height:32px;display:flex;padding:0 10px;justify-content:end;align-items:center;color:#333}.avatar{border-radius:50%;width:32px;height:32px;object-fit:cover}.audio-icon{color:#ff69b4;font-size:18px}.play-voice-playing{animation:play-voice-animation 1s infinite}@keyframes play-voice-animation{0%{width:0}to{width:32px}}.content-style.loading-text{color:#666;font-style:italic}.play-voice{transition:opacity .3s ease}.play-voice.loading-voice{opacity:.8;cursor:not-allowed}.play-voice.loading-voice .loading-spinner{width:18px;height:18px;color:var(--gray-secondary)}.play-voice.loading-voice .loading-text{font-size:.8em;color:var(--gray-secondary);margin-left:5px}.play-voice .voice-button{display:flex;align-items:center;justify-content:center;width:24px;height:24px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i2.DatePipe, name: "date" }, { kind: "component", type: CompUserAvatarComponent, selector: "app-comp-user-avatar", inputs: ["user"] }, { kind: "ngmodule", type: MarkdownPreviewModule }, { kind: "component", type: MarkdownPreviewComponent, selector: "fm-markdown-preview", inputs: ["content", "render"] }, { kind: "pipe", type: ChatContentPipe, name: "chatContent" }, { kind: "pipe", type: DurationStrPipe, name: "durationStr" }] }); }
16461
16581
  }
16462
16582
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FmChatMessageCard, decorators: [{
16463
16583
  type: Component,
@@ -16468,7 +16588,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
16468
16588
  ChatContentPipe,
16469
16589
  NzSanitizerPipe,
16470
16590
  DurationStrPipe
16471
- ], template: "<div class=\"message-card\" [class.right]=\"message?.get('role')=='user'\" [class.center]=\"message?.get('role')=='system'\">\r\n <!-- \u7528\u6237\u53CA\u64CD\u4F5C\u533A -->\r\n <div class=\"item-row user\" *ngIf=\"message?.get('role')!='system'\"> <!-- \u7CFB\u7EDF\u6D88\u606F\u4E0D\u663E\u793A\u5934\u50CF -->\r\n <div class=\"avatar-row\">\r\n <div class=\"actions\">\r\n <!-- \u5237\u65B0 -->\r\n <!-- <ion-button fill=\"outline\" slot=\"start\">\r\n <ion-icon name=\"refresh-outline\"></ion-icon>\r\n </ion-button> -->\r\n <!-- \u590D\u5236 -->\r\n <ion-button size=\"small\" fill=\"outline\" slot=\"start\" (click)=\"copy()\">\r\n <ion-icon name=\"copy-outline\"></ion-icon>\r\n </ion-button>\r\n <!-- \u7F16\u8F91 -->\r\n <!-- <ion-button fill=\"outline\" slot=\"start\">\r\n <ion-icon name=\"create-outline\"></ion-icon>\r\n </ion-button> -->\r\n </div>\r\n <!-- \u66F4\u65B0\u97F3\u9891\u6D88\u606F\u533A\u57DF -->\r\n <div *ngIf=\"((message?.get('role')=='assistant' && chat?.role?.get('voiceConfig')?.voice) || (message?.get('role')=='user'&&message?.get('voice')))\"\r\n class=\"play-voice\"\r\n (click)=\"!isLoadingText && toggleVoicePlay()\"\r\n [class.loading-voice]=\"chat?.isTalkMode && isLoadingText\">\r\n\r\n <div class=\"voice-button\">\r\n <!-- \u52A0\u8F7D\u65F6\u663E\u793Aspinner\uFF0C\u5426\u5219\u663E\u793Awifi\u56FE\u6807 -->\r\n <ion-spinner *ngIf=\"chat?.isTalkMode && isLoadingText\" name=\"lines\" class=\"loading-spinner\"></ion-spinner>\r\n <ion-icon *ngIf=\"!(chat?.isTalkMode && isLoadingText)\" name=\"wifi-outline\"\r\n [style.transform]=\"message?.get('role')=='user'?'rotate(-90deg)':'rotate(90deg)'\"\r\n class=\"audio-icon\"\r\n [class.play-voice-playing]=\"tts?.isPlaying\"></ion-icon>\r\n </div>\r\n <div class=\"voice-info\">\r\n <span *ngIf=\"message?.get('voice')?.duration && !isLoadingText\">\r\n {{((message?.get('voice')?.duration||0)/1000) | durationStr}}\r\n </span>\r\n </div>\r\n </div>\r\n <!-- \u5934\u50CF\u533A\u57DF -->\r\n <img class=\"avatar\" *ngIf=\"message?.get('role')!='user'\" [src]=\"(chat?.role?.get('avatar') || chat?.role?.get('thumb') || 'https://file-cloud.fmode.cn/E4KpGvTEto/20230930/l413e6090731854.png')+'?'+'x-image-process=image/resize,m_fixed,w_100'+'&imageView2/1/w/100/h/100'\" >\r\n <app-comp-user-avatar [user]=\"user\" *ngIf=\"message?.get('role')=='user'\"></app-comp-user-avatar>\r\n </div>\r\n </div>\r\n <!-- \u9644\u4EF6\uFF1A\u56FE\u7247 -->\r\n <div class=\"item-row images\" *ngIf=\"message?.get('content') | chatContent:'image_url'\">\r\n <img [src]=\"message?.get('content') | chatContent:'image_url'\" alt=\"\">\r\n </div>\r\n <!-- \u804A\u5929\u6C14\u6CE1 -->\r\n <!-- Replace the bubble section with this: -->\r\n <div class=\"item-row bubble\" [style.fontSize]=\"role?.get('uiConfig')?.msg?.bubble?.fontSize || '0.8rem'\">\r\n <!-- \u8BF4\u8BDD\u6A21\u5F0F\uFF1A\u5C55\u793A\u52A0\u8F7D\u72B6\u6001 Show loading state for talk mode when message is not complete -->\r\n\r\n <!-- Show normal content for non-talk mode or when loading is complete -->\r\n <ng-container *ngIf=\"!chat?.isTalkMode || message?.get('role') !== 'assistant' || !isLoadingText\">\r\n <fm-markdown-preview *ngIf=\"!message?.get('complete')\" class=\"content-style\"\r\n [content]=\"message?.get('content') | chatContent\" [render]=\"false\"></fm-markdown-preview>\r\n <fm-markdown-preview *ngIf=\"message?.get('complete')\"\r\n [content]=\"message?.get('content') | chatContent\"></fm-markdown-preview>\r\n </ng-container>\r\n </div>\r\n <!-- \u65F6\u95F4\u663E\u793A -->\r\n <div class=\"item-row loading\" *ngIf=\"message?.get('role')!='system' && !message?.get('complete')\">\r\n \u6B63\u5728\u8F93\u5165<ion-spinner name=\"dots\"></ion-spinner>\r\n </div>\r\n\r\n <div class=\"item-row created\" *ngIf=\"message?.get('createdAt')\">\r\n <span>{{message?.get('createdAt') | date:\"dd/MM/yy HH:mm\"}}</span>\r\n </div>\r\n</div>", styles: ["@charset \"UTF-8\";:host-context(body.dark) .message-card .actions .item-native{background:none!important}:host-context(body.dark) .message-card .bubble{color:#0e101d}:host-context(body.dark) .message-card .bubble .content-style{filter:invert(1)!important}:host-context(body.dark) .message-card .bubble fm-markdown-preview{filter:invert(1)!important}:host-context(body.dark) .message-card .play-voice{background-color:#0e101d}:host-context(body.dark) .message-card .play-voice .voice-info{color:#fff}:host-context(body.dark) .message-card .right .bubble{color:#921f8a!important;background:#921f8a!important}:host-context(body.dark) .message-card .right .play-voice{background:#921f8a!important}:host-context(body.dark) .message-card .created span{color:#fff}@media screen and (max-width: 800px){.message-card:focus .actions{opacity:1!important}}.message-card:hover .actions{opacity:1;transition:opacity .3s ease-in-out}.message-card{display:flex;flex-wrap:wrap;justify-content:start;align-items:flex-start}.message-card .avatar-row{width:300px;height:32px;display:flex;flex-direction:row-reverse;justify-content:start;align-items:center}.message-card .actions{display:flex;opacity:0;padding-left:10px;padding-right:10px}.message-card .item-row{display:flex;flex:100%;justify-content:start;margin-bottom:5px}.message-card .images img{max-width:300px}.message-card .bubble.loading{color:var(--gray-secondary)}.message-card .bubble.loading .content-style{filter:none}.message-card .bubble{display:flex;justify-content:center;max-width:100%;padding:.5rem .5rem 0rem;color:#333;flex:none;border-radius:0 1.5em 1.5em/0em 1.5em 1.5em;color:#fff;background-color:currentColor}.message-card .bubble .content-style{filter:grayscale(1) contrast(999) invert(1)}.message-card .loading{text-align:right;color:#101010}.message-card .created{display:flex}.message-card .created span{font-size:12px;opacity:.4;white-space:nowrap;transition:all .6s ease;color:var(--black);text-align:center;width:100%;box-sizing:border-box;padding-right:10px;pointer-events:none;z-index:1}.right{justify-content:end;align-items:flex-end}.right .avatar-row{flex-direction:row;justify-content:end;width:auto}.right .actions{position:relative;margin-left:0}.right .item-row{justify-content:end}.right .bubble{color:#bbdefb;border-top-left-radius:1.5em;border-top-right-radius:0}.right .play-voice{flex-direction:row-reverse;background-color:#bbdefb}.center{justify-content:center;align-items:center}.center .item-row{justify-content:center}.center .bubble{color:var(--gray-secondary);border-top-left-radius:1.5em;border-top-right-radius:1.5em;font-size:12px;font-weight:100;padding:5px 20px}.play-voice{min-width:100px;height:32px;display:flex;justify-content:space-around;align-items:center;background-color:#fff;border-radius:7px}.play-voice .voice-button{width:32px;height:32px;display:flex;justify-content:center;align-items:center}.play-voice .voice-button span{overflow:hidden;font-size:18px;color:#ff69b4}.play-voice .voice-info{height:32px;display:flex;padding:0 10px;justify-content:end;align-items:center;color:#333}.avatar{border-radius:50%;width:32px;height:32px;object-fit:cover}.audio-icon{color:#ff69b4;font-size:18px}.play-voice-playing{animation:play-voice-animation 1s infinite}@keyframes play-voice-animation{0%{width:0}to{width:32px}}.content-style.loading-text{color:#666;font-style:italic}.play-voice{transition:opacity .3s ease}.play-voice.loading-voice{opacity:.8;cursor:not-allowed}.play-voice.loading-voice .loading-spinner{width:18px;height:18px;color:var(--gray-secondary)}.play-voice.loading-voice .loading-text{font-size:.8em;color:var(--gray-secondary);margin-left:5px}.play-voice .voice-button{display:flex;align-items:center;justify-content:center;width:24px;height:24px}\n"] }]
16591
+ ], template: "<div class=\"message-card\" [class.right]=\"message?.get('content')?.role=='user'\" [class.center]=\"message?.get('content')?.role=='system'\">\r\n <!-- \u7528\u6237\u53CA\u64CD\u4F5C\u533A -->\r\n <div class=\"item-row user\" *ngIf=\"message?.get('content')?.role!='system'\"> <!-- \u7CFB\u7EDF\u6D88\u606F\u4E0D\u663E\u793A\u5934\u50CF -->\r\n <div class=\"avatar-row\">\r\n <div class=\"actions\">\r\n <!-- \u5237\u65B0 -->\r\n <!-- <ion-button fill=\"outline\" slot=\"start\">\r\n <ion-icon name=\"refresh-outline\"></ion-icon>\r\n </ion-button> -->\r\n <!-- \u590D\u5236 -->\r\n <ion-button size=\"small\" fill=\"outline\" slot=\"start\" (click)=\"copy()\">\r\n <ion-icon name=\"copy-outline\"></ion-icon>\r\n </ion-button>\r\n <!-- \u7F16\u8F91 -->\r\n <!-- <ion-button fill=\"outline\" slot=\"start\">\r\n <ion-icon name=\"create-outline\"></ion-icon>\r\n </ion-button> -->\r\n </div>\r\n <!-- \u66F4\u65B0\u97F3\u9891\u6D88\u606F\u533A\u57DF -->\r\n <div *ngIf=\"((message?.get('content')?.role=='assistant' && chat?.role?.get('voiceConfig')?.voice) || (message?.get('content')?.role=='user'&&message?.get('voice')))\"\r\n class=\"play-voice\"\r\n (click)=\"!isLoadingText && toggleVoicePlay()\"\r\n [class.loading-voice]=\"chat?.isTalkMode && isLoadingText\">\r\n\r\n <div class=\"voice-button\">\r\n <!-- \u52A0\u8F7D\u65F6\u663E\u793Aspinner\uFF0C\u5426\u5219\u663E\u793Awifi\u56FE\u6807 -->\r\n <ion-spinner *ngIf=\"chat?.isTalkMode && isLoadingText\" name=\"lines\" class=\"loading-spinner\"></ion-spinner>\r\n <ion-icon *ngIf=\"!(chat?.isTalkMode && isLoadingText)\" name=\"wifi-outline\"\r\n [style.transform]=\"message?.get('content')?.role=='user'?'rotate(-90deg)':'rotate(90deg)'\"\r\n class=\"audio-icon\"\r\n [class.play-voice-playing]=\"tts?.isPlaying\"></ion-icon>\r\n </div>\r\n <div class=\"voice-info\">\r\n <span *ngIf=\"message?.get('voice')?.duration && !isLoadingText\">\r\n {{((message?.get('voice')?.duration||0)/1000) | durationStr}}\r\n </span>\r\n </div>\r\n </div>\r\n <!-- \u5934\u50CF\u533A\u57DF -->\r\n <img class=\"avatar\" *ngIf=\"message?.get('content')?.role!='user'\" [src]=\"(chat?.role?.get('avatar') || chat?.role?.get('thumb') || 'https://file-cloud.fmode.cn/E4KpGvTEto/20230930/l413e6090731854.png')+'?'+'x-image-process=image/resize,m_fixed,w_100'+'&imageView2/1/w/100/h/100'\" >\r\n <app-comp-user-avatar [user]=\"user\" *ngIf=\"message?.get('content')?.role=='user'\"></app-comp-user-avatar>\r\n </div>\r\n </div>\r\n <!-- \u9644\u4EF6\uFF1A\u56FE\u7247 -->\r\n <div class=\"item-row images\" *ngIf=\"message?.get('content') | chatContent:'image_url'\">\r\n <img [src]=\"message?.get('content') | chatContent:'image_url'\" alt=\"\">\r\n </div>\r\n <!-- \u804A\u5929\u6C14\u6CE1 -->\r\n <!-- Replace the bubble section with this: -->\r\n <div class=\"item-row bubble\" [style.fontSize]=\"role?.get('uiConfig')?.msg?.bubble?.fontSize || '0.8rem'\">\r\n <!-- \u8BF4\u8BDD\u6A21\u5F0F\uFF1A\u5C55\u793A\u52A0\u8F7D\u72B6\u6001 Show loading state for talk mode when message is not complete -->\r\n\r\n <!-- Show normal content for non-talk mode or when loading is complete -->\r\n <ng-container *ngIf=\"!chat?.isTalkMode || message?.get('content')?.role !== 'assistant' || !isLoadingText\">\r\n <fm-markdown-preview *ngIf=\"!message?.get('complete')\" class=\"content-style\"\r\n [content]=\"message?.get('content') | chatContent\" [render]=\"false\"></fm-markdown-preview>\r\n <fm-markdown-preview *ngIf=\"message?.get('complete')\"\r\n [content]=\"message?.get('content') | chatContent\"></fm-markdown-preview>\r\n </ng-container>\r\n </div>\r\n <!-- \u65F6\u95F4\u663E\u793A -->\r\n <div class=\"item-row loading\" *ngIf=\"message?.get('content')?.role!='system' && !message?.get('complete')\">\r\n \u6B63\u5728\u8F93\u5165<ion-spinner name=\"dots\"></ion-spinner>\r\n </div>\r\n\r\n <div class=\"item-row created\" *ngIf=\"message?.get('createdAt')\">\r\n <span>{{message?.get('createdAt') | date:\"dd/MM/yy HH:mm\"}}</span>\r\n </div>\r\n</div>", styles: ["@charset \"UTF-8\";:host-context(body.dark) .message-card .actions .item-native{background:none!important}:host-context(body.dark) .message-card .bubble{color:#0e101d}:host-context(body.dark) .message-card .bubble .content-style{filter:invert(1)!important}:host-context(body.dark) .message-card .bubble fm-markdown-preview{filter:invert(1)!important}:host-context(body.dark) .message-card .play-voice{background-color:#0e101d}:host-context(body.dark) .message-card .play-voice .voice-info{color:#fff}:host-context(body.dark) .message-card .right .bubble{color:#921f8a!important;background:#921f8a!important}:host-context(body.dark) .message-card .right .play-voice{background:#921f8a!important}:host-context(body.dark) .message-card .created span{color:#fff}@media screen and (max-width: 800px){.message-card:focus .actions{opacity:1!important}}.message-card:hover .actions{opacity:1;transition:opacity .3s ease-in-out}.message-card{display:flex;flex-wrap:wrap;justify-content:start;align-items:flex-start}.message-card .avatar-row{width:300px;height:32px;display:flex;flex-direction:row-reverse;justify-content:start;align-items:center}.message-card .actions{display:flex;opacity:0;padding-left:10px;padding-right:10px}.message-card .item-row{display:flex;flex:100%;justify-content:start;margin-bottom:5px}.message-card .images img{max-width:300px}.message-card .bubble.loading{color:var(--gray-secondary)}.message-card .bubble.loading .content-style{filter:none}.message-card .bubble{display:flex;justify-content:center;max-width:100%;padding:.5rem .5rem 0rem;color:#333;flex:none;border-radius:0 1.5em 1.5em/0em 1.5em 1.5em;color:#fff;background-color:currentColor}.message-card .bubble .content-style{filter:grayscale(1) contrast(999) invert(1)}.message-card .loading{text-align:right;color:#101010}.message-card .created{display:flex}.message-card .created span{font-size:12px;opacity:.4;white-space:nowrap;transition:all .6s ease;color:var(--black);text-align:center;width:100%;box-sizing:border-box;padding-right:10px;pointer-events:none;z-index:1}.right{justify-content:end;align-items:flex-end}.right .avatar-row{flex-direction:row;justify-content:end;width:auto}.right .actions{position:relative;margin-left:0}.right .item-row{justify-content:end}.right .bubble{color:#bbdefb;border-top-left-radius:1.5em;border-top-right-radius:0}.right .play-voice{flex-direction:row-reverse;background-color:#bbdefb}.center{justify-content:center;align-items:center}.center .item-row{justify-content:center}.center .bubble{color:var(--gray-secondary);border-top-left-radius:1.5em;border-top-right-radius:1.5em;font-size:12px;font-weight:100;padding:5px 20px}.play-voice{min-width:100px;height:32px;display:flex;justify-content:space-around;align-items:center;background-color:#fff;border-radius:7px}.play-voice .voice-button{width:32px;height:32px;display:flex;justify-content:center;align-items:center}.play-voice .voice-button span{overflow:hidden;font-size:18px;color:#ff69b4}.play-voice .voice-info{height:32px;display:flex;padding:0 10px;justify-content:end;align-items:center;color:#333}.avatar{border-radius:50%;width:32px;height:32px;object-fit:cover}.audio-icon{color:#ff69b4;font-size:18px}.play-voice-playing{animation:play-voice-animation 1s infinite}@keyframes play-voice-animation{0%{width:0}to{width:32px}}.content-style.loading-text{color:#666;font-style:italic}.play-voice{transition:opacity .3s ease}.play-voice.loading-voice{opacity:.8;cursor:not-allowed}.play-voice.loading-voice .loading-spinner{width:18px;height:18px;color:var(--gray-secondary)}.play-voice.loading-voice .loading-text{font-size:.8em;color:var(--gray-secondary);margin-left:5px}.play-voice .voice-button{display:flex;align-items:center;justify-content:center;width:24px;height:24px}\n"] }]
16472
16592
  }], ctorParameters: () => [{ type: ClipboardService }], propDecorators: { index: [{
16473
16593
  type: Input
16474
16594
  }], message: [{
@@ -16551,20 +16671,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
16551
16671
  }] } });
16552
16672
 
16553
16673
  class FmChatMesssageArea {
16554
- get messageList() {
16555
- // 优先使用直接传入的chat对象
16556
- if (this.chat && this.chat.messageList) {
16557
- return this.chat.messageList;
16558
- }
16559
- return [];
16560
- // 降级到通过chatId获取
16561
- // else if(this.chatId && this.chatServ.chatMap[this.chatId]){
16562
- // messages = this.chatServ.chatMap[this.chatId].messageList;
16563
- // }
16564
- // else {
16565
- // return this.cachedMessageList;
16566
- // }
16567
- }
16568
16674
  /**
16569
16675
  * 获取预览消息的FmodeObject
16570
16676
  */
@@ -16586,23 +16692,44 @@ class FmChatMesssageArea {
16586
16692
  }
16587
16693
  constructor(chatServ) {
16588
16694
  this.chatServ = chatServ;
16589
- // 缓存的消息列表,避免重复获取
16590
- this.cachedMessageList = [];
16695
+ // 缓存的消息列表,用于模板绑定
16696
+ this.messageList = [];
16697
+ // 用于变更检测
16591
16698
  this.lastMessageCount = 0;
16699
+ this.lastMessageHash = '';
16700
+ this.refreshTimer = null;
16592
16701
  }
16593
16702
  ngOnChanges(changes) {
16594
- // 当chat或chatId发生变化时,重置缓存
16703
+ // 当chat或chatId发生变化时,重置并刷新
16595
16704
  if (changes.chat || changes.chatId) {
16705
+ this.lastMessageCount = 0;
16706
+ this.lastMessageHash = '';
16596
16707
  this.refreshMessageList();
16597
16708
  }
16598
16709
  }
16599
16710
  ngDoCheck() {
16600
- // 检查消息列表是否发生变化 - 使用同步方法避免性能问题
16601
- const currentMessages = this.messageList;
16602
- if (currentMessages.length !== this.lastMessageCount) {
16603
- this.lastMessageCount = currentMessages.length;
16604
- // 异步刷新,不等待结果
16605
- this.refreshMessageList();
16711
+ // 检查chat.messageList是否有变化
16712
+ if (!this.chat)
16713
+ return;
16714
+ const currentMessages = this.chat.messageList;
16715
+ const currentCount = currentMessages?.length || 0;
16716
+ // 简单hash检测内容变化(使用id和createdAt)
16717
+ let currentHash = '';
16718
+ if (currentMessages && currentMessages.length > 0) {
16719
+ const lastMsg = currentMessages[currentMessages.length - 1];
16720
+ currentHash = `${lastMsg.id}_${lastMsg.get?.('content')?.length || 0}_${currentCount}`;
16721
+ }
16722
+ // 如果消息数量变化或hash变化,触发刷新
16723
+ if (currentCount !== this.lastMessageCount || currentHash !== this.lastMessageHash) {
16724
+ this.lastMessageCount = currentCount;
16725
+ this.lastMessageHash = currentHash;
16726
+ // 防抖:避免频繁刷新
16727
+ if (this.refreshTimer) {
16728
+ clearTimeout(this.refreshTimer);
16729
+ }
16730
+ this.refreshTimer = setTimeout(() => {
16731
+ this.syncMessageList();
16732
+ }, 50);
16606
16733
  }
16607
16734
  }
16608
16735
  ngAfterViewInit() {
@@ -16613,21 +16740,40 @@ class FmChatMesssageArea {
16613
16740
  this.chat.scrollComp = { nativeElement: document.querySelector('.message-list') };
16614
16741
  }
16615
16742
  }
16743
+ ngOnDestroy() {
16744
+ if (this.refreshTimer) {
16745
+ clearTimeout(this.refreshTimer);
16746
+ }
16747
+ }
16748
+ /**
16749
+ * 同步消息列表(同步版本,用于DoCheck)
16750
+ */
16751
+ syncMessageList() {
16752
+ if (!this.chat)
16753
+ return;
16754
+ // 直接获取messageManager中的缓存列表
16755
+ const messages = this.chat.messageList;
16756
+ if (messages && messages.length !== this.messageList.length) {
16757
+ // 创建新数组触发变更检测
16758
+ this.messageList = [...messages];
16759
+ this.scrollToBottom();
16760
+ }
16761
+ }
16616
16762
  /**
16617
- * 刷新消息列表
16763
+ * 刷新消息列表(异步版本,用于初始加载)
16618
16764
  */
16619
16765
  async refreshMessageList() {
16620
16766
  try {
16621
16767
  let messages = [];
16622
16768
  if (this.chat) {
16623
- // 直接获取FmodeObject数组,无需转换
16769
+ // 触发消息加载(如果是第一次)
16624
16770
  messages = await this.chat.getMessageList();
16625
16771
  }
16626
16772
  else if (this.chatId && this.chatServ.chatMap[this.chatId]) {
16627
16773
  messages = await this.chatServ.chatMap[this.chatId].getMessageList();
16628
16774
  }
16629
- // 更新缓存
16630
- this.cachedMessageList = messages;
16775
+ // 创建新数组触发变更检测
16776
+ this.messageList = [...messages];
16631
16777
  this.lastMessageCount = messages.length;
16632
16778
  // 延迟滚动到底部
16633
16779
  setTimeout(() => {
@@ -16654,7 +16800,7 @@ class FmChatMesssageArea {
16654
16800
  }
16655
16801
  }
16656
16802
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FmChatMesssageArea, deps: [{ token: ChatService }], target: i0.ɵɵFactoryTarget.Component }); }
16657
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: FmChatMesssageArea, isStandalone: true, selector: "fm-chat-message-area", inputs: { chatId: "chatId", chat: "chat" }, usesOnChanges: true, ngImport: i0, template: "\r\n<div class=\"message-list\">\r\n <app-comp-role-prompt [chat]=\"chat\" [role]=\"chat?.role?.id\"></app-comp-role-prompt>\r\n\r\n <!-- \u4E3B\u6D88\u606F\u5217\u8868 -->\r\n <ng-container *ngIf=\"messageList && messageList.length > 0\">\r\n <ng-container *ngFor=\"let message of messageList;let index=index;trackBy: trackByMessageId\">\r\n <!-- \u5185\u5BB9\u683C\u5F0F\u5316\u533A\u57DF -->\r\n <fm-chat-message-card [chat]=\"chat\" *ngIf=\"!message?.hidden\" [index]=\"index\" [message]=\"message\" [role]=\"chat?.role\"></fm-chat-message-card>\r\n </ng-container>\r\n </ng-container>\r\n\r\n <!-- \u7A7A\u72B6\u6001\u63D0\u793A -->\r\n <div *ngIf=\"!messageList || messageList.length === 0\" class=\"empty-state\">\r\n <p>\u6682\u65E0\u6D88\u606F\u8BB0\u5F55</p>\r\n <p *ngIf=\"chat?.role\">\u5F00\u59CB\u4E0E {{chat?.role?.get('name')}} \u5BF9\u8BDD\u5427\uFF01</p>\r\n </div>\r\n\r\n <!-- \u9884\u89C8\u6D88\u606F -->\r\n @if(!chat?.hideInputPreview && previewMessage){\r\n <fm-chat-message-card [chat]=\"chat\" [message]=\"previewMessage\" [role]=\"chat?.role\"></fm-chat-message-card>\r\n }\r\n</div>", styles: [".message-list{padding:5px 20px;display:flex;flex-direction:column;height:100%;overflow-y:auto}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;text-align:center;color:#666}.empty-state p{margin:8px 0;font-size:14px}.empty-state p:first-child{font-weight:500;color:#999}:host-context(body.dark) .message-list{background-color:#000!important}:host-context(body.dark) .empty-state{color:#ccc}:host-context(body.dark) .empty-state p:first-child{color:#888}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: FmChatMessageCard, selector: "fm-chat-message-card", inputs: ["index", "message", "role", "chat"] }, { kind: "component", type: CompRolePromptComponent, selector: "app-comp-role-prompt", inputs: ["chat", "role"] }] }); }
16803
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: FmChatMesssageArea, isStandalone: true, selector: "fm-chat-message-area", inputs: { chatId: "chatId", chat: "chat" }, usesOnChanges: true, ngImport: i0, template: "\r\n<div class=\"message-list\">\r\n <app-comp-role-prompt [chat]=\"chat\" [role]=\"chat?.role?.id\"></app-comp-role-prompt>\r\n\r\n <!-- \u4E3B\u6D88\u606F\u5217\u8868 -->\r\n <ng-container *ngIf=\"messageList && messageList.length > 0\">\r\n <ng-container *ngFor=\"let message of messageList;let index=index;trackBy: trackByMessageId\">\r\n <!-- \u5185\u5BB9\u683C\u5F0F\u5316\u533A\u57DF -->\r\n @if(!message?.get(\"content\")?.hidden || !message?.get(\"content\")?.content ){\r\n <fm-chat-message-card [chat]=\"chat\" [index]=\"index\" [message]=\"message\" [role]=\"chat?.role\"></fm-chat-message-card>\r\n }\r\n </ng-container>\r\n </ng-container>\r\n\r\n <!-- \u7A7A\u72B6\u6001\u63D0\u793A -->\r\n <div *ngIf=\"!messageList || messageList.length === 0\" class=\"empty-state\">\r\n <p>\u6682\u65E0\u6D88\u606F\u8BB0\u5F55</p>\r\n <p *ngIf=\"chat?.role\">\u5F00\u59CB\u4E0E {{chat?.role?.get('name')}} \u5BF9\u8BDD\u5427\uFF01</p>\r\n </div>\r\n\r\n <!-- \u9884\u89C8\u6D88\u606F -->\r\n @if(!chat?.hideInputPreview && previewMessage){\r\n <fm-chat-message-card [chat]=\"chat\" [message]=\"previewMessage\" [role]=\"chat?.role\"></fm-chat-message-card>\r\n }\r\n</div>", styles: [".message-list{padding:5px 20px;display:flex;flex-direction:column;height:100%;overflow-y:auto}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;text-align:center;color:#666}.empty-state p{margin:8px 0;font-size:14px}.empty-state p:first-child{font-weight:500;color:#999}:host-context(body.dark) .message-list{background-color:#000!important}:host-context(body.dark) .empty-state{color:#ccc}:host-context(body.dark) .empty-state p:first-child{color:#888}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: FmChatMessageCard, selector: "fm-chat-message-card", inputs: ["index", "message", "role", "chat"] }, { kind: "component", type: CompRolePromptComponent, selector: "app-comp-role-prompt", inputs: ["chat", "role"] }] }); }
16658
16804
  }
16659
16805
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FmChatMesssageArea, decorators: [{
16660
16806
  type: Component,
@@ -16662,7 +16808,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
16662
16808
  CommonModule,
16663
16809
  FmChatMessageCard,
16664
16810
  CompRolePromptComponent,
16665
- ], template: "\r\n<div class=\"message-list\">\r\n <app-comp-role-prompt [chat]=\"chat\" [role]=\"chat?.role?.id\"></app-comp-role-prompt>\r\n\r\n <!-- \u4E3B\u6D88\u606F\u5217\u8868 -->\r\n <ng-container *ngIf=\"messageList && messageList.length > 0\">\r\n <ng-container *ngFor=\"let message of messageList;let index=index;trackBy: trackByMessageId\">\r\n <!-- \u5185\u5BB9\u683C\u5F0F\u5316\u533A\u57DF -->\r\n <fm-chat-message-card [chat]=\"chat\" *ngIf=\"!message?.hidden\" [index]=\"index\" [message]=\"message\" [role]=\"chat?.role\"></fm-chat-message-card>\r\n </ng-container>\r\n </ng-container>\r\n\r\n <!-- \u7A7A\u72B6\u6001\u63D0\u793A -->\r\n <div *ngIf=\"!messageList || messageList.length === 0\" class=\"empty-state\">\r\n <p>\u6682\u65E0\u6D88\u606F\u8BB0\u5F55</p>\r\n <p *ngIf=\"chat?.role\">\u5F00\u59CB\u4E0E {{chat?.role?.get('name')}} \u5BF9\u8BDD\u5427\uFF01</p>\r\n </div>\r\n\r\n <!-- \u9884\u89C8\u6D88\u606F -->\r\n @if(!chat?.hideInputPreview && previewMessage){\r\n <fm-chat-message-card [chat]=\"chat\" [message]=\"previewMessage\" [role]=\"chat?.role\"></fm-chat-message-card>\r\n }\r\n</div>", styles: [".message-list{padding:5px 20px;display:flex;flex-direction:column;height:100%;overflow-y:auto}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;text-align:center;color:#666}.empty-state p{margin:8px 0;font-size:14px}.empty-state p:first-child{font-weight:500;color:#999}:host-context(body.dark) .message-list{background-color:#000!important}:host-context(body.dark) .empty-state{color:#ccc}:host-context(body.dark) .empty-state p:first-child{color:#888}\n"] }]
16811
+ ], template: "\r\n<div class=\"message-list\">\r\n <app-comp-role-prompt [chat]=\"chat\" [role]=\"chat?.role?.id\"></app-comp-role-prompt>\r\n\r\n <!-- \u4E3B\u6D88\u606F\u5217\u8868 -->\r\n <ng-container *ngIf=\"messageList && messageList.length > 0\">\r\n <ng-container *ngFor=\"let message of messageList;let index=index;trackBy: trackByMessageId\">\r\n <!-- \u5185\u5BB9\u683C\u5F0F\u5316\u533A\u57DF -->\r\n @if(!message?.get(\"content\")?.hidden || !message?.get(\"content\")?.content ){\r\n <fm-chat-message-card [chat]=\"chat\" [index]=\"index\" [message]=\"message\" [role]=\"chat?.role\"></fm-chat-message-card>\r\n }\r\n </ng-container>\r\n </ng-container>\r\n\r\n <!-- \u7A7A\u72B6\u6001\u63D0\u793A -->\r\n <div *ngIf=\"!messageList || messageList.length === 0\" class=\"empty-state\">\r\n <p>\u6682\u65E0\u6D88\u606F\u8BB0\u5F55</p>\r\n <p *ngIf=\"chat?.role\">\u5F00\u59CB\u4E0E {{chat?.role?.get('name')}} \u5BF9\u8BDD\u5427\uFF01</p>\r\n </div>\r\n\r\n <!-- \u9884\u89C8\u6D88\u606F -->\r\n @if(!chat?.hideInputPreview && previewMessage){\r\n <fm-chat-message-card [chat]=\"chat\" [message]=\"previewMessage\" [role]=\"chat?.role\"></fm-chat-message-card>\r\n }\r\n</div>", styles: [".message-list{padding:5px 20px;display:flex;flex-direction:column;height:100%;overflow-y:auto}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;text-align:center;color:#666}.empty-state p{margin:8px 0;font-size:14px}.empty-state p:first-child{font-weight:500;color:#999}:host-context(body.dark) .message-list{background-color:#000!important}:host-context(body.dark) .empty-state{color:#ccc}:host-context(body.dark) .empty-state p:first-child{color:#888}\n"] }]
16666
16812
  }], ctorParameters: () => [{ type: ChatService }], propDecorators: { chatId: [{
16667
16813
  type: Input
16668
16814
  }], chat: [{