@nextclaw/ui 0.10.4 → 0.10.5

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/assets/{ChannelsList-ohaw9GpD.js → ChannelsList-Nu7Ig6_-.js} +1 -1
  3. package/dist/assets/{ChatPage-BqFTFaut.js → ChatPage-CBCFSk4e.js} +24 -24
  4. package/dist/assets/{DocBrowser-Cm8LqQ8S.js → DocBrowser-3CfKmJA6.js} +1 -1
  5. package/dist/assets/{LogoBadge-GTNIKJO9.js → LogoBadge-DdthDJOp.js} +1 -1
  6. package/dist/assets/{MarketplacePage-CfSXSQFi.js → MarketplacePage-inGGiv1T.js} +1 -1
  7. package/dist/assets/{McpMarketplacePage-BRJudD8Z.js → McpMarketplacePage-Dg8GSZh6.js} +1 -1
  8. package/dist/assets/{ModelConfig-B-P04UNK.js → ModelConfig-DyQ6cC92.js} +1 -1
  9. package/dist/assets/{ProvidersList-D9IocmDB.js → ProvidersList-B2T8Lc_i.js} +1 -1
  10. package/dist/assets/{RemoteAccessPage-58fYdCYK.js → RemoteAccessPage-C9LxgK-C.js} +1 -1
  11. package/dist/assets/{RuntimeConfig-C5NlEc57.js → RuntimeConfig-Ey4VIqTW.js} +1 -1
  12. package/dist/assets/{SearchConfig-Cw17ED0n.js → SearchConfig-R1BcCLWO.js} +1 -1
  13. package/dist/assets/{SecretsConfig-DliEIjCS.js → SecretsConfig-D-jZMHeY.js} +2 -2
  14. package/dist/assets/{SessionsConfig-BRqvuKqq.js → SessionsConfig-Cawoh4_2.js} +1 -1
  15. package/dist/assets/{chat-message-C6dxqsRj.js → chat-message-BbuIK4dQ.js} +1 -1
  16. package/dist/assets/index-BulnQWr6.js +8 -0
  17. package/dist/assets/{label-B3FlNUAA.js → label-C7yzBvzK.js} +1 -1
  18. package/dist/assets/{page-layout-HrT1hpq7.js → page-layout-DF0xpax2.js} +1 -1
  19. package/dist/assets/{popover-BEo9XeN-.js → popover-DjaScZDJ.js} +1 -1
  20. package/dist/assets/{security-config-BRE-9ipr.js → security-config-Bg2eriNx.js} +1 -1
  21. package/dist/assets/{skeleton-BeC_fgbu.js → skeleton-DycBJAJF.js} +1 -1
  22. package/dist/assets/{status-dot-D9tZgJWo.js → status-dot-B9opOZ22.js} +1 -1
  23. package/dist/assets/{switch-Cq7y46-d.js → switch-l1P0ev4D.js} +1 -1
  24. package/dist/assets/{tabs-custom-xrIDQNF2.js → tabs-custom-BG9y2JhC.js} +1 -1
  25. package/dist/assets/{useConfirmDialog-DCZbJzHW.js → useConfirmDialog-DTducNfn.js} +1 -1
  26. package/dist/index.html +1 -1
  27. package/package.json +6 -6
  28. package/src/api/ncp-attachments.ts +41 -0
  29. package/src/api/types.ts +13 -0
  30. package/src/components/chat/adapters/chat-message.adapter.test.ts +27 -0
  31. package/src/components/chat/adapters/chat-message.adapter.ts +5 -1
  32. package/src/components/chat/chat-composer-state.test.ts +33 -0
  33. package/src/components/chat/chat-composer-state.ts +3 -1
  34. package/src/components/chat/containers/chat-input-bar.container.tsx +9 -8
  35. package/src/components/chat/ncp/ncp-chat-input.manager.ts +3 -1
  36. package/src/components/chat/ncp/ncp-session-adapter.test.ts +3 -1
  37. package/src/components/chat/ncp/ncp-session-adapter.ts +15 -1
  38. package/src/lib/i18n.chat.ts +7 -7
  39. package/dist/assets/index-B3caHNCU.js +0 -8
