@godxjp/chat 32.0.0 → 32.0.1
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.
- package/dist/attachments.d.ts +24 -0
- package/dist/attachments.js +8 -0
- package/dist/chat-bubble.d.ts +33 -0
- package/dist/chat-bubble.js +9 -0
- package/dist/chat-composer.d.ts +71 -0
- package/dist/chat-composer.js +7 -0
- package/dist/chat-suggestion.d.ts +31 -0
- package/dist/chat-suggestion.js +7 -0
- package/dist/chunk-4K5JWTV5.js +203 -0
- package/dist/chunk-52TWSF6W.js +180 -0
- package/dist/chunk-DEYHFJBA.js +312 -0
- package/dist/chunk-IWWOS4BL.js +374 -0
- package/dist/chunk-NUCO7NAP.js +40 -0
- package/dist/chunk-PGCRFJHC.js +369 -0
- package/dist/chunk-XCTRKZCF.js +232 -0
- package/dist/conversations.d.ts +42 -0
- package/dist/conversations.js +7 -0
- package/dist/data-entry.prop-D4jLTqUk.d.ts +184 -0
- package/dist/index.d.ts +10 -424
- package/dist/index.js +22 -1672
- package/dist/thought-chain.d.ts +45 -0
- package/dist/thought-chain.js +9 -0
- package/dist/welcome.d.ts +29 -0
- package/dist/welcome.js +7 -0
- package/package.json +2 -2
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/welcome.tsx
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "@godxjp/ui/lib/utils";
|
|
6
|
+
import { Heading, Text } from "@godxjp/ui/general";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
function isImageUrl(icon) {
|
|
9
|
+
return typeof icon === "string" && icon.startsWith("http");
|
|
10
|
+
}
|
|
11
|
+
var Welcome = React.forwardRef(
|
|
12
|
+
({ icon, title, description, extra, variant = "filled", id, className, ...rest }, ref) => {
|
|
13
|
+
return /* @__PURE__ */ jsxs(
|
|
14
|
+
"div",
|
|
15
|
+
{
|
|
16
|
+
ref,
|
|
17
|
+
id,
|
|
18
|
+
"data-slot": "welcome",
|
|
19
|
+
"data-variant": variant,
|
|
20
|
+
className: cn("ui-welcome", className),
|
|
21
|
+
...rest,
|
|
22
|
+
children: [
|
|
23
|
+
icon ? /* @__PURE__ */ jsx("div", { "data-slot": "welcome-icon", className: "ui-welcome-icon", "aria-hidden": "true", children: isImageUrl(icon) ? /* @__PURE__ */ jsx("img", { src: icon, alt: "" }) : icon }) : null,
|
|
24
|
+
/* @__PURE__ */ jsxs("div", { "data-slot": "welcome-content", className: "ui-welcome-content", children: [
|
|
25
|
+
title || extra ? /* @__PURE__ */ jsxs("div", { "data-slot": "welcome-title-row", className: "ui-welcome-title-row", children: [
|
|
26
|
+
title ? /* @__PURE__ */ jsx(Heading, { level: 4, className: "ui-welcome-title", children: title }) : null,
|
|
27
|
+
extra ? /* @__PURE__ */ jsx("div", { "data-slot": "welcome-extra", className: "ui-welcome-extra", children: extra }) : null
|
|
28
|
+
] }) : null,
|
|
29
|
+
description ? /* @__PURE__ */ jsx(Text, { size: "sm", tone: "muted", className: "ui-welcome-description", children: description }) : null
|
|
30
|
+
] })
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
Welcome.displayName = "Welcome";
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
Welcome
|
|
40
|
+
};
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/attachments.tsx
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { FileIcon, ImagePlus, Plus, X } from "lucide-react";
|
|
6
|
+
import { useTranslation } from "@godxjp/ui/i18n";
|
|
7
|
+
import { formatBytes } from "@godxjp/ui/admin";
|
|
8
|
+
import { cn } from "@godxjp/ui/lib/utils";
|
|
9
|
+
import { Button } from "@godxjp/ui/general";
|
|
10
|
+
import { Progress } from "@godxjp/ui/data-display";
|
|
11
|
+
import { createUploadItem } from "@godxjp/ui/data-entry";
|
|
12
|
+
import { readDroppedFiles } from "@godxjp/ui/data-entry";
|
|
13
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
14
|
+
var AttachmentContext = React.createContext({ disabled: false });
|
|
15
|
+
function toAttachmentItem(item) {
|
|
16
|
+
const status = item.status === "uploading" ? "uploading" : item.status === "done" ? "done" : item.status === "error" ? "error" : item.status === "removed" ? "removed" : void 0;
|
|
17
|
+
return {
|
|
18
|
+
uid: item.uid,
|
|
19
|
+
name: item.name,
|
|
20
|
+
size: item.size,
|
|
21
|
+
status,
|
|
22
|
+
percent: item.percent,
|
|
23
|
+
url: item.url,
|
|
24
|
+
thumbUrl: item.previewUrl,
|
|
25
|
+
originFileObj: item.file,
|
|
26
|
+
response: item.response,
|
|
27
|
+
error: item.error
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function resolvePlaceholder(placeholder, type) {
|
|
31
|
+
if (!placeholder) return {};
|
|
32
|
+
return typeof placeholder === "function" ? placeholder(type) : placeholder;
|
|
33
|
+
}
|
|
34
|
+
function splitFileName(name) {
|
|
35
|
+
const match = /^(.*)\.[^.]+$/.exec(name ?? "");
|
|
36
|
+
return match ? [match[1], (name ?? "").slice(match[1].length)] : [name ?? "", ""];
|
|
37
|
+
}
|
|
38
|
+
function fileMatchesAccept(file, accept) {
|
|
39
|
+
if (!accept) return true;
|
|
40
|
+
return accept.split(",").some((rule) => {
|
|
41
|
+
const trimmed = rule.trim().toLowerCase();
|
|
42
|
+
if (trimmed.endsWith("/*")) return file.type.toLowerCase().startsWith(trimmed.slice(0, -1));
|
|
43
|
+
return file.type.toLowerCase() === trimmed || trimmed.startsWith(".") && file.name.toLowerCase().endsWith(trimmed);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function PlaceholderUploader({
|
|
47
|
+
type,
|
|
48
|
+
placeholder,
|
|
49
|
+
onOpen,
|
|
50
|
+
onFiles,
|
|
51
|
+
className,
|
|
52
|
+
style,
|
|
53
|
+
hidden
|
|
54
|
+
}) {
|
|
55
|
+
const { t } = useTranslation();
|
|
56
|
+
const { disabled } = React.useContext(AttachmentContext);
|
|
57
|
+
const [dragActive, setDragActive] = React.useState(false);
|
|
58
|
+
if (hidden) return null;
|
|
59
|
+
const title = placeholder.title ?? t("dataEntry.attachments.placeholderTitle");
|
|
60
|
+
const description = placeholder.description ?? t("dataEntry.attachments.placeholderDescription");
|
|
61
|
+
const icon = placeholder.icon ?? /* @__PURE__ */ jsx(ImagePlus, { "aria-hidden": "true" });
|
|
62
|
+
return /* @__PURE__ */ jsxs(
|
|
63
|
+
"div",
|
|
64
|
+
{
|
|
65
|
+
className: cn("ui-attachments-placeholder", className),
|
|
66
|
+
style,
|
|
67
|
+
"data-drag-active": dragActive ? "true" : void 0,
|
|
68
|
+
"data-disabled": disabled ? "true" : void 0,
|
|
69
|
+
"aria-hidden": disabled ? true : void 0,
|
|
70
|
+
onDragEnter: (event) => {
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
if (!disabled) setDragActive(true);
|
|
73
|
+
},
|
|
74
|
+
onDragOver: (event) => event.preventDefault(),
|
|
75
|
+
onDragLeave: (event) => {
|
|
76
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setDragActive(false);
|
|
77
|
+
},
|
|
78
|
+
onDrop: async (event) => {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
event.stopPropagation();
|
|
81
|
+
setDragActive(false);
|
|
82
|
+
if (disabled) return;
|
|
83
|
+
const files = await readDroppedFiles(event.dataTransfer, false);
|
|
84
|
+
if (files.length) onFiles(files);
|
|
85
|
+
},
|
|
86
|
+
onClick: () => {
|
|
87
|
+
if (!disabled) onOpen();
|
|
88
|
+
},
|
|
89
|
+
onKeyDown: (event) => {
|
|
90
|
+
if (disabled) return;
|
|
91
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
onOpen();
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
role: "button",
|
|
97
|
+
tabIndex: disabled ? -1 : 0,
|
|
98
|
+
"aria-label": String(title),
|
|
99
|
+
children: [
|
|
100
|
+
/* @__PURE__ */ jsx("span", { className: "ui-attachments-placeholder-icon", children: icon }),
|
|
101
|
+
title ? /* @__PURE__ */ jsx("span", { className: "ui-attachments-placeholder-title", children: title }) : null,
|
|
102
|
+
description ? /* @__PURE__ */ jsx("span", { className: "ui-attachments-placeholder-description", children: description }) : null,
|
|
103
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: t("dataEntry.attachments.dropLabel", { type }) })
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
function AttachmentCard({
|
|
109
|
+
item,
|
|
110
|
+
description,
|
|
111
|
+
onRemove,
|
|
112
|
+
classNames
|
|
113
|
+
}) {
|
|
114
|
+
const { t } = useTranslation();
|
|
115
|
+
const { disabled } = React.useContext(AttachmentContext);
|
|
116
|
+
const preview = item.thumbUrl ?? item.url;
|
|
117
|
+
const [namePrefix, nameSuffix] = splitFileName(item.name);
|
|
118
|
+
const desc = description ?? item.description ?? (item.status === "uploading" ? `${item.percent ?? 0}%` : item.status === "error" ? typeof item.response === "string" ? item.response : t("dataEntry.attachments.error") : item.size ? formatBytes(item.size) : "");
|
|
119
|
+
return /* @__PURE__ */ jsxs(
|
|
120
|
+
"li",
|
|
121
|
+
{
|
|
122
|
+
className: cn("ui-attachments-card", classNames?.card, classNames?.file),
|
|
123
|
+
"data-status": item.status,
|
|
124
|
+
children: [
|
|
125
|
+
preview ? /* @__PURE__ */ jsx("img", { src: preview, alt: "", className: "ui-attachments-card-preview", "aria-hidden": "true" }) : /* @__PURE__ */ jsx("span", { className: "ui-attachments-card-placeholder", "aria-hidden": "true", children: /* @__PURE__ */ jsx(FileIcon, {}) }),
|
|
126
|
+
/* @__PURE__ */ jsxs("span", { className: "ui-attachments-card-content", children: [
|
|
127
|
+
/* @__PURE__ */ jsxs("span", { className: "ui-attachments-card-name", title: item.name, children: [
|
|
128
|
+
/* @__PURE__ */ jsx("span", { className: "ui-attachments-card-name-prefix", children: namePrefix }),
|
|
129
|
+
/* @__PURE__ */ jsx("span", { className: "ui-attachments-card-name-suffix", children: nameSuffix })
|
|
130
|
+
] }),
|
|
131
|
+
desc ? /* @__PURE__ */ jsx(
|
|
132
|
+
"span",
|
|
133
|
+
{
|
|
134
|
+
className: "ui-attachments-card-meta",
|
|
135
|
+
title: typeof desc === "string" ? desc : void 0,
|
|
136
|
+
children: desc
|
|
137
|
+
}
|
|
138
|
+
) : null
|
|
139
|
+
] }),
|
|
140
|
+
item.status === "uploading" && item.percent != null ? /* @__PURE__ */ jsx("span", { className: "ui-attachments-card-progress", children: /* @__PURE__ */ jsx(
|
|
141
|
+
Progress,
|
|
142
|
+
{
|
|
143
|
+
size: "sm",
|
|
144
|
+
value: item.percent,
|
|
145
|
+
"aria-label": t("dataEntry.attachments.uploading")
|
|
146
|
+
}
|
|
147
|
+
) }) : null,
|
|
148
|
+
!disabled && onRemove ? /* @__PURE__ */ jsx(
|
|
149
|
+
Button,
|
|
150
|
+
{
|
|
151
|
+
type: "button",
|
|
152
|
+
variant: "ghost",
|
|
153
|
+
size: "icon-xs",
|
|
154
|
+
className: "ui-attachments-card-remove",
|
|
155
|
+
"aria-label": t("dataEntry.attachments.remove", { name: item.name }),
|
|
156
|
+
onClick: (event) => {
|
|
157
|
+
event.stopPropagation();
|
|
158
|
+
onRemove();
|
|
159
|
+
},
|
|
160
|
+
children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
|
|
161
|
+
}
|
|
162
|
+
) : null
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
var Attachments = React.forwardRef(
|
|
168
|
+
({
|
|
169
|
+
className,
|
|
170
|
+
rootClassName,
|
|
171
|
+
classNames = {},
|
|
172
|
+
styles = {},
|
|
173
|
+
items: controlledItems,
|
|
174
|
+
onChange,
|
|
175
|
+
onRemove,
|
|
176
|
+
overflow,
|
|
177
|
+
placeholder,
|
|
178
|
+
getDropContainer,
|
|
179
|
+
disabled = false,
|
|
180
|
+
maxCount,
|
|
181
|
+
accept,
|
|
182
|
+
multiple,
|
|
183
|
+
children
|
|
184
|
+
}, ref) => {
|
|
185
|
+
const { t } = useTranslation();
|
|
186
|
+
const containerRef = React.useRef(null);
|
|
187
|
+
const inputRef = React.useRef(null);
|
|
188
|
+
const [internalItems, setInternalItems] = React.useState([]);
|
|
189
|
+
const [dropActive, setDropActive] = React.useState(false);
|
|
190
|
+
const items = controlledItems ?? internalItems;
|
|
191
|
+
const setItems = React.useCallback(
|
|
192
|
+
(next, file) => {
|
|
193
|
+
if (controlledItems === void 0) setInternalItems(next);
|
|
194
|
+
onChange?.({ file, fileList: next });
|
|
195
|
+
},
|
|
196
|
+
[controlledItems, onChange]
|
|
197
|
+
);
|
|
198
|
+
const openPicker = React.useCallback(() => {
|
|
199
|
+
inputRef.current?.click();
|
|
200
|
+
}, []);
|
|
201
|
+
const dispatchFiles = React.useCallback(
|
|
202
|
+
(files) => {
|
|
203
|
+
const accepted = files.filter((file) => fileMatchesAccept(file, accept));
|
|
204
|
+
if (!accepted.length) return;
|
|
205
|
+
const room = maxCount == null ? accepted.length : Math.max(0, maxCount - items.length);
|
|
206
|
+
const picked = accepted.slice(0, room);
|
|
207
|
+
const next = [
|
|
208
|
+
...items,
|
|
209
|
+
...picked.map((file) => {
|
|
210
|
+
const uploadItem = createUploadItem(file);
|
|
211
|
+
return toAttachmentItem(uploadItem);
|
|
212
|
+
})
|
|
213
|
+
];
|
|
214
|
+
const last = next[next.length - 1];
|
|
215
|
+
if (last) setItems(next, last);
|
|
216
|
+
},
|
|
217
|
+
[accept, items, maxCount, setItems]
|
|
218
|
+
);
|
|
219
|
+
React.useImperativeHandle(ref, () => ({
|
|
220
|
+
nativeElement: containerRef.current,
|
|
221
|
+
fileNativeElement: inputRef.current,
|
|
222
|
+
upload: (file) => dispatchFiles([file]),
|
|
223
|
+
select: (options) => {
|
|
224
|
+
const input = inputRef.current;
|
|
225
|
+
if (!input) return;
|
|
226
|
+
input.multiple = options?.multiple ?? false;
|
|
227
|
+
input.accept = options?.accept ?? accept ?? "";
|
|
228
|
+
input.click();
|
|
229
|
+
}
|
|
230
|
+
}));
|
|
231
|
+
const handleRemove = async (item) => {
|
|
232
|
+
const ret = await Promise.resolve(typeof onRemove === "function" ? onRemove(item) : onRemove);
|
|
233
|
+
if (ret === false) return;
|
|
234
|
+
const next = items.filter((entry) => entry.uid !== item.uid);
|
|
235
|
+
setItems(next, { ...item, status: "removed" });
|
|
236
|
+
};
|
|
237
|
+
const showExtension = !disabled && (maxCount == null || maxCount > items.length) && items.length > 0;
|
|
238
|
+
const inlinePlaceholder = resolvePlaceholder(placeholder, "inline");
|
|
239
|
+
const dropPlaceholder = resolvePlaceholder(placeholder, "drop");
|
|
240
|
+
const hasFiles = items.length > 0;
|
|
241
|
+
const fileInput = /* @__PURE__ */ jsx(
|
|
242
|
+
"input",
|
|
243
|
+
{
|
|
244
|
+
ref: inputRef,
|
|
245
|
+
type: "file",
|
|
246
|
+
className: cn("ui-attachments-input", "sr-only"),
|
|
247
|
+
accept,
|
|
248
|
+
multiple: multiple ?? (maxCount == null || maxCount > 1),
|
|
249
|
+
disabled,
|
|
250
|
+
"aria-label": t("dataEntry.attachments.inputLabel"),
|
|
251
|
+
onChange: (event) => {
|
|
252
|
+
const files = event.target.files ? Array.from(event.target.files) : [];
|
|
253
|
+
event.target.value = "";
|
|
254
|
+
dispatchFiles(files);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
if (children) {
|
|
259
|
+
return /* @__PURE__ */ jsxs(AttachmentContext.Provider, { value: { disabled }, children: [
|
|
260
|
+
fileInput,
|
|
261
|
+
/* @__PURE__ */ jsx(
|
|
262
|
+
"div",
|
|
263
|
+
{
|
|
264
|
+
ref: containerRef,
|
|
265
|
+
className: cn("ui-attachments", rootClassName, classNames.root),
|
|
266
|
+
style: styles.root,
|
|
267
|
+
children
|
|
268
|
+
}
|
|
269
|
+
),
|
|
270
|
+
/* @__PURE__ */ jsx(
|
|
271
|
+
PlaceholderUploader,
|
|
272
|
+
{
|
|
273
|
+
type: "drop",
|
|
274
|
+
placeholder: dropPlaceholder,
|
|
275
|
+
onOpen: openPicker,
|
|
276
|
+
onFiles: dispatchFiles,
|
|
277
|
+
className: classNames.placeholder,
|
|
278
|
+
style: styles.placeholder
|
|
279
|
+
}
|
|
280
|
+
)
|
|
281
|
+
] });
|
|
282
|
+
}
|
|
283
|
+
const dropScope = getDropContainer?.() ? "viewport" : "control";
|
|
284
|
+
return /* @__PURE__ */ jsx(AttachmentContext.Provider, { value: { disabled }, children: /* @__PURE__ */ jsxs(
|
|
285
|
+
"div",
|
|
286
|
+
{
|
|
287
|
+
ref: containerRef,
|
|
288
|
+
className: cn("ui-attachments", className, rootClassName, classNames.root),
|
|
289
|
+
style: styles.root,
|
|
290
|
+
dir: "auto",
|
|
291
|
+
onDragEnter: (event) => {
|
|
292
|
+
event.preventDefault();
|
|
293
|
+
if (!disabled) setDropActive(true);
|
|
294
|
+
},
|
|
295
|
+
onDragOver: (event) => event.preventDefault(),
|
|
296
|
+
onDragLeave: (event) => {
|
|
297
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setDropActive(false);
|
|
298
|
+
},
|
|
299
|
+
onDrop: async (event) => {
|
|
300
|
+
event.preventDefault();
|
|
301
|
+
setDropActive(false);
|
|
302
|
+
if (disabled) return;
|
|
303
|
+
const files = await readDroppedFiles(event.dataTransfer, false);
|
|
304
|
+
if (files.length) dispatchFiles(files);
|
|
305
|
+
},
|
|
306
|
+
children: [
|
|
307
|
+
fileInput,
|
|
308
|
+
hasFiles ? /* @__PURE__ */ jsxs(
|
|
309
|
+
"ul",
|
|
310
|
+
{
|
|
311
|
+
className: cn("ui-attachments-list", classNames.list),
|
|
312
|
+
style: styles.list,
|
|
313
|
+
"data-overflow": overflow ?? "wrap",
|
|
314
|
+
children: [
|
|
315
|
+
items.map((item) => /* @__PURE__ */ jsx(
|
|
316
|
+
AttachmentCard,
|
|
317
|
+
{
|
|
318
|
+
item,
|
|
319
|
+
onRemove: () => handleRemove(item),
|
|
320
|
+
classNames
|
|
321
|
+
},
|
|
322
|
+
item.uid
|
|
323
|
+
)),
|
|
324
|
+
showExtension ? /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
|
|
325
|
+
"button",
|
|
326
|
+
{
|
|
327
|
+
type: "button",
|
|
328
|
+
className: cn("ui-attachments-upload-btn", classNames.upload),
|
|
329
|
+
style: styles.upload,
|
|
330
|
+
disabled,
|
|
331
|
+
"aria-label": t("dataEntry.attachments.add"),
|
|
332
|
+
onClick: openPicker,
|
|
333
|
+
children: /* @__PURE__ */ jsx(Plus, { "aria-hidden": "true" })
|
|
334
|
+
}
|
|
335
|
+
) }) : null
|
|
336
|
+
]
|
|
337
|
+
}
|
|
338
|
+
) : null,
|
|
339
|
+
/* @__PURE__ */ jsx(
|
|
340
|
+
PlaceholderUploader,
|
|
341
|
+
{
|
|
342
|
+
type: "inline",
|
|
343
|
+
placeholder: inlinePlaceholder,
|
|
344
|
+
onOpen: openPicker,
|
|
345
|
+
onFiles: dispatchFiles,
|
|
346
|
+
className: classNames.placeholder,
|
|
347
|
+
style: hasFiles ? { display: "none" } : styles.placeholder,
|
|
348
|
+
hidden: hasFiles
|
|
349
|
+
}
|
|
350
|
+
),
|
|
351
|
+
/* @__PURE__ */ jsx(
|
|
352
|
+
"div",
|
|
353
|
+
{
|
|
354
|
+
className: "ui-attachments-drop-layer",
|
|
355
|
+
"data-scope": dropScope,
|
|
356
|
+
"data-active": dropActive ? "true" : void 0,
|
|
357
|
+
"aria-hidden": "true"
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
]
|
|
361
|
+
}
|
|
362
|
+
) });
|
|
363
|
+
}
|
|
364
|
+
);
|
|
365
|
+
Attachments.displayName = "Attachments";
|
|
366
|
+
|
|
367
|
+
export {
|
|
368
|
+
Attachments
|
|
369
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/chat-bubble.tsx
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { ArrowDown } from "lucide-react";
|
|
6
|
+
import { useTranslation } from "@godxjp/ui/i18n";
|
|
7
|
+
import { useMediaQuery } from "@godxjp/ui/hooks";
|
|
8
|
+
import { cn } from "@godxjp/ui/lib/utils";
|
|
9
|
+
import { Skeleton } from "@godxjp/ui/feedback";
|
|
10
|
+
import { Button } from "@godxjp/ui/general";
|
|
11
|
+
import { Text } from "@godxjp/ui/general";
|
|
12
|
+
import { VisuallyHidden } from "@godxjp/ui/general";
|
|
13
|
+
import { ScrollArea } from "@godxjp/ui/data-display";
|
|
14
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
+
var BODY_SIZE = {
|
|
16
|
+
xs: "2xs",
|
|
17
|
+
sm: "xs",
|
|
18
|
+
md: "sm",
|
|
19
|
+
lg: "lg"
|
|
20
|
+
};
|
|
21
|
+
var META_SIZE = {
|
|
22
|
+
xs: "2xs",
|
|
23
|
+
sm: "2xs",
|
|
24
|
+
md: "xs",
|
|
25
|
+
lg: "sm"
|
|
26
|
+
};
|
|
27
|
+
var TYPING_STEP = 1;
|
|
28
|
+
var TYPING_INTERVAL = 50;
|
|
29
|
+
function resolveTyping(typing) {
|
|
30
|
+
if (typing === void 0 || typing === false) {
|
|
31
|
+
return { enabled: false, step: TYPING_STEP, interval: TYPING_INTERVAL };
|
|
32
|
+
}
|
|
33
|
+
if (typing === true) return { enabled: true, step: TYPING_STEP, interval: TYPING_INTERVAL };
|
|
34
|
+
return {
|
|
35
|
+
enabled: true,
|
|
36
|
+
// A non-positive step would never reach the end of the string and a non-positive interval
|
|
37
|
+
// would spin the timer — both are "the animation never completes", which is the one outcome
|
|
38
|
+
// a reader can neither read nor dismiss.
|
|
39
|
+
step: typing.step != null && typing.step > 0 ? Math.floor(typing.step) : TYPING_STEP,
|
|
40
|
+
interval: typing.interval != null && typing.interval > 0 ? typing.interval : TYPING_INTERVAL
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function useRevealedLength(text, enabled, step, interval) {
|
|
44
|
+
const full = text?.length ?? 0;
|
|
45
|
+
const [revealed, setRevealed] = React.useState(() => enabled ? 0 : full);
|
|
46
|
+
React.useEffect(() => {
|
|
47
|
+
if (!enabled || text === void 0) {
|
|
48
|
+
setRevealed(text?.length ?? 0);
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
setRevealed(0);
|
|
52
|
+
let shown = 0;
|
|
53
|
+
const timer = setInterval(() => {
|
|
54
|
+
shown = Math.min(text.length, shown + step);
|
|
55
|
+
setRevealed(shown);
|
|
56
|
+
if (shown >= text.length) clearInterval(timer);
|
|
57
|
+
}, interval);
|
|
58
|
+
return () => {
|
|
59
|
+
clearInterval(timer);
|
|
60
|
+
};
|
|
61
|
+
}, [text, enabled, step, interval]);
|
|
62
|
+
return Math.min(revealed, full);
|
|
63
|
+
}
|
|
64
|
+
var TONE_KEY = {
|
|
65
|
+
info: "chat.bubble.tone.info",
|
|
66
|
+
success: "chat.bubble.tone.success",
|
|
67
|
+
warning: "chat.bubble.tone.warning",
|
|
68
|
+
destructive: "chat.bubble.tone.destructive"
|
|
69
|
+
};
|
|
70
|
+
var ChatBubble = React.forwardRef(
|
|
71
|
+
({
|
|
72
|
+
children,
|
|
73
|
+
placement = "start",
|
|
74
|
+
variant = "filled",
|
|
75
|
+
avatar,
|
|
76
|
+
header,
|
|
77
|
+
footer,
|
|
78
|
+
loading = false,
|
|
79
|
+
typing,
|
|
80
|
+
size = "md",
|
|
81
|
+
tone = "default",
|
|
82
|
+
className,
|
|
83
|
+
id,
|
|
84
|
+
...props
|
|
85
|
+
}, ref) => {
|
|
86
|
+
const { t } = useTranslation();
|
|
87
|
+
const generatedId = React.useId();
|
|
88
|
+
const headerId = `${id ?? generatedId}-header`;
|
|
89
|
+
const reduceMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
|
|
90
|
+
const text = typeof children === "string" ? children : void 0;
|
|
91
|
+
const { enabled, step, interval } = resolveTyping(typing);
|
|
92
|
+
const animates = enabled && !loading && text !== void 0 && !reduceMotion;
|
|
93
|
+
const revealed = useRevealedLength(text, animates, step, interval);
|
|
94
|
+
const streaming = animates && revealed < (text?.length ?? 0);
|
|
95
|
+
const body = loading ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
96
|
+
/* @__PURE__ */ jsxs("div", { className: "ui-chat-bubble-skeleton", "aria-hidden": "true", children: [
|
|
97
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "ui-chat-bubble-skeleton-line" }),
|
|
98
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "ui-chat-bubble-skeleton-line ui-chat-bubble-skeleton-line--short" })
|
|
99
|
+
] }),
|
|
100
|
+
/* @__PURE__ */ jsx(VisuallyHidden, { children: t("chat.bubble.loading") })
|
|
101
|
+
] }) : streaming ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
102
|
+
/* @__PURE__ */ jsxs(Text, { as: "div", size: BODY_SIZE[size], whitespace: "pre-wrap", "aria-hidden": "true", children: [
|
|
103
|
+
text?.slice(0, revealed),
|
|
104
|
+
/* @__PURE__ */ jsx("span", { className: "ui-chat-bubble-caret" })
|
|
105
|
+
] }),
|
|
106
|
+
/* @__PURE__ */ jsx(VisuallyHidden, { children: t("chat.bubble.typing") })
|
|
107
|
+
] }) : /* @__PURE__ */ jsx(
|
|
108
|
+
Text,
|
|
109
|
+
{
|
|
110
|
+
as: "div",
|
|
111
|
+
size: BODY_SIZE[size],
|
|
112
|
+
whitespace: text !== void 0 ? "pre-wrap" : void 0,
|
|
113
|
+
children
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
return /* @__PURE__ */ jsxs(
|
|
117
|
+
"article",
|
|
118
|
+
{
|
|
119
|
+
ref,
|
|
120
|
+
id,
|
|
121
|
+
"data-slot": "chat-bubble",
|
|
122
|
+
"data-placement": placement,
|
|
123
|
+
"data-variant": variant,
|
|
124
|
+
"data-size": size,
|
|
125
|
+
"data-tone": tone,
|
|
126
|
+
"aria-busy": loading || streaming ? true : void 0,
|
|
127
|
+
"aria-labelledby": header != null ? headerId : void 0,
|
|
128
|
+
className: cn("ui-chat-bubble", className),
|
|
129
|
+
...props,
|
|
130
|
+
children: [
|
|
131
|
+
avatar != null ? /* @__PURE__ */ jsx("span", { "data-slot": "chat-bubble-avatar", className: "ui-chat-bubble-avatar", children: avatar }) : null,
|
|
132
|
+
/* @__PURE__ */ jsxs("div", { "data-slot": "chat-bubble-column", className: "ui-chat-bubble-column", children: [
|
|
133
|
+
header != null ? /* @__PURE__ */ jsx(
|
|
134
|
+
Text,
|
|
135
|
+
{
|
|
136
|
+
as: "div",
|
|
137
|
+
id: headerId,
|
|
138
|
+
className: "ui-chat-bubble-header",
|
|
139
|
+
size: META_SIZE[size],
|
|
140
|
+
tone: "muted",
|
|
141
|
+
weight: "medium",
|
|
142
|
+
children: header
|
|
143
|
+
}
|
|
144
|
+
) : null,
|
|
145
|
+
/* @__PURE__ */ jsxs("div", { "data-slot": "chat-bubble-body", className: "ui-chat-bubble-body", children: [
|
|
146
|
+
tone !== "default" ? /* @__PURE__ */ jsx(VisuallyHidden, { children: t(TONE_KEY[tone]) }) : null,
|
|
147
|
+
body
|
|
148
|
+
] }),
|
|
149
|
+
footer != null ? /* @__PURE__ */ jsx(Text, { as: "div", className: "ui-chat-bubble-footer", size: META_SIZE[size], tone: "muted", children: footer }) : null
|
|
150
|
+
] })
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
ChatBubble.displayName = "ChatBubble";
|
|
157
|
+
var ChatBubbleList = React.forwardRef(
|
|
158
|
+
({ items, roles, autoScroll = true, label, className, id, ...props }, ref) => {
|
|
159
|
+
const { t } = useTranslation();
|
|
160
|
+
const viewportRef = React.useRef(null);
|
|
161
|
+
const [anchored, setAnchored] = React.useState(true);
|
|
162
|
+
const seen = React.useRef(items.length);
|
|
163
|
+
React.useEffect(() => {
|
|
164
|
+
if (anchored) seen.current = items.length;
|
|
165
|
+
}, [anchored, items.length]);
|
|
166
|
+
const pending = anchored ? 0 : Math.max(0, items.length - seen.current);
|
|
167
|
+
const jumpToLatest = React.useCallback(() => {
|
|
168
|
+
const viewport = viewportRef.current;
|
|
169
|
+
if (viewport) {
|
|
170
|
+
const bottom = Math.max(0, viewport.scrollHeight - viewport.clientHeight);
|
|
171
|
+
if (typeof viewport.scrollTo === "function") {
|
|
172
|
+
viewport.scrollTo({ top: bottom, behavior: "instant" });
|
|
173
|
+
} else {
|
|
174
|
+
viewport.scrollTop = bottom;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
setAnchored(true);
|
|
178
|
+
}, []);
|
|
179
|
+
return /* @__PURE__ */ jsxs(
|
|
180
|
+
"div",
|
|
181
|
+
{
|
|
182
|
+
ref,
|
|
183
|
+
id,
|
|
184
|
+
"data-slot": "chat-bubble-list",
|
|
185
|
+
"data-anchored": anchored ? "" : void 0,
|
|
186
|
+
className: cn("ui-chat-bubble-list", className),
|
|
187
|
+
...props,
|
|
188
|
+
children: [
|
|
189
|
+
/* @__PURE__ */ jsx(
|
|
190
|
+
ScrollArea,
|
|
191
|
+
{
|
|
192
|
+
className: "ui-chat-bubble-list-scroller",
|
|
193
|
+
anchor: autoScroll ? "bottom" : "none",
|
|
194
|
+
viewportRef,
|
|
195
|
+
onAnchoredChange: setAnchored,
|
|
196
|
+
children: /* @__PURE__ */ jsx(
|
|
197
|
+
"div",
|
|
198
|
+
{
|
|
199
|
+
role: "log",
|
|
200
|
+
"aria-live": "polite",
|
|
201
|
+
"aria-label": label ?? t("chat.list.label"),
|
|
202
|
+
"data-slot": "chat-bubble-list-feed",
|
|
203
|
+
className: "ui-chat-bubble-list-feed",
|
|
204
|
+
children: items.map(({ id: messageId, role, content, ...message }) => /* @__PURE__ */ jsx(
|
|
205
|
+
ChatBubble,
|
|
206
|
+
{
|
|
207
|
+
id: messageId,
|
|
208
|
+
...role != null ? roles?.[role] : void 0,
|
|
209
|
+
...message,
|
|
210
|
+
children: content
|
|
211
|
+
},
|
|
212
|
+
messageId
|
|
213
|
+
))
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
),
|
|
218
|
+
anchored ? null : /* @__PURE__ */ jsx("div", { "data-slot": "chat-bubble-list-jump", className: "ui-chat-bubble-list-jump", children: /* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", variant: "secondary", shape: "pill", onClick: jumpToLatest, children: [
|
|
219
|
+
/* @__PURE__ */ jsx(ArrowDown, { "aria-hidden": "true" }),
|
|
220
|
+
pending > 0 ? t("chat.list.newMessages", { count: pending }) : t("chat.list.jumpToLatest")
|
|
221
|
+
] }) })
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
ChatBubbleList.displayName = "ChatBubbleList";
|
|
228
|
+
|
|
229
|
+
export {
|
|
230
|
+
ChatBubble,
|
|
231
|
+
ChatBubbleList
|
|
232
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ConversationsProp } from '@godxjp/ui/props/components';
|
|
3
|
+
export { ConversationsCreationProp, ConversationsDividerProp, ConversationsEntryProp, ConversationsGroupableProp, ConversationsItemProp, ConversationsMenuItemProp, ConversationsMenuProp, ConversationsProp, ConversationsProp as ConversationsProps } from '@godxjp/ui/props/components';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Conversations — the session rail of a chat surface (Ant Design X `Conversations`).
|
|
7
|
+
*
|
|
8
|
+
* ## What is ported, and the one half that is deliberately not
|
|
9
|
+
*
|
|
10
|
+
* Every prop a caller passes keeps Ant X's spelling and semantics: `items` (conversations and
|
|
11
|
+
* `{ type: "divider" }` rules), `activeKey` / `defaultActiveKey` / `onActiveChange`, `menu` (flat
|
|
12
|
+
* or per-row), `groupable` (with `label`, `collapsible` and the `expandedKeys` triad) and
|
|
13
|
+
* `creation`. The two knobs left out are `styles` / `classNames` — the semantic style maps — and
|
|
14
|
+
* they are left out by a standing decision of this repository, not by omission: a free-form style
|
|
15
|
+
* hole freezes internal DOM slot names into public API and is unmeasurable by the audit
|
|
16
|
+
* (docs/WHAT-BELONGS-HERE.md · "Lỗ kiểu dáng tự do"; docs/DESIGN-AUTHORITY.md · "A knob that only
|
|
17
|
+
* a fork could reach is not parity either"). The rail is retuned through
|
|
18
|
+
* `src/tokens/components/conversations.css` instead. `prefixCls`, `rootClassName`,
|
|
19
|
+
* `menu.getPopupContainer`, `menu.trigger` and `shortcutKeys` are out for the same reason or
|
|
20
|
+
* because the subsystem behind them does not exist here (see the PR for the per-prop ledger).
|
|
21
|
+
*
|
|
22
|
+
* ## What this adds, because Ant X's rail has no keyboard model at all
|
|
23
|
+
*
|
|
24
|
+
* Measured in `@ant-design/x@2.9.0`: `es/conversations/Item.js` renders a bare
|
|
25
|
+
* `<li title … onClick>`. No `role`, no `tabIndex`, no key handling — the rail is unreachable by
|
|
26
|
+
* keyboard, and a screen reader meets twelve list items with no hint that any of them is a
|
|
27
|
+
* control or that one of them is current. Neither is the hand-rolled alternative the issue
|
|
28
|
+
* describes (a stack of `Button`s) any better on the axis that matters: it is one tab stop PER
|
|
29
|
+
* conversation.
|
|
30
|
+
*
|
|
31
|
+
* So the rows here are real `Button`s under ONE roving tabindex, the APG composite-widget rule:
|
|
32
|
+
*
|
|
33
|
+
* - the whole rail — group headings included — is a single tab stop;
|
|
34
|
+
* - `↑` / `↓` walk it and wrap, `Home` / `End` jump to its ends;
|
|
35
|
+
* - the logical FORWARD arrow (`→` in LTR, `←` in RTL) steps from a row onto its overflow menu,
|
|
36
|
+
* and the backward arrow steps back, so a row's second action costs no extra tab stop;
|
|
37
|
+
* - the current conversation carries `aria-current="true"`, which is what makes "current" survive
|
|
38
|
+
* a theme that does not tint (WCAG 1.4.1 — never colour alone).
|
|
39
|
+
*/
|
|
40
|
+
declare const Conversations: React.ForwardRefExoticComponent<ConversationsProp & React.RefAttributes<HTMLDivElement>>;
|
|
41
|
+
|
|
42
|
+
export { Conversations };
|