@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/index.js CHANGED
@@ -1,1677 +1,27 @@
1
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
- // src/chat-composer.tsx
230
- import * as React2 from "react";
231
- import { SendHorizontal, Square } from "lucide-react";
232
- import { useTranslation as useTranslation2 } from "@godxjp/ui/i18n";
233
- import { cn as cn2 } from "@godxjp/ui/lib/utils";
234
- import { isApplePlatform } from "@godxjp/ui/lib/utils";
235
- import { omitFieldA11y, pickFieldA11y, useFieldIdentity } from "@godxjp/ui/data-entry";
236
- import { Button as Button2 } from "@godxjp/ui/general";
237
- import { Textarea } from "@godxjp/ui/data-entry";
238
- import { controlSurfaceAttrs, resolveAriaInvalid } from "@godxjp/ui/data-entry";
239
- import { isImeComposing } from "@godxjp/ui/data-entry";
240
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
241
- function isSendable(text) {
242
- return text.trim().length > 0;
243
- }
244
- var ChatComposer = React2.forwardRef(
245
- ({
246
- className,
247
- id,
248
- name,
249
- value,
250
- defaultValue,
251
- onValueChange,
252
- onSubmit,
253
- onCancel,
254
- loading = false,
255
- submitType = "enter",
256
- allowEmptySubmit = false,
257
- placeholder,
258
- disabled = false,
259
- readOnly = false,
260
- header,
261
- prefix,
262
- footer,
263
- actions,
264
- size,
265
- maxLength,
266
- status,
267
- submitLabel,
268
- cancelLabel,
269
- onKeyDown,
270
- ...props
271
- }, ref) => {
272
- const { t } = useTranslation2();
273
- const innerRef = React2.useRef(null);
274
- const setRefs = React2.useCallback(
275
- (node) => {
276
- innerRef.current = node;
277
- if (typeof ref === "function") ref(node);
278
- else if (ref) ref.current = node;
279
- },
280
- [ref]
281
- );
282
- const [draft, setDraft] = React2.useState(() => String(value ?? defaultValue ?? ""));
283
- React2.useEffect(() => {
284
- if (value !== void 0) setDraft(String(value));
285
- }, [value]);
286
- const composing = React2.useRef(false);
287
- const fieldA11y = pickFieldA11y(props);
288
- const rest = omitFieldA11y(props);
289
- const identity = useFieldIdentity({ id, name, "data-field": props["data-field"] });
290
- const surface = controlSurfaceAttrs({ status, size });
291
- const canSubmit = (allowEmptySubmit || isSendable(draft)) && !disabled && !readOnly && !loading;
292
- const submit = React2.useCallback(() => {
293
- if (disabled || readOnly || loading) return;
294
- const text = innerRef.current?.value ?? draft;
295
- if (isSendable(text)) onSubmit?.(text);
296
- else if (allowEmptySubmit) onSubmit?.("");
297
- }, [draft, disabled, readOnly, loading, allowEmptySubmit, onSubmit]);
298
- const handleKeyDown = (event) => {
299
- onKeyDown?.(event);
300
- if (event.defaultPrevented) return;
301
- if (event.key !== "Enter") return;
302
- if (composing.current || isImeComposing(event)) return;
303
- const wantsSend = submitType === "modEnter" ? (
304
- // ⌘ on Apple platforms, Ctrl everywhere else — the other one is left alone, because
305
- // Ctrl+Enter on a Mac is not the convention a Mac user reaches for.
306
- isApplePlatform() ? event.metaKey : event.ctrlKey
307
- ) : submitType === "enter" ? !event.shiftKey : event.shiftKey;
308
- if (!wantsSend) return;
309
- event.preventDefault();
310
- submit();
311
- };
312
- const sendName = submitLabel ?? t("dataEntry.chatComposer.send");
313
- const cancelName = cancelLabel ?? t("dataEntry.chatComposer.cancel");
314
- const actionSize = size === "xs" ? "icon-xs" : size === "sm" ? "icon-sm" : size === "lg" ? "icon-lg" : "icon";
315
- const liveRef = React2.useRef({
316
- actionSize,
317
- sendName,
318
- cancelName,
319
- submit,
320
- onCancel,
321
- canSubmit,
322
- disabled
323
- });
324
- liveRef.current = { actionSize, sendName, cancelName, submit, onCancel, canSubmit, disabled };
325
- const SubmitButton = React2.useMemo(
326
- () => function SubmitButton2(buttonProps) {
327
- const live = liveRef.current;
328
- return /* @__PURE__ */ jsx2(
329
- Button2,
330
- {
331
- type: "button",
332
- size: live.actionSize,
333
- "aria-label": live.sendName,
334
- onClick: live.submit,
335
- disabled: !live.canSubmit,
336
- ...buttonProps,
337
- children: buttonProps.children ?? /* @__PURE__ */ jsx2(SendHorizontal, { "aria-hidden": "true" })
338
- }
339
- );
340
- },
341
- []
342
- );
343
- const CancelButton = React2.useMemo(
344
- () => function CancelButton2(buttonProps) {
345
- const live = liveRef.current;
346
- return /* @__PURE__ */ jsx2(
347
- Button2,
348
- {
349
- type: "button",
350
- size: live.actionSize,
351
- variant: "secondary",
352
- "aria-label": live.cancelName,
353
- onClick: live.onCancel,
354
- disabled: live.disabled,
355
- ...buttonProps,
356
- children: buttonProps.children ?? /* @__PURE__ */ jsx2(Square, { "aria-hidden": "true" })
357
- }
358
- );
359
- },
360
- []
361
- );
362
- const actionComponents = React2.useMemo(
363
- () => ({ SubmitButton, CancelButton }),
364
- [SubmitButton, CancelButton]
365
- );
366
- const footerContent = typeof footer === "function" ? footer({ components: actionComponents }) : footer;
367
- return /* @__PURE__ */ jsxs2(
368
- "div",
369
- {
370
- "data-slot": "chat-composer",
371
- "data-disabled": disabled ? "" : void 0,
372
- "data-loading": loading ? "" : void 0,
373
- "data-submit-type": submitType,
374
- "aria-busy": loading || void 0,
375
- className: cn2("ui-chat-composer ui-control-surface", className),
376
- ...rest,
377
- ...surface,
378
- children: [
379
- header ? /* @__PURE__ */ jsx2("div", { "data-slot": "chat-composer-header", className: "ui-chat-composer-header", children: header }) : null,
380
- /* @__PURE__ */ jsxs2("div", { "data-slot": "chat-composer-row", className: "ui-chat-composer-row", children: [
381
- prefix ? /* @__PURE__ */ jsx2("div", { "data-slot": "chat-composer-prefix", className: "ui-chat-composer-prefix", children: prefix }) : null,
382
- /* @__PURE__ */ jsx2(
383
- Textarea,
384
- {
385
- ref: setRefs,
386
- id,
387
- name,
388
- variant: "borderless",
389
- autoGrow: true,
390
- className: "ui-chat-composer-field",
391
- value,
392
- defaultValue,
393
- placeholder: placeholder ?? t("dataEntry.chatComposer.placeholder"),
394
- disabled,
395
- readOnly,
396
- maxLength,
397
- status,
398
- "aria-invalid": resolveAriaInvalid(fieldA11y["aria-invalid"], status),
399
- onValueChange: (next) => {
400
- if (value === void 0) setDraft(next);
401
- onValueChange?.(next);
402
- },
403
- onKeyDown: handleKeyDown,
404
- onCompositionStart: () => {
405
- composing.current = true;
406
- },
407
- onCompositionEnd: () => {
408
- composing.current = false;
409
- },
410
- ...fieldA11y,
411
- ...identity
412
- }
413
- ),
414
- actions === false ? null : /* @__PURE__ */ jsxs2("div", { "data-slot": "chat-composer-actions", className: "ui-chat-composer-actions", children: [
415
- actions,
416
- loading ? /* @__PURE__ */ jsx2(CancelButton, {}) : /* @__PURE__ */ jsx2(SubmitButton, {})
417
- ] })
418
- ] }),
419
- footerContent ? /* @__PURE__ */ jsx2("div", { "data-slot": "chat-composer-footer", className: "ui-chat-composer-footer", children: footerContent }) : null
420
- ]
421
- }
422
- );
423
- }
424
- );
425
- ChatComposer.displayName = "ChatComposer";
426
-
427
- // src/chat-suggestion.tsx
428
- import * as React3 from "react";
429
- import { useTranslation as useTranslation3 } from "@godxjp/ui/i18n";
430
- import { cn as cn3 } from "@godxjp/ui/lib/utils";
431
- import { VisuallyHidden as VisuallyHidden2 } from "@godxjp/ui/general";
432
- import { Popover, PopoverAnchor, PopoverContent } from "@godxjp/ui/data-display";
433
- import { Command, CommandEmpty, CommandItem, CommandList } from "@godxjp/ui/data-entry";
434
- import { isImeComposing as isImeComposing2 } from "@godxjp/ui/data-entry";
435
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
436
- function matchTrigger(text, caret, trigger, { allowSpaces = false, terminator } = {}) {
437
- if (!trigger) return null;
438
- const before = text.slice(0, caret);
439
- const start = before.lastIndexOf(trigger);
440
- if (start === -1) return null;
441
- const query = before.slice(start + trigger.length);
442
- if (allowSpaces ? /\n/.test(query) : /\s/.test(query)) return null;
443
- if (terminator && query.includes(terminator)) return null;
444
- const preceding = start === 0 ? "" : before[start - 1];
445
- if (trigger.length === 1 && preceding !== "" && !/\s/.test(preceding) && preceding.charCodeAt(0) < 128) {
446
- return null;
447
- }
448
- return { query, start };
449
- }
450
- function levelItems(items, path) {
451
- let current = items;
452
- for (const step of path) {
453
- const next = current.find((item) => item.value === step)?.children;
454
- if (!next) return current;
455
- current = next;
456
- }
457
- return current;
458
- }
459
- function labelOf(item) {
460
- return item.label ?? item.value;
461
- }
462
- function matches(item, query) {
463
- if (!query) return true;
464
- const needle = query.toLocaleLowerCase();
465
- return labelOf(item).toLocaleLowerCase().includes(needle) || item.value.toLocaleLowerCase().includes(needle) || (item.description?.toLocaleLowerCase().includes(needle) ?? false);
466
- }
467
- function ChatSuggestion({
468
- items,
469
- onValueChange,
470
- triggerCharacter = "/",
471
- open,
472
- defaultOpen,
473
- onOpenChange,
474
- children,
475
- emptyMessage,
476
- listLabel: listLabelProp,
477
- id,
478
- className,
479
- allowSpaces = false,
480
- terminator,
481
- shouldFilter = true,
482
- onQueryChange
483
- }) {
484
- const { t } = useTranslation3();
485
- const [uncontrolledOpen, setUncontrolledOpen] = React3.useState(defaultOpen ?? false);
486
- const isOpen = open ?? uncontrolledOpen;
487
- const [query, setQuery] = React3.useState("");
488
- const [path, setPath] = React3.useState([]);
489
- const [activeValue, setActiveValue] = React3.useState("");
490
- const [activeId, setActiveId] = React3.useState(void 0);
491
- const anchorRef = React3.useRef(null);
492
- const contentRef = React3.useRef(null);
493
- const [listId, setListId] = React3.useState(void 0);
494
- const field = React3.useCallback(() => anchorRef.current?.querySelector("textarea") ?? null, []);
495
- const openRef = React3.useRef(isOpen);
496
- openRef.current = isOpen;
497
- const setOpen = React3.useCallback(
498
- (next) => {
499
- if (openRef.current === next) return;
500
- openRef.current = next;
501
- if (open === void 0) setUncontrolledOpen(next);
502
- onOpenChange?.(next);
503
- if (!next) {
504
- setQuery("");
505
- setPath([]);
506
- }
507
- },
508
- [open, onOpenChange]
509
- );
510
- const visible = React3.useMemo(() => {
511
- const level = levelItems(items, path);
512
- return shouldFilter ? level.filter((item) => matches(item, query)) : level;
513
- }, [items, path, query, shouldFilter]);
514
- React3.useEffect(() => {
515
- if (isOpen) onQueryChange?.(query);
516
- }, [isOpen, query, onQueryChange]);
517
- const enabled = React3.useMemo(() => visible.filter((item) => !item.disabled), [visible]);
518
- React3.useEffect(() => {
519
- if (enabled.length === 0) {
520
- setActiveValue("");
521
- return;
522
- }
523
- setActiveValue(
524
- (current) => enabled.some((item) => item.value === current) ? current : enabled[0].value
525
- );
526
- }, [enabled]);
527
- React3.useEffect(() => {
528
- if (!isOpen) {
529
- setListId(void 0);
530
- setActiveId(void 0);
531
- return;
532
- }
533
- const content = contentRef.current;
534
- setListId(content?.querySelector("[cmdk-list]")?.id || void 0);
535
- setActiveId(content?.querySelector('[cmdk-item][aria-selected="true"]')?.id || void 0);
536
- });
537
- const dismissedStartRef = React3.useRef(null);
538
- const close = React3.useCallback(() => {
539
- const node = field();
540
- const caret = node ? node.selectionStart ?? node.value.length : 0;
541
- const hit = node ? matchTrigger(node.value, caret, triggerCharacter, { allowSpaces, terminator }) : null;
542
- dismissedStartRef.current = hit ? hit.start : null;
543
- setOpen(false);
544
- field()?.focus();
545
- }, [field, setOpen, triggerCharacter, allowSpaces, terminator]);
546
- const evaluate = React3.useCallback(() => {
547
- const node = field();
548
- if (!node) return;
549
- const caret = node.selectionStart ?? node.value.length;
550
- const hit = matchTrigger(node.value, caret, triggerCharacter, { allowSpaces, terminator });
551
- if (!hit) {
552
- dismissedStartRef.current = null;
553
- setOpen(false);
554
- return;
555
- }
556
- if (hit.start === dismissedStartRef.current) {
557
- setOpen(false);
558
- return;
559
- }
560
- dismissedStartRef.current = null;
561
- setQuery(hit.query);
562
- setOpen(true);
563
- }, [field, setOpen, triggerCharacter, allowSpaces, terminator]);
564
- const onTrigger = React3.useCallback(
565
- (next) => {
566
- if (next === false) {
567
- setOpen(false);
568
- return;
569
- }
570
- if (next === void 0) {
571
- setQuery("");
572
- setOpen(true);
573
- return;
574
- }
575
- window.setTimeout(evaluate, 0);
576
- },
577
- [evaluate, setOpen]
578
- );
579
- const pick = React3.useCallback(
580
- (item) => {
581
- if (item.disabled) return;
582
- if (item.children && item.children.length > 0) {
583
- setPath((current) => [...current, item.value]);
584
- setQuery("");
585
- field()?.focus();
586
- return;
587
- }
588
- onValueChange?.(item.value);
589
- setOpen(false);
590
- field()?.focus();
591
- },
592
- [field, onValueChange, setOpen]
593
- );
594
- const move = React3.useCallback(
595
- (direction) => {
596
- if (enabled.length === 0) return;
597
- const index = enabled.findIndex((item) => item.value === activeValue);
598
- const from = index === -1 ? direction === 1 ? -1 : 0 : index;
599
- const next = (from + direction + enabled.length) % enabled.length;
600
- setActiveValue(enabled[next].value);
601
- },
602
- [activeValue, enabled]
603
- );
604
- const onKeyDown = React3.useCallback(
605
- (event) => {
606
- if (isImeComposing2(event)) return;
607
- if (!openRef.current) {
608
- if (event.key.startsWith("Arrow") || event.key === "Home" || event.key === "End") {
609
- window.setTimeout(evaluate, 0);
610
- }
611
- return;
612
- }
613
- switch (event.key) {
614
- case "ArrowDown":
615
- event.preventDefault();
616
- move(1);
617
- return;
618
- case "ArrowUp":
619
- event.preventDefault();
620
- move(-1);
621
- return;
622
- case "Enter":
623
- case "Tab": {
624
- const item = enabled.find((candidate) => candidate.value === activeValue);
625
- if (!item) return;
626
- event.preventDefault();
627
- pick(item);
628
- return;
629
- }
630
- case "Escape":
631
- event.preventDefault();
632
- event.stopPropagation();
633
- close();
634
- return;
635
- default:
636
- if (event.key.startsWith("Arrow") || event.key === "Home" || event.key === "End") {
637
- window.setTimeout(evaluate, 0);
638
- }
639
- }
640
- },
641
- [activeValue, close, enabled, evaluate, move, pick]
642
- );
643
- const listLabel = listLabelProp ?? t("dataEntry.chatSuggestion.label");
644
- return /* @__PURE__ */ jsxs3(Popover, { open: isOpen, onOpenChange: setOpen, children: [
645
- /* @__PURE__ */ jsxs3(
646
- PopoverAnchor,
647
- {
648
- ref: anchorRef,
649
- id,
650
- "data-slot": "chat-suggestion",
651
- className: cn3("block w-full", className),
652
- onPointerUp: () => window.setTimeout(evaluate, 0),
653
- onBlurCapture: (event) => {
654
- const next = event.relatedTarget;
655
- if (next && anchorRef.current?.contains(next)) return;
656
- if (openRef.current) setOpen(false);
657
- },
658
- children: [
659
- children({
660
- onTrigger,
661
- onKeyDown
662
- }),
663
- /* @__PURE__ */ jsx3(FieldPopupWiring, { field, open: isOpen, listId, activeId }),
664
- /* @__PURE__ */ jsx3(VisuallyHidden2, { role: "status", "aria-live": "polite", children: isOpen ? t("dataEntry.chatSuggestion.count", { count: visible.length }) : "" })
665
- ]
666
- }
667
- ),
668
- /* @__PURE__ */ jsx3(
669
- PopoverContent,
670
- {
671
- ref: contentRef,
672
- flush: true,
673
- align: "start",
674
- "aria-label": listLabel,
675
- className: "ui-chat-suggestion-panel",
676
- onOpenAutoFocus: (event) => event.preventDefault(),
677
- onCloseAutoFocus: (event) => event.preventDefault(),
678
- children: /* @__PURE__ */ jsx3(
679
- Command,
680
- {
681
- value: activeValue,
682
- onValueChange: setActiveValue,
683
- shouldFilter: false,
684
- label: listLabel,
685
- children: /* @__PURE__ */ jsxs3(CommandList, { label: listLabel, children: [
686
- /* @__PURE__ */ jsx3(CommandEmpty, { children: emptyMessage ?? t("dataEntry.chatSuggestion.empty") }),
687
- visible.map((item) => /* @__PURE__ */ jsxs3(
688
- CommandItem,
689
- {
690
- value: item.value,
691
- disabled: item.disabled,
692
- onSelect: () => pick(item),
693
- children: [
694
- item.icon ? /* @__PURE__ */ jsx3("span", { className: "flex shrink-0 items-center", "aria-hidden": "true", children: item.icon }) : null,
695
- /* @__PURE__ */ jsxs3("span", { className: "ui-chat-suggestion-item-text", children: [
696
- /* @__PURE__ */ jsx3("span", { className: "truncate", children: labelOf(item) }),
697
- item.description ? /* @__PURE__ */ jsx3("span", { className: "text-muted-foreground truncate text-xs", children: item.description }) : null
698
- ] })
699
- ]
700
- },
701
- item.value
702
- ))
703
- ] })
704
- }
705
- )
706
- }
707
- )
708
- ] });
709
- }
710
- ChatSuggestion.displayName = "ChatSuggestion";
711
- function FieldPopupWiring({
712
- field,
713
- open,
714
- listId,
715
- activeId
716
- }) {
717
- React3.useEffect(() => {
718
- const node = field();
719
- if (!node) return;
720
- node.setAttribute("aria-haspopup", "listbox");
721
- node.setAttribute("aria-autocomplete", "list");
722
- if (open && listId) {
723
- node.setAttribute("aria-controls", listId);
724
- if (activeId) node.setAttribute("aria-activedescendant", activeId);
725
- else node.removeAttribute("aria-activedescendant");
726
- } else {
727
- node.removeAttribute("aria-controls");
728
- node.removeAttribute("aria-activedescendant");
729
- }
730
- }, [activeId, field, listId, open]);
731
- return null;
732
- }
733
-
734
- // src/attachments.tsx
735
- import * as React4 from "react";
736
- import { FileIcon, ImagePlus, Plus, X } from "lucide-react";
737
- import { useTranslation as useTranslation4 } from "@godxjp/ui/i18n";
738
- import { formatBytes } from "@godxjp/ui/admin";
739
- import { cn as cn4 } from "@godxjp/ui/lib/utils";
740
- import { Button as Button3 } from "@godxjp/ui/general";
741
- import { Progress } from "@godxjp/ui/data-display";
742
- import { createUploadItem } from "@godxjp/ui/data-entry";
743
- import { readDroppedFiles } from "@godxjp/ui/data-entry";
744
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
745
- var AttachmentContext = React4.createContext({ disabled: false });
746
- function toAttachmentItem(item) {
747
- const status = item.status === "uploading" ? "uploading" : item.status === "done" ? "done" : item.status === "error" ? "error" : item.status === "removed" ? "removed" : void 0;
748
- return {
749
- uid: item.uid,
750
- name: item.name,
751
- size: item.size,
752
- status,
753
- percent: item.percent,
754
- url: item.url,
755
- thumbUrl: item.previewUrl,
756
- originFileObj: item.file,
757
- response: item.response,
758
- error: item.error
759
- };
760
- }
761
- function resolvePlaceholder(placeholder, type) {
762
- if (!placeholder) return {};
763
- return typeof placeholder === "function" ? placeholder(type) : placeholder;
764
- }
765
- function splitFileName(name) {
766
- const match = /^(.*)\.[^.]+$/.exec(name ?? "");
767
- return match ? [match[1], (name ?? "").slice(match[1].length)] : [name ?? "", ""];
768
- }
769
- function fileMatchesAccept(file, accept) {
770
- if (!accept) return true;
771
- return accept.split(",").some((rule) => {
772
- const trimmed = rule.trim().toLowerCase();
773
- if (trimmed.endsWith("/*")) return file.type.toLowerCase().startsWith(trimmed.slice(0, -1));
774
- return file.type.toLowerCase() === trimmed || trimmed.startsWith(".") && file.name.toLowerCase().endsWith(trimmed);
775
- });
776
- }
777
- function PlaceholderUploader({
778
- type,
779
- placeholder,
780
- onOpen,
781
- onFiles,
782
- className,
783
- style,
784
- hidden
785
- }) {
786
- const { t } = useTranslation4();
787
- const { disabled } = React4.useContext(AttachmentContext);
788
- const [dragActive, setDragActive] = React4.useState(false);
789
- if (hidden) return null;
790
- const title = placeholder.title ?? t("dataEntry.attachments.placeholderTitle");
791
- const description = placeholder.description ?? t("dataEntry.attachments.placeholderDescription");
792
- const icon = placeholder.icon ?? /* @__PURE__ */ jsx4(ImagePlus, { "aria-hidden": "true" });
793
- return /* @__PURE__ */ jsxs4(
794
- "div",
795
- {
796
- className: cn4("ui-attachments-placeholder", className),
797
- style,
798
- "data-drag-active": dragActive ? "true" : void 0,
799
- "data-disabled": disabled ? "true" : void 0,
800
- "aria-hidden": disabled ? true : void 0,
801
- onDragEnter: (event) => {
802
- event.preventDefault();
803
- if (!disabled) setDragActive(true);
804
- },
805
- onDragOver: (event) => event.preventDefault(),
806
- onDragLeave: (event) => {
807
- if (!event.currentTarget.contains(event.relatedTarget)) setDragActive(false);
808
- },
809
- onDrop: async (event) => {
810
- event.preventDefault();
811
- event.stopPropagation();
812
- setDragActive(false);
813
- if (disabled) return;
814
- const files = await readDroppedFiles(event.dataTransfer, false);
815
- if (files.length) onFiles(files);
816
- },
817
- onClick: () => {
818
- if (!disabled) onOpen();
819
- },
820
- onKeyDown: (event) => {
821
- if (disabled) return;
822
- if (event.key === "Enter" || event.key === " ") {
823
- event.preventDefault();
824
- onOpen();
825
- }
826
- },
827
- role: "button",
828
- tabIndex: disabled ? -1 : 0,
829
- "aria-label": String(title),
830
- children: [
831
- /* @__PURE__ */ jsx4("span", { className: "ui-attachments-placeholder-icon", children: icon }),
832
- title ? /* @__PURE__ */ jsx4("span", { className: "ui-attachments-placeholder-title", children: title }) : null,
833
- description ? /* @__PURE__ */ jsx4("span", { className: "ui-attachments-placeholder-description", children: description }) : null,
834
- /* @__PURE__ */ jsx4("span", { className: "sr-only", children: t("dataEntry.attachments.dropLabel", { type }) })
835
- ]
836
- }
837
- );
838
- }
839
- function AttachmentCard({
840
- item,
841
- description,
842
- onRemove,
843
- classNames
844
- }) {
845
- const { t } = useTranslation4();
846
- const { disabled } = React4.useContext(AttachmentContext);
847
- const preview = item.thumbUrl ?? item.url;
848
- const [namePrefix, nameSuffix] = splitFileName(item.name);
849
- 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) : "");
850
- return /* @__PURE__ */ jsxs4(
851
- "li",
852
- {
853
- className: cn4("ui-attachments-card", classNames?.card, classNames?.file),
854
- "data-status": item.status,
855
- children: [
856
- preview ? /* @__PURE__ */ jsx4("img", { src: preview, alt: "", className: "ui-attachments-card-preview", "aria-hidden": "true" }) : /* @__PURE__ */ jsx4("span", { className: "ui-attachments-card-placeholder", "aria-hidden": "true", children: /* @__PURE__ */ jsx4(FileIcon, {}) }),
857
- /* @__PURE__ */ jsxs4("span", { className: "ui-attachments-card-content", children: [
858
- /* @__PURE__ */ jsxs4("span", { className: "ui-attachments-card-name", title: item.name, children: [
859
- /* @__PURE__ */ jsx4("span", { className: "ui-attachments-card-name-prefix", children: namePrefix }),
860
- /* @__PURE__ */ jsx4("span", { className: "ui-attachments-card-name-suffix", children: nameSuffix })
861
- ] }),
862
- desc ? /* @__PURE__ */ jsx4(
863
- "span",
864
- {
865
- className: "ui-attachments-card-meta",
866
- title: typeof desc === "string" ? desc : void 0,
867
- children: desc
868
- }
869
- ) : null
870
- ] }),
871
- item.status === "uploading" && item.percent != null ? /* @__PURE__ */ jsx4("span", { className: "ui-attachments-card-progress", children: /* @__PURE__ */ jsx4(
872
- Progress,
873
- {
874
- size: "sm",
875
- value: item.percent,
876
- "aria-label": t("dataEntry.attachments.uploading")
877
- }
878
- ) }) : null,
879
- !disabled && onRemove ? /* @__PURE__ */ jsx4(
880
- Button3,
881
- {
882
- type: "button",
883
- variant: "ghost",
884
- size: "icon-xs",
885
- className: "ui-attachments-card-remove",
886
- "aria-label": t("dataEntry.attachments.remove", { name: item.name }),
887
- onClick: (event) => {
888
- event.stopPropagation();
889
- onRemove();
890
- },
891
- children: /* @__PURE__ */ jsx4(X, { "aria-hidden": "true" })
892
- }
893
- ) : null
894
- ]
895
- }
896
- );
897
- }
898
- var Attachments = React4.forwardRef(
899
- ({
900
- className,
901
- rootClassName,
902
- classNames = {},
903
- styles = {},
904
- items: controlledItems,
905
- onChange,
906
- onRemove,
907
- overflow,
908
- placeholder,
909
- getDropContainer,
910
- disabled = false,
911
- maxCount,
912
- accept,
913
- multiple,
914
- children
915
- }, ref) => {
916
- const { t } = useTranslation4();
917
- const containerRef = React4.useRef(null);
918
- const inputRef = React4.useRef(null);
919
- const [internalItems, setInternalItems] = React4.useState([]);
920
- const [dropActive, setDropActive] = React4.useState(false);
921
- const items = controlledItems ?? internalItems;
922
- const setItems = React4.useCallback(
923
- (next, file) => {
924
- if (controlledItems === void 0) setInternalItems(next);
925
- onChange?.({ file, fileList: next });
926
- },
927
- [controlledItems, onChange]
928
- );
929
- const openPicker = React4.useCallback(() => {
930
- inputRef.current?.click();
931
- }, []);
932
- const dispatchFiles = React4.useCallback(
933
- (files) => {
934
- const accepted = files.filter((file) => fileMatchesAccept(file, accept));
935
- if (!accepted.length) return;
936
- const room = maxCount == null ? accepted.length : Math.max(0, maxCount - items.length);
937
- const picked = accepted.slice(0, room);
938
- const next = [
939
- ...items,
940
- ...picked.map((file) => {
941
- const uploadItem = createUploadItem(file);
942
- return toAttachmentItem(uploadItem);
943
- })
944
- ];
945
- const last = next[next.length - 1];
946
- if (last) setItems(next, last);
947
- },
948
- [accept, items, maxCount, setItems]
949
- );
950
- React4.useImperativeHandle(ref, () => ({
951
- nativeElement: containerRef.current,
952
- fileNativeElement: inputRef.current,
953
- upload: (file) => dispatchFiles([file]),
954
- select: (options) => {
955
- const input = inputRef.current;
956
- if (!input) return;
957
- input.multiple = options?.multiple ?? false;
958
- input.accept = options?.accept ?? accept ?? "";
959
- input.click();
960
- }
961
- }));
962
- const handleRemove = async (item) => {
963
- const ret = await Promise.resolve(typeof onRemove === "function" ? onRemove(item) : onRemove);
964
- if (ret === false) return;
965
- const next = items.filter((entry) => entry.uid !== item.uid);
966
- setItems(next, { ...item, status: "removed" });
967
- };
968
- const showExtension = !disabled && (maxCount == null || maxCount > items.length) && items.length > 0;
969
- const inlinePlaceholder = resolvePlaceholder(placeholder, "inline");
970
- const dropPlaceholder = resolvePlaceholder(placeholder, "drop");
971
- const hasFiles = items.length > 0;
972
- const fileInput = /* @__PURE__ */ jsx4(
973
- "input",
974
- {
975
- ref: inputRef,
976
- type: "file",
977
- className: cn4("ui-attachments-input", "sr-only"),
978
- accept,
979
- multiple: multiple ?? (maxCount == null || maxCount > 1),
980
- disabled,
981
- "aria-label": t("dataEntry.attachments.inputLabel"),
982
- onChange: (event) => {
983
- const files = event.target.files ? Array.from(event.target.files) : [];
984
- event.target.value = "";
985
- dispatchFiles(files);
986
- }
987
- }
988
- );
989
- if (children) {
990
- return /* @__PURE__ */ jsxs4(AttachmentContext.Provider, { value: { disabled }, children: [
991
- fileInput,
992
- /* @__PURE__ */ jsx4(
993
- "div",
994
- {
995
- ref: containerRef,
996
- className: cn4("ui-attachments", rootClassName, classNames.root),
997
- style: styles.root,
998
- children
999
- }
1000
- ),
1001
- /* @__PURE__ */ jsx4(
1002
- PlaceholderUploader,
1003
- {
1004
- type: "drop",
1005
- placeholder: dropPlaceholder,
1006
- onOpen: openPicker,
1007
- onFiles: dispatchFiles,
1008
- className: classNames.placeholder,
1009
- style: styles.placeholder
1010
- }
1011
- )
1012
- ] });
1013
- }
1014
- const dropScope = getDropContainer?.() ? "viewport" : "control";
1015
- return /* @__PURE__ */ jsx4(AttachmentContext.Provider, { value: { disabled }, children: /* @__PURE__ */ jsxs4(
1016
- "div",
1017
- {
1018
- ref: containerRef,
1019
- className: cn4("ui-attachments", className, rootClassName, classNames.root),
1020
- style: styles.root,
1021
- dir: "auto",
1022
- onDragEnter: (event) => {
1023
- event.preventDefault();
1024
- if (!disabled) setDropActive(true);
1025
- },
1026
- onDragOver: (event) => event.preventDefault(),
1027
- onDragLeave: (event) => {
1028
- if (!event.currentTarget.contains(event.relatedTarget)) setDropActive(false);
1029
- },
1030
- onDrop: async (event) => {
1031
- event.preventDefault();
1032
- setDropActive(false);
1033
- if (disabled) return;
1034
- const files = await readDroppedFiles(event.dataTransfer, false);
1035
- if (files.length) dispatchFiles(files);
1036
- },
1037
- children: [
1038
- fileInput,
1039
- hasFiles ? /* @__PURE__ */ jsxs4(
1040
- "ul",
1041
- {
1042
- className: cn4("ui-attachments-list", classNames.list),
1043
- style: styles.list,
1044
- "data-overflow": overflow ?? "wrap",
1045
- children: [
1046
- items.map((item) => /* @__PURE__ */ jsx4(
1047
- AttachmentCard,
1048
- {
1049
- item,
1050
- onRemove: () => handleRemove(item),
1051
- classNames
1052
- },
1053
- item.uid
1054
- )),
1055
- showExtension ? /* @__PURE__ */ jsx4("li", { children: /* @__PURE__ */ jsx4(
1056
- "button",
1057
- {
1058
- type: "button",
1059
- className: cn4("ui-attachments-upload-btn", classNames.upload),
1060
- style: styles.upload,
1061
- disabled,
1062
- "aria-label": t("dataEntry.attachments.add"),
1063
- onClick: openPicker,
1064
- children: /* @__PURE__ */ jsx4(Plus, { "aria-hidden": "true" })
1065
- }
1066
- ) }) : null
1067
- ]
1068
- }
1069
- ) : null,
1070
- /* @__PURE__ */ jsx4(
1071
- PlaceholderUploader,
1072
- {
1073
- type: "inline",
1074
- placeholder: inlinePlaceholder,
1075
- onOpen: openPicker,
1076
- onFiles: dispatchFiles,
1077
- className: classNames.placeholder,
1078
- style: hasFiles ? { display: "none" } : styles.placeholder,
1079
- hidden: hasFiles
1080
- }
1081
- ),
1082
- /* @__PURE__ */ jsx4(
1083
- "div",
1084
- {
1085
- className: "ui-attachments-drop-layer",
1086
- "data-scope": dropScope,
1087
- "data-active": dropActive ? "true" : void 0,
1088
- "aria-hidden": "true"
1089
- }
1090
- )
1091
- ]
1092
- }
1093
- ) });
1094
- }
1095
- );
1096
- Attachments.displayName = "Attachments";
1097
-
1098
- // src/conversations.tsx
1099
- import * as React5 from "react";
1100
- import { ChevronDown, MoreHorizontal, Plus as Plus2 } from "lucide-react";
1101
- import { useTranslation as useTranslation5 } from "@godxjp/ui/i18n";
1102
- import { formatDate } from "@godxjp/ui/datetime";
1103
- import { cn as cn5 } from "@godxjp/ui/lib/utils";
1104
- import { Button as Button4 } from "@godxjp/ui/general";
1105
- import { Text as Text2 } from "@godxjp/ui/general";
1106
- import { Separator } from "@godxjp/ui/layout";
1107
2
  import {
1108
- DropdownMenu,
1109
- DropdownMenuContent,
1110
- DropdownMenuItem,
1111
- DropdownMenuTrigger
1112
- } from "@godxjp/ui/navigation";
1113
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1114
- function isDivider(entry) {
1115
- return "type" in entry && entry.type === "divider";
1116
- }
1117
- function isRtl(element) {
1118
- return element?.closest("[dir]")?.getAttribute("dir")?.toLowerCase() === "rtl";
1119
- }
1120
- function toSections(items, groupable) {
1121
- const collapsibleOption = typeof groupable === "object" ? groupable.collapsible : void 0;
1122
- const sections = [];
1123
- for (const entry of items) {
1124
- const group = !groupable || isDivider(entry) ? void 0 : entry.group;
1125
- if (group === void 0) {
1126
- const tail = sections[sections.length - 1];
1127
- if (tail && tail.group === void 0) {
1128
- tail.entries.push(entry);
1129
- continue;
1130
- }
1131
- sections.push({ entries: [entry], collapsible: false });
1132
- continue;
1133
- }
1134
- const existing = sections.find((section) => section.group === group);
1135
- if (existing) {
1136
- existing.entries.push(entry);
1137
- continue;
1138
- }
1139
- sections.push({
1140
- group,
1141
- entries: [entry],
1142
- collapsible: typeof collapsibleOption === "function" ? collapsibleOption(group) : collapsibleOption ?? false
1143
- });
1144
- }
1145
- return sections;
1146
- }
1147
- var groupNavKey = (group) => `group:${group}`;
1148
- var rowNavKey = (key) => `row:${key}`;
1149
- var Conversations = React5.forwardRef(
1150
- ({
1151
- items = [],
1152
- activeKey,
1153
- defaultActiveKey,
1154
- onActiveChange,
1155
- menu,
1156
- groupable,
1157
- creation,
1158
- label,
1159
- id,
1160
- className,
1161
- ...rest
1162
- }, ref) => {
1163
- const { t } = useTranslation5();
1164
- const rootRef = React5.useRef(null);
1165
- const setRootRef = React5.useCallback(
1166
- (node) => {
1167
- rootRef.current = node;
1168
- if (typeof ref === "function") ref(node);
1169
- else if (ref) ref.current = node;
1170
- },
1171
- [ref]
1172
- );
1173
- const [uncontrolledActive, setUncontrolledActive] = React5.useState(defaultActiveKey);
1174
- const mergedActiveKey = activeKey ?? uncontrolledActive;
1175
- const sections = React5.useMemo(() => toSections(items, groupable), [items, groupable]);
1176
- const groupOptions = typeof groupable === "object" ? groupable : {};
1177
- const collapsibleGroups = React5.useMemo(
1178
- () => sections.filter((section) => section.collapsible && section.group).map((s) => s.group),
1179
- [sections]
1180
- );
1181
- const [uncontrolledExpanded, setUncontrolledExpanded] = React5.useState(() => [
1182
- ...groupOptions.defaultExpandedKeys ?? collapsibleGroups
1183
- ]);
1184
- const expandedKeys = groupOptions.expandedKeys ? [...groupOptions.expandedKeys] : uncontrolledExpanded;
1185
- const toggleGroup = (group) => {
1186
- const next = expandedKeys.includes(group) ? expandedKeys.filter((key) => key !== group) : [...expandedKeys, group];
1187
- if (!groupOptions.expandedKeys) setUncontrolledExpanded(next);
1188
- groupOptions.onExpand?.(next);
1189
- };
1190
- const navKeys = [];
1191
- for (const section of sections) {
1192
- if (section.group && section.collapsible) {
1193
- navKeys.push(groupNavKey(section.group));
1194
- if (!expandedKeys.includes(section.group)) continue;
1195
- }
1196
- for (const entry of section.entries) {
1197
- if (!isDivider(entry) && !entry.disabled) navKeys.push(rowNavKey(entry.key));
1198
- }
1199
- }
1200
- const [focusKey, setFocusKey] = React5.useState(null);
1201
- const activeNavKey = mergedActiveKey ? rowNavKey(mergedActiveKey) : void 0;
1202
- const tabStop = focusKey && navKeys.includes(focusKey) ? focusKey : activeNavKey && navKeys.includes(activeNavKey) ? activeNavKey : navKeys[0];
1203
- const nodes = React5.useRef(/* @__PURE__ */ new Map());
1204
- const registerNode = (key) => (node) => {
1205
- if (node) nodes.current.set(key, node);
1206
- else nodes.current.delete(key);
1207
- };
1208
- const moveTo = (key) => {
1209
- if (!key) return;
1210
- setFocusKey(key);
1211
- nodes.current.get(key)?.focus();
1212
- };
1213
- const step = (from, delta) => {
1214
- if (navKeys.length === 0) return;
1215
- const index = navKeys.indexOf(from);
1216
- const next = index === -1 ? 0 : (index + delta + navKeys.length) % navKeys.length;
1217
- moveTo(navKeys[next]);
1218
- };
1219
- const handleNavKeyDown = (navKey) => (event) => {
1220
- const rtl = isRtl(event.currentTarget);
1221
- const forward = rtl ? "ArrowLeft" : "ArrowRight";
1222
- const backward = rtl ? "ArrowRight" : "ArrowLeft";
1223
- switch (event.key) {
1224
- case "ArrowDown":
1225
- event.preventDefault();
1226
- step(navKey, 1);
1227
- return;
1228
- case "ArrowUp":
1229
- event.preventDefault();
1230
- step(navKey, -1);
1231
- return;
1232
- case "Home":
1233
- event.preventDefault();
1234
- moveTo(navKeys[0]);
1235
- return;
1236
- case "End":
1237
- event.preventDefault();
1238
- moveTo(navKeys[navKeys.length - 1]);
1239
- return;
1240
- case forward: {
1241
- const trigger = nodes.current.get(`menu:${navKey}`);
1242
- if (!trigger) return;
1243
- event.preventDefault();
1244
- trigger.focus();
1245
- return;
1246
- }
1247
- case backward: {
1248
- if (!navKey.startsWith("menu:")) return;
1249
- event.preventDefault();
1250
- moveTo(navKey.slice("menu:".length));
1251
- return;
1252
- }
1253
- default:
1254
- }
1255
- };
1256
- const selectConversation = (item) => {
1257
- if (item.disabled) return;
1258
- if (activeKey === void 0) setUncontrolledActive(item.key);
1259
- onActiveChange?.(
1260
- item.key,
1261
- items.find((entry) => !isDivider(entry) && entry.key === item.key)
1262
- );
1263
- };
1264
- const menuFor = (item) => {
1265
- const resolved = typeof menu === "function" ? menu(item) : menu;
1266
- return resolved && resolved.items.length > 0 ? resolved : void 0;
1267
- };
1268
- const renderEntry = (entry, index) => {
1269
- if (isDivider(entry)) {
1270
- return /* @__PURE__ */ jsx5("li", { className: "ui-conversations-rule", children: /* @__PURE__ */ jsx5(
1271
- Separator,
1272
- {
1273
- "data-dashed": entry.dashed ? "true" : void 0,
1274
- className: "ui-conversations-separator"
1275
- }
1276
- ) }, entry.key ?? `divider-${index}`);
1277
- }
1278
- const navKey = rowNavKey(entry.key);
1279
- const active = mergedActiveKey === entry.key;
1280
- const rowMenu = entry.disabled ? void 0 : menuFor(entry);
1281
- const labelText = typeof entry.label === "string" ? entry.label : entry.key;
1282
- return /* @__PURE__ */ jsxs5(
1283
- "li",
1284
- {
1285
- "data-slot": "conversations-item",
1286
- "data-active": active ? "true" : void 0,
1287
- className: "ui-conversations-item",
1288
- children: [
1289
- /* @__PURE__ */ jsxs5(
1290
- Button4,
1291
- {
1292
- ref: registerNode(navKey),
1293
- type: "button",
1294
- variant: "ghost",
1295
- fullWidth: true,
1296
- align: "start",
1297
- disabled: entry.disabled,
1298
- "aria-current": active ? "true" : void 0,
1299
- tabIndex: tabStop === navKey ? 0 : -1,
1300
- className: "ui-conversations-row",
1301
- onFocus: () => setFocusKey(navKey),
1302
- onClick: () => selectConversation(entry),
1303
- onKeyDown: handleNavKeyDown(navKey),
1304
- children: [
1305
- entry.icon ? /* @__PURE__ */ jsx5("span", { className: "ui-conversations-icon", "aria-hidden": "true", children: entry.icon }) : null,
1306
- /* @__PURE__ */ jsxs5("span", { className: "ui-conversations-line", children: [
1307
- /* @__PURE__ */ jsx5(Text2, { truncate: true, children: entry.label }),
1308
- entry.timestamp === void 0 ? null : (
1309
- /*
1310
- * Through `formatDate`, never through `toLocaleString` or a sliced ISO string.
1311
- * `docs/DATETIME.md` rule 1 forbids the alternatives for a measured reason: the
1312
- * tenant's locale, timezone and 12/24h choice live in AppProvider, so the same
1313
- * epoch has to read 2026/03/15 in a Japanese tenant and 15/03/2026 in a
1314
- * Vietnamese one. A component that formats its own dates cannot do that.
1315
- */
1316
- /* @__PURE__ */ jsx5(Text2, { size: "xs", tone: "muted", tabular: true, className: "ui-conversations-timestamp", children: formatDate(new Date(entry.timestamp)) })
1317
- )
1318
- ] })
1319
- ]
1320
- }
1321
- ),
1322
- rowMenu ? /* @__PURE__ */ jsxs5(DropdownMenu, { children: [
1323
- /* @__PURE__ */ jsx5(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx5(
1324
- Button4,
1325
- {
1326
- ref: registerNode(`menu:${navKey}`),
1327
- type: "button",
1328
- variant: "ghost",
1329
- size: "icon-sm",
1330
- tabIndex: -1,
1331
- className: "ui-conversations-menu-trigger",
1332
- "aria-label": rowMenu.triggerLabel?.(entry) ?? t("navigation.conversations.rowActions", { label: labelText }),
1333
- onKeyDown: handleNavKeyDown(`menu:${navKey}`),
1334
- children: /* @__PURE__ */ jsx5(MoreHorizontal, { "aria-hidden": "true" })
1335
- }
1336
- ) }),
1337
- /* @__PURE__ */ jsx5(DropdownMenuContent, { align: "end", children: rowMenu.items.map((command) => /* @__PURE__ */ jsxs5(
1338
- DropdownMenuItem,
1339
- {
1340
- disabled: command.disabled,
1341
- variant: command.danger ? "destructive" : void 0,
1342
- onSelect: () => rowMenu.onClick?.({ key: command.key, conversation: entry }),
1343
- children: [
1344
- command.icon,
1345
- command.label
1346
- ]
1347
- },
1348
- command.key
1349
- )) })
1350
- ] }) : null
1351
- ]
1352
- },
1353
- entry.key
1354
- );
1355
- };
1356
- const railLabel = label ?? t("navigation.conversations.label");
1357
- return /* @__PURE__ */ jsxs5(
1358
- "div",
1359
- {
1360
- ref: setRootRef,
1361
- id,
1362
- "data-slot": "conversations",
1363
- className: cn5("ui-conversations", className),
1364
- ...rest,
1365
- children: [
1366
- creation ? /* @__PURE__ */ jsx5(CreationButton, { creation }) : null,
1367
- sections.map((section, sectionIndex) => {
1368
- if (!section.group) {
1369
- return /* @__PURE__ */ jsx5(
1370
- "ul",
1371
- {
1372
- "aria-label": railLabel,
1373
- className: "ui-conversations-list",
1374
- children: section.entries.map(renderEntry)
1375
- },
1376
- `run-${sectionIndex}`
1377
- );
1378
- }
1379
- const heading = typeof groupOptions.label === "function" ? groupOptions.label(section.group) : groupOptions.label ?? section.group;
1380
- const navKey = groupNavKey(section.group);
1381
- const expanded = expandedKeys.includes(section.group);
1382
- const listId = `${id ?? "ui-conversations"}-${section.group}`;
1383
- return /* @__PURE__ */ jsxs5(
1384
- "div",
1385
- {
1386
- "data-slot": "conversations-group",
1387
- className: "ui-conversations-group",
1388
- children: [
1389
- section.collapsible ? /* @__PURE__ */ jsxs5(
1390
- Button4,
1391
- {
1392
- ref: registerNode(navKey),
1393
- type: "button",
1394
- variant: "ghost",
1395
- size: "sm",
1396
- fullWidth: true,
1397
- align: "start",
1398
- "aria-expanded": expanded,
1399
- "aria-controls": listId,
1400
- tabIndex: tabStop === navKey ? 0 : -1,
1401
- className: "ui-conversations-group-trigger",
1402
- onFocus: () => setFocusKey(navKey),
1403
- onClick: () => toggleGroup(section.group),
1404
- onKeyDown: handleNavKeyDown(navKey),
1405
- children: [
1406
- /* @__PURE__ */ jsx5(
1407
- ChevronDown,
1408
- {
1409
- "aria-hidden": "true",
1410
- className: "ui-conversations-group-chevron",
1411
- "data-expanded": expanded ? "true" : void 0
1412
- }
1413
- ),
1414
- /* @__PURE__ */ jsx5(Text2, { size: "xs", tone: "muted", weight: "medium", truncate: true, children: heading })
1415
- ]
1416
- }
1417
- ) : /* @__PURE__ */ jsx5(
1418
- Text2,
1419
- {
1420
- size: "xs",
1421
- tone: "muted",
1422
- weight: "medium",
1423
- className: "ui-conversations-group-label",
1424
- children: heading
1425
- }
1426
- ),
1427
- section.collapsible && !expanded ? null : /* @__PURE__ */ jsx5(
1428
- "ul",
1429
- {
1430
- id: listId,
1431
- "aria-label": typeof heading === "string" ? heading : section.group,
1432
- className: "ui-conversations-list",
1433
- children: section.entries.map(renderEntry)
1434
- }
1435
- )
1436
- ]
1437
- },
1438
- section.group
1439
- );
1440
- })
1441
- ]
1442
- }
1443
- );
1444
- }
1445
- );
1446
- Conversations.displayName = "Conversations";
1447
- function CreationButton({ creation }) {
1448
- const { t } = useTranslation5();
1449
- return /* @__PURE__ */ jsxs5(
1450
- Button4,
1451
- {
1452
- type: "button",
1453
- variant: "outline",
1454
- fullWidth: true,
1455
- align: "start",
1456
- disabled: creation.disabled,
1457
- className: "ui-conversations-creation",
1458
- onClick: () => creation.onClick?.(),
1459
- children: [
1460
- creation.icon ?? /* @__PURE__ */ jsx5(Plus2, { "aria-hidden": "true" }),
1461
- creation.label ?? t("navigation.conversations.creation")
1462
- ]
1463
- }
1464
- );
1465
- }
1466
-
1467
- // src/thought-chain.tsx
1468
- import * as React6 from "react";
1469
- import { ChevronRight, CircleCheck, CircleMinus, CircleX, Loader2 } from "lucide-react";
1470
- import { useTranslation as useTranslation6 } from "@godxjp/ui/i18n";
1471
- import { cn as cn6 } from "@godxjp/ui/lib/utils";
1472
- import { VisuallyHidden as VisuallyHidden3 } from "@godxjp/ui/general";
1473
- import { Text as Text3 } from "@godxjp/ui/general";
1474
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1475
- var STATUS_GLYPH = {
1476
- loading: /* @__PURE__ */ jsx6(Loader2, { "aria-hidden": "true", className: "ui-thought-chain-spinner" }),
1477
- success: /* @__PURE__ */ jsx6(CircleCheck, { "aria-hidden": "true" }),
1478
- error: /* @__PURE__ */ jsx6(CircleX, { "aria-hidden": "true" }),
1479
- abort: /* @__PURE__ */ jsx6(CircleMinus, { "aria-hidden": "true" })
1480
- };
1481
- function StatusMark({
1482
- status,
1483
- fallback
1484
- }) {
1485
- const { t } = useTranslation6();
1486
- if (!status) return /* @__PURE__ */ jsx6(Fragment2, { children: fallback });
1487
- return /* @__PURE__ */ jsxs6(Fragment2, { children: [
1488
- STATUS_GLYPH[status],
1489
- /* @__PURE__ */ jsx6(VisuallyHidden3, { children: t(`dataDisplay.thoughtChain.status.${status}`) })
1490
- ] });
1491
- }
1492
- var ThoughtChain = React6.forwardRef(
1493
- ({
1494
- items = [],
1495
- defaultExpandedKeys,
1496
- expandedKeys,
1497
- onExpand,
1498
- line = true,
1499
- label,
1500
- id,
1501
- className,
1502
- ...rest
1503
- }, ref) => {
1504
- const { t } = useTranslation6();
1505
- const reactId = React6.useId();
1506
- const [uncontrolled, setUncontrolled] = React6.useState(() => [
1507
- ...defaultExpandedKeys ?? []
1508
- ]);
1509
- const open = expandedKeys ? [...expandedKeys] : uncontrolled;
1510
- const toggle = (key) => {
1511
- const next = open.includes(key) ? open.filter((k) => k !== key) : [...open, key];
1512
- if (!expandedKeys) setUncontrolled(next);
1513
- onExpand?.(next);
1514
- };
1515
- return /* @__PURE__ */ jsx6(
1516
- "ol",
1517
- {
1518
- ref,
1519
- id,
1520
- "aria-label": label ?? t("dataDisplay.thoughtChain.label"),
1521
- "data-slot": "thought-chain",
1522
- "data-line": line === false ? "none" : line === true ? "solid" : line,
1523
- className: cn6("ui-thought-chain", className),
1524
- ...rest,
1525
- children: items.map((item, index) => {
1526
- const key = item.key ?? `${reactId}-${index}`;
1527
- const bodyId = `${reactId}-body-${index}`;
1528
- const collapsible = Boolean(item.collapsible && item.content);
1529
- const expanded = collapsible ? open.includes(key) : true;
1530
- const glyph = item.icon === false ? null : item.icon ?? index + 1;
1531
- return /* @__PURE__ */ jsxs6(
1532
- "li",
1533
- {
1534
- "data-slot": "thought-chain-node",
1535
- "data-status": item.status,
1536
- className: "ui-thought-chain-node",
1537
- children: [
1538
- glyph === null ? null : /* @__PURE__ */ jsx6(
1539
- "span",
1540
- {
1541
- "data-slot": "thought-chain-icon",
1542
- "data-status": item.status,
1543
- className: "ui-thought-chain-icon",
1544
- children: /* @__PURE__ */ jsx6(StatusMark, { status: item.status, fallback: glyph })
1545
- }
1546
- ),
1547
- /* @__PURE__ */ jsxs6("div", { className: "ui-thought-chain-body", children: [
1548
- /* @__PURE__ */ jsxs6("div", { className: "ui-thought-chain-header", children: [
1549
- collapsible ? /* @__PURE__ */ jsxs6(
1550
- "button",
1551
- {
1552
- type: "button",
1553
- "aria-expanded": expanded,
1554
- "aria-controls": bodyId,
1555
- "data-blink": item.blink ? "" : void 0,
1556
- className: "ui-thought-chain-trigger",
1557
- onClick: () => toggle(key),
1558
- children: [
1559
- /* @__PURE__ */ jsx6(
1560
- ChevronRight,
1561
- {
1562
- "aria-hidden": "true",
1563
- "data-expanded": expanded ? "true" : void 0,
1564
- className: "ui-thought-chain-chevron"
1565
- }
1566
- ),
1567
- /* @__PURE__ */ jsx6(Text3, { weight: "medium", children: item.title })
1568
- ]
1569
- }
1570
- ) : /* @__PURE__ */ jsx6(Text3, { weight: "medium", "data-blink": item.blink ? "" : void 0, children: item.title }),
1571
- item.description ? /* @__PURE__ */ jsx6(Text3, { size: "xs", tone: "muted", children: item.description }) : null
1572
- ] }),
1573
- item.content && (expanded || item.destroyOnHidden === false) ? /* @__PURE__ */ jsx6(
1574
- "div",
1575
- {
1576
- id: bodyId,
1577
- hidden: !expanded,
1578
- "data-blink": item.blink ? "" : void 0,
1579
- className: "ui-thought-chain-content",
1580
- children: item.content
1581
- }
1582
- ) : null,
1583
- item.footer ? /* @__PURE__ */ jsx6("div", { className: "ui-thought-chain-footer", children: item.footer }) : null
1584
- ] })
1585
- ]
1586
- },
1587
- key
1588
- );
1589
- })
1590
- }
1591
- );
1592
- }
1593
- );
1594
- ThoughtChain.displayName = "ThoughtChain";
1595
- var ThoughtChainItem = React6.forwardRef(
1596
- ({
1597
- title,
1598
- description,
1599
- icon,
1600
- status,
1601
- variant = "solid",
1602
- blink,
1603
- disabled,
1604
- onClick,
1605
- id,
1606
- className
1607
- }, ref) => {
1608
- const shared = {
1609
- id,
1610
- "data-slot": "thought-chain-item",
1611
- "data-variant": variant,
1612
- "data-status": status,
1613
- "data-blink": blink ? "" : void 0,
1614
- className: cn6("ui-thought-chain-item", className)
1615
- };
1616
- const inner = /* @__PURE__ */ jsxs6(Fragment2, { children: [
1617
- status || icon ? /* @__PURE__ */ jsx6("span", { "data-status": status, className: "ui-thought-chain-icon", children: /* @__PURE__ */ jsx6(StatusMark, { status, fallback: icon }) }) : null,
1618
- /* @__PURE__ */ jsxs6("span", { className: "ui-thought-chain-item-text", children: [
1619
- title ? /* @__PURE__ */ jsx6(Text3, { weight: "medium", children: title }) : null,
1620
- description ? /* @__PURE__ */ jsx6(Text3, { size: "xs", tone: "muted", children: description }) : null
1621
- ] })
1622
- ] });
1623
- if (!onClick) {
1624
- return /* @__PURE__ */ jsx6("div", { ref, ...shared, children: inner });
1625
- }
1626
- return /* @__PURE__ */ jsx6(
1627
- "button",
1628
- {
1629
- ref,
1630
- type: "button",
1631
- disabled,
1632
- onClick,
1633
- ...shared,
1634
- children: inner
1635
- }
1636
- );
1637
- }
1638
- );
1639
- ThoughtChainItem.displayName = "ThoughtChainItem";
1640
-
1641
- // src/welcome.tsx
1642
- import * as React7 from "react";
1643
- import { cn as cn7 } from "@godxjp/ui/lib/utils";
1644
- import { Heading, Text as Text4 } from "@godxjp/ui/general";
1645
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1646
- function isImageUrl(icon) {
1647
- return typeof icon === "string" && icon.startsWith("http");
1648
- }
1649
- var Welcome = React7.forwardRef(
1650
- ({ icon, title, description, extra, variant = "filled", id, className, ...rest }, ref) => {
1651
- return /* @__PURE__ */ jsxs7(
1652
- "div",
1653
- {
1654
- ref,
1655
- id,
1656
- "data-slot": "welcome",
1657
- "data-variant": variant,
1658
- className: cn7("ui-welcome", className),
1659
- ...rest,
1660
- children: [
1661
- icon ? /* @__PURE__ */ jsx7("div", { "data-slot": "welcome-icon", className: "ui-welcome-icon", "aria-hidden": "true", children: isImageUrl(icon) ? /* @__PURE__ */ jsx7("img", { src: icon, alt: "" }) : icon }) : null,
1662
- /* @__PURE__ */ jsxs7("div", { "data-slot": "welcome-content", className: "ui-welcome-content", children: [
1663
- title || extra ? /* @__PURE__ */ jsxs7("div", { "data-slot": "welcome-title-row", className: "ui-welcome-title-row", children: [
1664
- title ? /* @__PURE__ */ jsx7(Heading, { level: 4, className: "ui-welcome-title", children: title }) : null,
1665
- extra ? /* @__PURE__ */ jsx7("div", { "data-slot": "welcome-extra", className: "ui-welcome-extra", children: extra }) : null
1666
- ] }) : null,
1667
- description ? /* @__PURE__ */ jsx7(Text4, { size: "sm", tone: "muted", className: "ui-welcome-description", children: description }) : null
1668
- ] })
1669
- ]
1670
- }
1671
- );
1672
- }
1673
- );
1674
- Welcome.displayName = "Welcome";
3
+ Attachments
4
+ } from "./chunk-PGCRFJHC.js";
5
+ import {
6
+ ChatBubble,
7
+ ChatBubbleList
8
+ } from "./chunk-XCTRKZCF.js";
9
+ import {
10
+ ChatComposer
11
+ } from "./chunk-4K5JWTV5.js";
12
+ import {
13
+ ChatSuggestion
14
+ } from "./chunk-DEYHFJBA.js";
15
+ import {
16
+ Conversations
17
+ } from "./chunk-IWWOS4BL.js";
18
+ import {
19
+ ThoughtChain,
20
+ ThoughtChainItem
21
+ } from "./chunk-52TWSF6W.js";
22
+ import {
23
+ Welcome
24
+ } from "./chunk-NUCO7NAP.js";
1675
25
  export {
1676
26
  Attachments,
1677
27
  ChatBubble,