@@ -1 +1 @@
1
- import{j as e}from"./vendor-CNhxtHCf.js";import{as as m,c as s}from"./index-B3caHNCU.js";function c({tabs:a,activeTab:i,onChange:o,className:n}){return e.jsx("div",{className:s("flex items-center gap-6 border-b border-gray-200/60 mb-6",n),children:a.map(t=>{const r=i===t.id;return e.jsxs("button",{onClick:()=>o(t.id),className:s("relative pb-3 text-[14px] font-medium transition-all duration-fast flex items-center gap-1.5",r?"text-gray-900":"text-gray-600 hover:text-gray-900"),children:[t.label,t.count!==void 0&&e.jsx("span",{className:s("text-[11px] font-medium","text-gray-500"),children:m(t.count)}),r&&e.jsx("div",{className:"absolute bottom-0 left-0 right-0 h-[2px] bg-primary rounded-full"})]},t.id)})})}export{c as T};
1
+ import{j as e}from"./vendor-CNhxtHCf.js";import{as as m,c as s}from"./index-BulnQWr6.js";function c({tabs:a,activeTab:i,onChange:o,className:n}){return e.jsx("div",{className:s("flex items-center gap-6 border-b border-gray-200/60 mb-6",n),children:a.map(t=>{const r=i===t.id;return e.jsxs("button",{onClick:()=>o(t.id),className:s("relative pb-3 text-[14px] font-medium transition-all duration-fast flex items-center gap-1.5",r?"text-gray-900":"text-gray-600 hover:text-gray-900"),children:[t.label,t.count!==void 0&&e.jsx("span",{className:s("text-[11px] font-medium","text-gray-500"),children:m(t.count)}),r&&e.jsx("div",{className:"absolute bottom-0 left-0 right-0 h-[2px] bg-primary rounded-full"})]},t.id)})})}export{c as T};
@@ -1 +1 @@
1
- import{j as a,r as t}from"./vendor-CNhxtHCf.js";import{am as p,an as C,ao as h,ap as x,aq as g,ar as D,B as d,t as i}from"./index-B3caHNCU.js";const j=({open:l,onOpenChange:r,title:c,description:o,confirmLabel:s=i("confirm"),cancelLabel:e=i("cancel"),variant:n="default",onConfirm:u,onCancel:f})=>{const m=()=>{u(),r(!1)},v=()=>{f(),r(!1)};return a.jsx(p,{open:l,onOpenChange:r,children:a.jsxs(C,{className:"[&>:last-child]:hidden",onCloseAutoFocus:b=>b.preventDefault(),children:[a.jsxs(h,{children:[a.jsx(x,{children:c}),o?a.jsx(g,{children:o}):null]}),a.jsxs(D,{className:"gap-2 sm:gap-0",children:[a.jsx(d,{type:"button",variant:"outline",onClick:v,children:e}),a.jsx(d,{type:"button",variant:n==="destructive"?"destructive":"default",onClick:m,children:s})]})]})})},L={open:!1,title:"",description:"",confirmLabel:i("confirm"),cancelLabel:i("cancel"),variant:"default",resolve:null};function y(){const[l,r]=t.useState(L),c=t.useCallback(e=>new Promise(n=>{r({open:!0,title:e.title,description:e.description??"",confirmLabel:e.confirmLabel??i("confirm"),cancelLabel:e.cancelLabel??i("cancel"),variant:e.variant??"default",resolve:u=>{n(u),r(f=>({...f,open:!1,resolve:null}))}})}),[]),o=t.useCallback(e=>{r(n=>(!e&&n.resolve&&n.resolve(!1),{...n,open:e,resolve:e?n.resolve:null}))},[]),s=t.useCallback(()=>a.jsx(j,{open:l.open,onOpenChange:o,title:l.title,description:l.description||void 0,confirmLabel:l.confirmLabel,cancelLabel:l.cancelLabel,variant:l.variant,onConfirm:()=>{var e;return(e=l.resolve)==null?void 0:e.call(l,!0)},onCancel:()=>{var e;return(e=l.resolve)==null?void 0:e.call(l,!1)}}),[l,o]);return{confirm:c,ConfirmDialog:s}}export{y as u};
1
+ import{j as a,r as t}from"./vendor-CNhxtHCf.js";import{am as p,an as C,ao as h,ap as x,aq as g,ar as D,B as d,t as i}from"./index-BulnQWr6.js";const j=({open:l,onOpenChange:r,title:c,description:o,confirmLabel:s=i("confirm"),cancelLabel:e=i("cancel"),variant:n="default",onConfirm:u,onCancel:f})=>{const m=()=>{u(),r(!1)},v=()=>{f(),r(!1)};return a.jsx(p,{open:l,onOpenChange:r,children:a.jsxs(C,{className:"[&>:last-child]:hidden",onCloseAutoFocus:b=>b.preventDefault(),children:[a.jsxs(h,{children:[a.jsx(x,{children:c}),o?a.jsx(g,{children:o}):null]}),a.jsxs(D,{className:"gap-2 sm:gap-0",children:[a.jsx(d,{type:"button",variant:"outline",onClick:v,children:e}),a.jsx(d,{type:"button",variant:n==="destructive"?"destructive":"default",onClick:m,children:s})]})]})})},L={open:!1,title:"",description:"",confirmLabel:i("confirm"),cancelLabel:i("cancel"),variant:"default",resolve:null};function y(){const[l,r]=t.useState(L),c=t.useCallback(e=>new Promise(n=>{r({open:!0,title:e.title,description:e.description??"",confirmLabel:e.confirmLabel??i("confirm"),cancelLabel:e.cancelLabel??i("cancel"),variant:e.variant??"default",resolve:u=>{n(u),r(f=>({...f,open:!1,resolve:null}))}})}),[]),o=t.useCallback(e=>{r(n=>(!e&&n.resolve&&n.resolve(!1),{...n,open:e,resolve:e?n.resolve:null}))},[]),s=t.useCallback(()=>a.jsx(j,{open:l.open,onOpenChange:o,title:l.title,description:l.description||void 0,confirmLabel:l.confirmLabel,cancelLabel:l.cancelLabel,variant:l.variant,onConfirm:()=>{var e;return(e=l.resolve)==null?void 0:e.call(l,!0)},onCancel:()=>{var e;return(e=l.resolve)==null?void 0:e.call(l,!1)}}),[l,o]);return{confirm:c,ConfirmDialog:s}}export{y as u};
package/dist/index.html CHANGED
@@ -6,7 +6,7 @@
6
6
  <link rel="icon" type="image/svg+xml" href="/logo.svg" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
  <title>NextClaw</title>
9
- <script type="module" crossorigin src="/assets/index-B3caHNCU.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-BulnQWr6.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-CNhxtHCf.js">
11
11
  <link rel="stylesheet" crossorigin href="/assets/index-kaPUhd-8.css">
12
12
  </head>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.10.4",
3
+ "version": "0.10.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,11 +28,11 @@
28
28
  "tailwind-merge": "^2.5.4",
29
29
  "zod": "^3.23.8",
30
30
  "zustand": "^5.0.2",
31
- "@nextclaw/ncp-react": "0.3.5",
32
- "@nextclaw/ncp": "0.3.2",
33
- "@nextclaw/ncp-http-agent-client": "0.3.2",
34
- "@nextclaw/agent-chat-ui": "0.2.4",
35
- "@nextclaw/agent-chat": "0.1.2"
31
+ "@nextclaw/ncp": "0.3.3",
32
+ "@nextclaw/agent-chat": "0.1.3",
33
+ "@nextclaw/agent-chat-ui": "0.2.5",
34
+ "@nextclaw/ncp-http-agent-client": "0.3.3",
35
+ "@nextclaw/ncp-react": "0.3.6"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@testing-library/react": "^16.3.0",
@@ -0,0 +1,41 @@
1
+ import type { NcpDraftAttachment } from "@nextclaw/ncp-react";
2
+ import { API_BASE } from "./api-base";
3
+ import type { ApiResponse, NcpAttachmentUploadView } from "./types";
4
+
5
+ function readErrorMessage(payload: unknown, fallback: string): string {
6
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
7
+ return fallback;
8
+ }
9
+ const error = (payload as { error?: unknown }).error;
10
+ if (!error || typeof error !== "object" || Array.isArray(error)) {
11
+ return fallback;
12
+ }
13
+ const message = (error as { message?: unknown }).message;
14
+ return typeof message === "string" && message.trim().length > 0 ? message : fallback;
15
+ }
16
+
17
+ export async function uploadNcpAttachments(files: File[]): Promise<NcpDraftAttachment[]> {
18
+ const formData = new FormData();
19
+ for (const file of files) {
20
+ formData.append("files", file);
21
+ }
22
+
23
+ const response = await fetch(`${API_BASE}/api/ncp/attachments`, {
24
+ method: "POST",
25
+ body: formData,
26
+ credentials: "include",
27
+ });
28
+ const payload = (await response.json()) as ApiResponse<NcpAttachmentUploadView>;
29
+ if (!response.ok || !payload.ok) {
30
+ throw new Error(readErrorMessage(payload, "Failed to upload attachments."));
31
+ }
32
+
33
+ return payload.data.attachments.map((attachment) => ({
34
+ id: attachment.id,
35
+ name: attachment.name,
36
+ mimeType: attachment.mimeType,
37
+ sizeBytes: attachment.sizeBytes,
38
+ attachmentUri: attachment.attachmentUri,
39
+ url: attachment.url,
40
+ }));
41
+ }
package/src/api/types.ts CHANGED
@@ -259,6 +259,19 @@ export type NcpSessionMessagesView = {
259
259
  total: number;
260
260
  };
261
261
 
262
+ export type NcpAttachmentView = {
263
+ id: string;
264
+ name: string;
265
+ mimeType: string;
266
+ sizeBytes: number;
267
+ attachmentUri: string;
268
+ url: string;
269
+ };
270
+
271
+ export type NcpAttachmentUploadView = {
272
+ attachments: NcpAttachmentView[];
273
+ };
274
+
262
275
  export type NcpSessionStatusView = NcpSessionStatus;
263
276
 
264
277
  export type SessionPatchUpdate = {
@@ -164,3 +164,30 @@ it("maps file parts into previewable attachment view models", () => {
164
164
  },
165
165
  });
166
166
  });
167
+
168
+ it("keeps named non-image files as downloadable attachments", () => {
169
+ const adapted = adapt([
170
+ {
171
+ id: "assistant-doc",
172
+ role: "assistant",
173
+ parts: [
174
+ {
175
+ type: "file",
176
+ name: "spec.pdf",
177
+ mimeType: "application/pdf",
178
+ data: "cGRm",
179
+ },
180
+ ],
181
+ },
182
+ ] as unknown as ChatMessageSource[]);
183
+
184
+ expect(adapted[0]?.parts[0]).toEqual({
185
+ type: "file",
186
+ file: {
187
+ label: "spec.pdf",
188
+ mimeType: "application/pdf",
189
+ dataUrl: "data:application/pdf;base64,cGRm",
190
+ isImage: false,
191
+ },
192
+ });
193
+ });
@@ -18,6 +18,7 @@ export type ChatMessagePartSource =
18
18
  type: 'file';
19
19
  mimeType: string;
20
20
  data: string;
21
+ url?: string;
21
22
  name?: string;
22
23
  }
23
24
  | {
@@ -212,7 +213,10 @@ export function adaptChatMessages(params: {
212
213
  ? params.texts.imageAttachmentLabel
213
214
  : params.texts.fileAttachmentLabel,
214
215
  mimeType: part.mimeType,
215
- dataUrl: `data:${part.mimeType};base64,${part.data}`,
216
+ dataUrl:
217
+ typeof part.url === 'string' && part.url.trim().length > 0
218
+ ? part.url.trim()
219
+ : `data:${part.mimeType};base64,${part.data}`,
216
220
  isImage
217
221
  }
218
222
  };
@@ -71,4 +71,37 @@ describe('deriveNcpMessagePartsFromComposer', () => {
71
71
  }
72
72
  ]);
73
73
  });
74
+
75
+ it('preserves uploaded attachment references when the attachment has a server uri', () => {
76
+ const parts = deriveNcpMessagePartsFromComposer(
77
+ [
78
+ createChatComposerTokenNode({
79
+ tokenKind: 'file',
80
+ tokenKey: 'config',
81
+ label: 'config.json'
82
+ })
83
+ ],
84
+ [
85
+ {
86
+ id: 'config',
87
+ name: 'config.json',
88
+ mimeType: 'application/json',
89
+ sizeBytes: 18,
90
+ attachmentUri: 'attachment://local/2026/03/26/att_123',
91
+ url: '/api/ncp/attachments/content?uri=attachment%3A%2F%2Flocal%2F2026%2F03%2F26%2Fatt_123'
92
+ }
93
+ ]
94
+ );
95
+
96
+ expect(parts).toEqual([
97
+ {
98
+ type: 'file',
99
+ name: 'config.json',
100
+ mimeType: 'application/json',
101
+ attachmentUri: 'attachment://local/2026/03/26/att_123',
102
+ url: '/api/ncp/attachments/content?uri=attachment%3A%2F%2Flocal%2F2026%2F03%2F26%2Fatt_123',
103
+ sizeBytes: 18
104
+ }
105
+ ]);
106
+ });
74
107
  });
@@ -108,7 +108,9 @@ export function deriveNcpMessagePartsFromComposer(
108
108
  type: 'file',
109
109
  name: attachment.name,
110
110
  mimeType: attachment.mimeType,
111
- contentBase64: attachment.contentBase64,
111
+ ...(attachment.attachmentUri ? { attachmentUri: attachment.attachmentUri } : {}),
112
+ ...(attachment.url ? { url: attachment.url } : {}),
113
+ ...(attachment.contentBase64 ? { contentBase64: attachment.contentBase64 } : {}),
112
114
  sizeBytes: attachment.sizeBytes
113
115
  });
114
116
  }
@@ -1,10 +1,10 @@
1
1
  import { useCallback, useMemo, useRef, useState } from 'react';
2
2
  import { ChatInputBar, type ChatInputBarHandle } from '@nextclaw/agent-chat-ui';
3
3
  import {
4
- DEFAULT_NCP_IMAGE_ATTACHMENT_ACCEPT,
5
- DEFAULT_NCP_IMAGE_ATTACHMENT_MAX_BYTES,
6
- readFilesAsNcpDraftAttachments
4
+ DEFAULT_NCP_ATTACHMENT_MAX_BYTES,
5
+ uploadFilesAsNcpDraftAttachments
7
6
  } from '@nextclaw/ncp-react';
7
+ import { uploadNcpAttachments } from '@/api/ncp-attachments';
8
8
  import {
9
9
  buildChatSlashItems,
10
10
  buildModelStateHint,
@@ -133,23 +133,25 @@ export function ChatInputBarContainer() {
133
133
 
134
134
  const showAttachmentError = useCallback((reason: 'unsupported-type' | 'too-large' | 'read-failed') => {
135
135
  if (reason === 'unsupported-type') {
136
- toast.error(t('chatInputImageUnsupported'));
136
+ toast.error(t('chatInputAttachmentUnsupported'));
137
137
  return;
138
138
  }
139
139
  if (reason === 'too-large') {
140
140
  toast.error(
141
- t('chatInputImageTooLarge').replace('{maxMb}', String(DEFAULT_NCP_IMAGE_ATTACHMENT_MAX_BYTES / (1024 * 1024)))
141
+ t('chatInputAttachmentTooLarge').replace('{maxMb}', String(DEFAULT_NCP_ATTACHMENT_MAX_BYTES / (1024 * 1024)))
142
142
  );
143
143
  return;
144
144
  }
145
- toast.error(t('chatInputImageReadFailed'));
145
+ toast.error(t('chatInputAttachmentReadFailed'));
146
146
  }, []);
147
147
 
148
148
  const handleFilesAdd = useCallback(async (files: File[]) => {
149
149
  if (!attachmentSupported || files.length === 0) {
150
150
  return;
151
151
  }
152
- const result = await readFilesAsNcpDraftAttachments(files);
152
+ const result = await uploadFilesAsNcpDraftAttachments(files, {
153
+ uploadBatch: uploadNcpAttachments,
154
+ });
153
155
  if (result.attachments.length > 0) {
154
156
  const insertedAttachments = presenter.chatInputManager.addAttachments?.(result.attachments) ?? [];
155
157
  if (insertedAttachments.length > 0) {
@@ -275,7 +277,6 @@ export function ChatInputBarContainer() {
275
277
  <input
276
278
  ref={fileInputRef}
277
279
  type="file"
278
- accept={DEFAULT_NCP_IMAGE_ATTACHMENT_ACCEPT}
279
280
  multiple
280
281
  className="hidden"
281
282
  onChange={async (event) => {
@@ -27,10 +27,12 @@ export class NcpChatInputManager {
27
27
 
28
28
  private buildAttachmentSignature = (attachment: NcpDraftAttachment): string =>
29
29
  [
30
+ attachment.attachmentUri ?? '',
31
+ attachment.url ?? '',
30
32
  attachment.name,
31
33
  attachment.mimeType,
32
34
  String(attachment.sizeBytes),
33
- attachment.contentBase64,
35
+ attachment.contentBase64 ?? '',
34
36
  ].join(':');
35
37
 
36
38
  constructor(
@@ -69,8 +69,10 @@ describe('adaptNcpMessageToUiMessage', () => {
69
69
  },
70
70
  {
71
71
  type: 'file',
72
+ name: 'sample.png',
72
73
  mimeType: 'image/png',
73
- data: 'ZmFrZS1pbWFnZQ=='
74
+ data: 'ZmFrZS1pbWFnZQ==',
75
+ sizeBytes: 10
74
76
  },
75
77
  {
76
78
  type: 'text',
@@ -121,8 +121,22 @@ function toUiParts(parts: NcpMessagePart[]): UIMessage['parts'] {
121
121
  if (part.type === 'file' && part.contentBase64) {
122
122
  uiParts.push({
123
123
  type: 'file',
124
+ ...(part.name ? { name: part.name } : {}),
124
125
  mimeType: part.mimeType ?? 'application/octet-stream',
125
- data: part.contentBase64
126
+ data: part.contentBase64,
127
+ ...(part.url ? { url: part.url } : {}),
128
+ ...(typeof part.sizeBytes === 'number' ? { sizeBytes: part.sizeBytes } : {})
129
+ });
130
+ continue;
131
+ }
132
+ if (part.type === 'file' && part.url) {
133
+ uiParts.push({
134
+ type: 'file',
135
+ ...(part.name ? { name: part.name } : {}),
136
+ mimeType: part.mimeType ?? 'application/octet-stream',
137
+ data: '',
138
+ url: part.url,
139
+ ...(typeof part.sizeBytes === 'number' ? { sizeBytes: part.sizeBytes } : {})
126
140
  });
127
141
  continue;
128
142
  }
@@ -104,14 +104,14 @@ export const CHAT_LABELS: Record<string, { zh: string; en: string }> = {
104
104
  chatSkillsPickerOfficial: { zh: '官方', en: 'Official' },
105
105
  chatSkillsPickerManage: { zh: '管理技能', en: 'Manage Skills' },
106
106
  chatInputAttach: { zh: '添加附件', en: 'Attach file' },
107
- chatInputImageUnsupported: {
108
- zh: '当前仅支持 PNG、JPEG、WEBP、GIF 图片。',
109
- en: 'Only PNG, JPEG, WEBP, and GIF images are supported.'
107
+ chatInputAttachmentUnsupported: {
108
+ zh: '当前上传流程不支持该文件类型。',
109
+ en: 'This file type is not supported in the current upload flow.'
110
110
  },
111
- chatInputImageTooLarge: {
112
- zh: '图片不能超过 {maxMb} MB。',
113
- en: 'Images must be {maxMb} MB or smaller.'
111
+ chatInputAttachmentTooLarge: {
112
+ zh: '文件不能超过 {maxMb} MB。',
113
+ en: 'Files must be {maxMb} MB or smaller.'
114
114
  },
115
- chatInputImageReadFailed: { zh: '读取图片失败,请重试。', en: 'Failed to read the image. Please try again.' },
115
+ chatInputAttachmentReadFailed: { zh: '读取文件失败,请重试。', en: 'Failed to read the file. Please try again.' },
116
116
  chatInputAttachComingSoon: { zh: '即将支持', en: 'Coming soon' }
117
117
  };