tempest-react-sdk 0.35.0 → 0.37.0
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/README.md +72 -70
- package/dist/audio/audio-output.cjs +2 -0
- package/dist/audio/audio-output.cjs.map +1 -0
- package/dist/audio/audio-output.js +17 -0
- package/dist/audio/audio-output.js.map +1 -0
- package/dist/audio/audio-player.cjs +1 -1
- package/dist/audio/audio-player.cjs.map +1 -1
- package/dist/audio/audio-player.js +21 -20
- package/dist/audio/audio-player.js.map +1 -1
- package/dist/audio/audio-recorder.cjs +2 -0
- package/dist/audio/audio-recorder.cjs.map +1 -0
- package/dist/audio/audio-recorder.js +30 -0
- package/dist/audio/audio-recorder.js.map +1 -0
- package/dist/audio/duration.cjs +2 -0
- package/dist/audio/duration.cjs.map +1 -0
- package/dist/audio/duration.js +10 -0
- package/dist/audio/duration.js.map +1 -0
- package/dist/audio/level-meter.cjs +2 -0
- package/dist/audio/level-meter.cjs.map +1 -0
- package/dist/audio/level-meter.js +34 -0
- package/dist/audio/level-meter.js.map +1 -0
- package/dist/audio/media-access.cjs +2 -0
- package/dist/audio/media-access.cjs.map +1 -0
- package/dist/audio/media-access.js +51 -0
- package/dist/audio/media-access.js.map +1 -0
- package/dist/audio/use-audio-recorder.cjs +2 -0
- package/dist/audio/use-audio-recorder.cjs.map +1 -0
- package/dist/audio/use-audio-recorder.js +94 -0
- package/dist/audio/use-audio-recorder.js.map +1 -0
- package/dist/audio/use-audio.cjs.map +1 -1
- package/dist/audio/use-audio.js.map +1 -1
- package/dist/audio/use-microphone.cjs +2 -0
- package/dist/audio/use-microphone.cjs.map +1 -0
- package/dist/audio/use-microphone.js +54 -0
- package/dist/audio/use-microphone.js.map +1 -0
- package/dist/audio/wav.cjs +2 -0
- package/dist/audio/wav.cjs.map +1 -0
- package/dist/audio/wav.js +49 -0
- package/dist/audio/wav.js.map +1 -0
- package/dist/auth/passkey.cjs +2 -0
- package/dist/auth/passkey.cjs.map +1 -0
- package/dist/auth/passkey.js +171 -0
- package/dist/auth/passkey.js.map +1 -0
- package/dist/auth/use-passkey.cjs +2 -0
- package/dist/auth/use-passkey.cjs.map +1 -0
- package/dist/auth/use-passkey.js +120 -0
- package/dist/auth/use-passkey.js.map +1 -0
- package/dist/br/PixQRCode.cjs +2 -0
- package/dist/br/PixQRCode.cjs.map +1 -0
- package/dist/br/PixQRCode.js +70 -0
- package/dist/br/PixQRCode.js.map +1 -0
- package/dist/br/PixQRCode.module.cjs +2 -0
- package/dist/br/PixQRCode.module.cjs.map +1 -0
- package/dist/br/PixQRCode.module.js +15 -0
- package/dist/br/PixQRCode.module.js.map +1 -0
- package/dist/br/boleto.cjs +2 -0
- package/dist/br/boleto.cjs.map +1 -0
- package/dist/br/boleto.js +225 -0
- package/dist/br/boleto.js.map +1 -0
- package/dist/br/holidays.cjs +2 -0
- package/dist/br/holidays.cjs.map +1 -0
- package/dist/br/holidays.js +148 -0
- package/dist/br/holidays.js.map +1 -0
- package/dist/br/nfe.cjs +2 -0
- package/dist/br/nfe.cjs.map +1 -0
- package/dist/br/nfe.js +104 -0
- package/dist/br/nfe.js.map +1 -0
- package/dist/br/pix.cjs +2 -0
- package/dist/br/pix.cjs.map +1 -0
- package/dist/br/pix.js +148 -0
- package/dist/br/pix.js.map +1 -0
- package/dist/br.cjs +1 -1
- package/dist/br.cjs.map +1 -1
- package/dist/br.d.ts +878 -0
- package/dist/br.js +7 -2
- package/dist/br.js.map +1 -1
- package/dist/capture/barcode.cjs +2 -0
- package/dist/capture/barcode.cjs.map +1 -0
- package/dist/capture/barcode.js +59 -0
- package/dist/capture/barcode.js.map +1 -0
- package/dist/capture/media-recorder.cjs +2 -0
- package/dist/capture/media-recorder.cjs.map +1 -0
- package/dist/capture/media-recorder.js +64 -0
- package/dist/capture/media-recorder.js.map +1 -0
- package/dist/capture/use-barcode-scanner.cjs +2 -0
- package/dist/capture/use-barcode-scanner.cjs.map +1 -0
- package/dist/capture/use-barcode-scanner.js +86 -0
- package/dist/capture/use-barcode-scanner.js.map +1 -0
- package/dist/capture/use-screen-capture.cjs +2 -0
- package/dist/capture/use-screen-capture.cjs.map +1 -0
- package/dist/capture/use-screen-capture.js +79 -0
- package/dist/capture/use-screen-capture.js.map +1 -0
- package/dist/capture/use-speech-recognition.cjs +2 -0
- package/dist/capture/use-speech-recognition.cjs.map +1 -0
- package/dist/capture/use-speech-recognition.js +118 -0
- package/dist/capture/use-speech-recognition.js.map +1 -0
- package/dist/capture/use-torch.cjs +2 -0
- package/dist/capture/use-torch.cjs.map +1 -0
- package/dist/capture/use-torch.js +33 -0
- package/dist/capture/use-torch.js.map +1 -0
- package/dist/capture/use-video-recorder.cjs +2 -0
- package/dist/capture/use-video-recorder.cjs.map +1 -0
- package/dist/capture/use-video-recorder.js +90 -0
- package/dist/capture/use-video-recorder.js.map +1 -0
- package/dist/capture/video-recorder.cjs +2 -0
- package/dist/capture/video-recorder.cjs.map +1 -0
- package/dist/capture/video-recorder.js +31 -0
- package/dist/capture/video-recorder.js.map +1 -0
- package/dist/components/AIChat/AIChat.cjs +1 -1
- package/dist/components/AIChat/AIChat.cjs.map +1 -1
- package/dist/components/AIChat/AIChat.js +93 -97
- package/dist/components/AIChat/AIChat.js.map +1 -1
- package/dist/components/AIChat/AIChatComposer.cjs +1 -1
- package/dist/components/AIChat/AIChatComposer.cjs.map +1 -1
- package/dist/components/AIChat/AIChatComposer.js +2 -1
- package/dist/components/AIChat/AIChatComposer.js.map +1 -1
- package/dist/components/AIChat/AIChatTurn.cjs +2 -2
- package/dist/components/AIChat/AIChatTurn.js +6 -6
- package/dist/components/AIChat/AIChatTurn.js.map +1 -1
- package/dist/components/Alert/Alert.module.cjs.map +1 -1
- package/dist/components/Alert/Alert.module.js.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.cjs +2 -0
- package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -0
- package/dist/components/AudioPlayer/AudioPlayer.js +123 -0
- package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -0
- package/dist/components/AudioPlayer/AudioPlayer.module.cjs +2 -0
- package/dist/components/AudioPlayer/AudioPlayer.module.cjs.map +1 -0
- package/dist/components/AudioPlayer/AudioPlayer.module.js +12 -0
- package/dist/components/AudioPlayer/AudioPlayer.module.js.map +1 -0
- package/dist/components/AudioRecorder/AudioRecorder.cjs +2 -0
- package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -0
- package/dist/components/AudioRecorder/AudioRecorder.js +179 -0
- package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -0
- package/dist/components/AudioRecorder/AudioRecorder.module.cjs +2 -0
- package/dist/components/AudioRecorder/AudioRecorder.module.cjs.map +1 -0
- package/dist/components/AudioRecorder/AudioRecorder.module.js +19 -0
- package/dist/components/AudioRecorder/AudioRecorder.module.js.map +1 -0
- package/dist/components/Badge/Badge.module.cjs.map +1 -1
- package/dist/components/Badge/Badge.module.js.map +1 -1
- package/dist/components/BarcodeScanner/BarcodeScanner.cjs +2 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.cjs.map +1 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.js +123 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.js.map +1 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.module.cjs +2 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.module.cjs.map +1 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.module.js +22 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.module.js.map +1 -0
- package/dist/components/BottomNavigation/BottomNavigation.module.cjs.map +1 -1
- package/dist/components/BottomNavigation/BottomNavigation.module.js.map +1 -1
- package/dist/components/Button/Button.module.cjs.map +1 -1
- package/dist/components/Button/Button.module.js.map +1 -1
- package/dist/components/Combobox/Combobox.module.cjs.map +1 -1
- package/dist/components/Combobox/Combobox.module.js.map +1 -1
- package/dist/components/DataTable/DataTable.cjs +1 -1
- package/dist/components/DataTable/DataTable.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.js +203 -66
- package/dist/components/DataTable/DataTable.js.map +1 -1
- package/dist/components/DataTable/DataTable.module.cjs +1 -1
- package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.module.js +9 -3
- package/dist/components/DataTable/DataTable.module.js.map +1 -1
- package/dist/components/DataTable/EditableCell.cjs +2 -0
- package/dist/components/DataTable/EditableCell.cjs.map +1 -0
- package/dist/components/DataTable/EditableCell.js +77 -0
- package/dist/components/DataTable/EditableCell.js.map +1 -0
- package/dist/components/DataTable/edit-labels.cjs +2 -0
- package/dist/components/DataTable/edit-labels.cjs.map +1 -0
- package/dist/components/DataTable/edit-labels.js +11 -0
- package/dist/components/DataTable/edit-labels.js.map +1 -0
- package/dist/components/DateRangePicker/DateRangePicker.module.cjs.map +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.module.js.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.module.cjs.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.module.js.map +1 -1
- package/dist/components/NavigationRail/NavigationRail.module.cjs.map +1 -1
- package/dist/components/NavigationRail/NavigationRail.module.js.map +1 -1
- package/dist/components/SyncStatusBadge/SyncStatusBadge.cjs +1 -1
- package/dist/components/SyncStatusBadge/SyncStatusBadge.cjs.map +1 -1
- package/dist/components/SyncStatusBadge/SyncStatusBadge.js +33 -32
- package/dist/components/SyncStatusBadge/SyncStatusBadge.js.map +1 -1
- package/dist/components/Toast/ToastProvider.cjs +1 -1
- package/dist/components/Toast/ToastProvider.cjs.map +1 -1
- package/dist/components/Toast/ToastProvider.js +1 -2
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/hooks/use-announce.cjs +2 -0
- package/dist/hooks/use-announce.cjs.map +1 -0
- package/dist/hooks/use-announce.js +50 -0
- package/dist/hooks/use-announce.js.map +1 -0
- package/dist/hooks/use-media-devices.cjs +2 -0
- package/dist/hooks/use-media-devices.cjs.map +1 -0
- package/dist/hooks/use-media-devices.js +46 -0
- package/dist/hooks/use-media-devices.js.map +1 -0
- package/dist/hooks/use-media-permission.cjs +2 -0
- package/dist/hooks/use-media-permission.cjs.map +1 -0
- package/dist/hooks/use-media-permission.js +33 -0
- package/dist/hooks/use-media-permission.js.map +1 -0
- package/dist/http/resumable-upload.cjs +2 -0
- package/dist/http/resumable-upload.cjs.map +1 -0
- package/dist/http/resumable-upload.js +249 -0
- package/dist/http/resumable-upload.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +2485 -29
- package/dist/tempest-react-sdk.js +268 -244
- package/dist/vision/core/graph.cjs +2 -0
- package/dist/vision/core/graph.cjs.map +1 -0
- package/dist/vision/core/graph.js +17 -0
- package/dist/vision/core/graph.js.map +1 -0
- package/dist/vision/core/session.cjs +1 -1
- package/dist/vision/core/session.cjs.map +1 -1
- package/dist/vision/core/session.js +19 -9
- package/dist/vision/core/session.js.map +1 -1
- package/dist/vision/index.cjs +1 -1
- package/dist/vision/index.cjs.map +1 -1
- package/dist/vision/index.js +14 -13
- package/dist/vision/index.js.map +1 -1
- package/dist/vision/tasks/classifier.cjs +1 -1
- package/dist/vision/tasks/classifier.cjs.map +1 -1
- package/dist/vision/tasks/classifier.js +33 -25
- package/dist/vision/tasks/classifier.js.map +1 -1
- package/dist/vision/tasks/detector.cjs +1 -1
- package/dist/vision/tasks/detector.cjs.map +1 -1
- package/dist/vision/tasks/detector.js +28 -20
- package/dist/vision/tasks/detector.js.map +1 -1
- package/dist/vision/tasks/segmenter.cjs +1 -1
- package/dist/vision/tasks/segmenter.cjs.map +1 -1
- package/dist/vision/tasks/segmenter.js +23 -15
- package/dist/vision/tasks/segmenter.js.map +1 -1
- package/dist/vision/use-camera-stream.cjs +1 -1
- package/dist/vision/use-camera-stream.cjs.map +1 -1
- package/dist/vision/use-camera-stream.js +40 -57
- package/dist/vision/use-camera-stream.js.map +1 -1
- package/dist/vision.cjs +1 -1
- package/dist/vision.d.ts +149 -8
- package/dist/vision.js +22 -21
- package/package.json +1 -1
|
@@ -1,141 +1,137 @@
|
|
|
1
1
|
import { cn as e } from "../../utils/cn.js";
|
|
2
2
|
import { EmptyState as t } from "../EmptyState/EmptyState.js";
|
|
3
|
-
import {
|
|
4
|
-
import { aiChatStrings as r, isGenerating as i, lastAssistantId as a, tailSignature as o, visibleTurns as
|
|
5
|
-
import
|
|
6
|
-
import { AIChatComposer as
|
|
7
|
-
import { AIChatTurn as
|
|
8
|
-
import { useEffect as
|
|
9
|
-
import { jsx as
|
|
10
|
-
import { ArrowDown as
|
|
3
|
+
import { useAnnounce as n } from "../../hooks/use-announce.js";
|
|
4
|
+
import { aiChatStrings as r, isGenerating as i, lastAssistantId as a, tailSignature as o, visibleTurns as s } from "./ai-chat-turns.js";
|
|
5
|
+
import c from "./AIChat.module.js";
|
|
6
|
+
import { AIChatComposer as l } from "./AIChatComposer.js";
|
|
7
|
+
import { AIChatTurn as u } from "./AIChatTurn.js";
|
|
8
|
+
import { useEffect as d, useLayoutEffect as f, useRef as p, useState as m } from "react";
|
|
9
|
+
import { jsx as h, jsxs as g } from "react/jsx-runtime";
|
|
10
|
+
import { ArrowDown as _ } from "lucide-react";
|
|
11
11
|
//#region src/components/AIChat/AIChat.tsx
|
|
12
|
-
var
|
|
13
|
-
function
|
|
14
|
-
let
|
|
15
|
-
messages:
|
|
16
|
-
showSystem:
|
|
17
|
-
}),
|
|
18
|
-
let e =
|
|
12
|
+
var v = 48;
|
|
13
|
+
function y({ messages: y, onSend: b, onStop: x, onRegenerate: ee, onEditSubmit: te, onFeedback: ne, onRetry: S, pending: C = !1, suggestions: w = [], renderAvatar: T, renderContent: E, votes: D, header: O, emptyState: k, showSystem: A = !1, defaultReasoningOpen: j = !1, showLineNumbers: M = !1, locale: N = "pt-BR", placeholder: P, composerActions: F, composerRef: I, composerFooter: L, composerDisabled: re, maxRows: R, onSendError: z, className: B, ...V }) {
|
|
14
|
+
let H = r(N), U = s({
|
|
15
|
+
messages: y,
|
|
16
|
+
showSystem: A
|
|
17
|
+
}), W = C || i(y), G = a(y), K = o(y), q = p(null), J = p(null), Y = p(!0), X = p(!1), [ie, Z] = m(!0), Q = n(), ae = () => {
|
|
18
|
+
let e = J.current;
|
|
19
19
|
if (!e) return;
|
|
20
|
-
let t = e.scrollHeight - e.scrollTop - e.clientHeight <
|
|
21
|
-
|
|
20
|
+
let t = e.scrollHeight - e.scrollTop - e.clientHeight < v;
|
|
21
|
+
Y.current = t, Z(t);
|
|
22
22
|
}, oe = () => {
|
|
23
|
-
let e =
|
|
24
|
-
e && (
|
|
23
|
+
let e = J.current;
|
|
24
|
+
e && (Y.current = !0, Z(!0), e.scrollTop = e.scrollHeight);
|
|
25
25
|
};
|
|
26
|
-
|
|
27
|
-
let e =
|
|
28
|
-
!e || !
|
|
29
|
-
}, [
|
|
30
|
-
let e =
|
|
26
|
+
f(() => {
|
|
27
|
+
let e = J.current;
|
|
28
|
+
!e || !Y.current || (e.scrollTop = e.scrollHeight);
|
|
29
|
+
}, [K, C]), d(() => {
|
|
30
|
+
let e = J.current;
|
|
31
31
|
e && (e.scrollTop = e.scrollHeight);
|
|
32
|
-
}, []),
|
|
33
|
-
if (
|
|
34
|
-
Q(
|
|
32
|
+
}, []), d(() => {
|
|
33
|
+
if (W) {
|
|
34
|
+
Q(H.generating), X.current = !0;
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
X.current &&= (Q(H.done), !1);
|
|
38
38
|
}, [
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
W,
|
|
40
|
+
H.generating,
|
|
41
|
+
H.done,
|
|
42
|
+
Q
|
|
42
43
|
]);
|
|
43
|
-
let $ =
|
|
44
|
-
return /* @__PURE__ */
|
|
45
|
-
className: e(
|
|
46
|
-
...
|
|
44
|
+
let $ = U.length === 0 && w.length > 0 && b !== void 0, se = !ie && U.length > 0;
|
|
45
|
+
return /* @__PURE__ */ g("div", {
|
|
46
|
+
className: e(c.panel, B),
|
|
47
|
+
...V,
|
|
47
48
|
children: [
|
|
48
|
-
|
|
49
|
-
className:
|
|
50
|
-
children:
|
|
49
|
+
O && /* @__PURE__ */ h("header", {
|
|
50
|
+
className: c.header,
|
|
51
|
+
children: O
|
|
51
52
|
}),
|
|
52
|
-
/* @__PURE__ */
|
|
53
|
-
className:
|
|
54
|
-
children: [/* @__PURE__ */
|
|
55
|
-
ref:
|
|
56
|
-
className:
|
|
53
|
+
/* @__PURE__ */ g("div", {
|
|
54
|
+
className: c.threadWrapper,
|
|
55
|
+
children: [/* @__PURE__ */ g("div", {
|
|
56
|
+
ref: J,
|
|
57
|
+
className: c.thread,
|
|
57
58
|
onScroll: ae,
|
|
58
59
|
role: "log",
|
|
59
60
|
tabIndex: 0,
|
|
60
|
-
"aria-label":
|
|
61
|
+
"aria-label": H.thread,
|
|
61
62
|
children: [
|
|
62
|
-
|
|
63
|
-
title:
|
|
64
|
-
description:
|
|
65
|
-
}) :
|
|
63
|
+
U.length === 0 && !$ ? k ?? /* @__PURE__ */ h(t, {
|
|
64
|
+
title: H.empty,
|
|
65
|
+
description: H.emptyHint
|
|
66
|
+
}) : U.map((e) => /* @__PURE__ */ h(u, {
|
|
66
67
|
message: e,
|
|
67
|
-
locale:
|
|
68
|
-
canRegenerate: e.id ===
|
|
69
|
-
onRegenerate:
|
|
68
|
+
locale: N,
|
|
69
|
+
canRegenerate: e.id === G && e.streaming !== !0,
|
|
70
|
+
onRegenerate: ee,
|
|
70
71
|
onFeedback: ne,
|
|
71
72
|
onEditSubmit: te,
|
|
72
|
-
onEditError:
|
|
73
|
-
onRetry:
|
|
74
|
-
renderAvatar:
|
|
75
|
-
renderContent:
|
|
76
|
-
vote:
|
|
77
|
-
defaultReasoningOpen:
|
|
78
|
-
showLineNumbers:
|
|
73
|
+
onEditError: z,
|
|
74
|
+
onRetry: S,
|
|
75
|
+
renderAvatar: T,
|
|
76
|
+
renderContent: E,
|
|
77
|
+
vote: D?.[e.id],
|
|
78
|
+
defaultReasoningOpen: j,
|
|
79
|
+
showLineNumbers: M
|
|
79
80
|
}, e.id)),
|
|
80
|
-
$ && /* @__PURE__ */
|
|
81
|
-
className:
|
|
82
|
-
children: [/* @__PURE__ */
|
|
83
|
-
className:
|
|
84
|
-
children:
|
|
85
|
-
}),
|
|
81
|
+
$ && /* @__PURE__ */ g("div", {
|
|
82
|
+
className: c.suggestions,
|
|
83
|
+
children: [/* @__PURE__ */ h("p", {
|
|
84
|
+
className: c.suggestionsTitle,
|
|
85
|
+
children: H.emptyHint
|
|
86
|
+
}), w.map((e) => /* @__PURE__ */ h("button", {
|
|
86
87
|
type: "button",
|
|
87
|
-
className:
|
|
88
|
-
onClick: () => void
|
|
88
|
+
className: c.suggestion,
|
|
89
|
+
onClick: () => void b?.(e),
|
|
89
90
|
children: e
|
|
90
91
|
}, e))]
|
|
91
92
|
}),
|
|
92
|
-
|
|
93
|
-
className:
|
|
94
|
-
children: [/* @__PURE__ */
|
|
95
|
-
className:
|
|
93
|
+
C && !i(y) && /* @__PURE__ */ g("p", {
|
|
94
|
+
className: c.thinking,
|
|
95
|
+
children: [/* @__PURE__ */ g("span", {
|
|
96
|
+
className: c.dots,
|
|
96
97
|
"aria-hidden": "true",
|
|
97
98
|
children: [
|
|
98
|
-
/* @__PURE__ */
|
|
99
|
-
/* @__PURE__ */
|
|
100
|
-
/* @__PURE__ */
|
|
99
|
+
/* @__PURE__ */ h("span", {}),
|
|
100
|
+
/* @__PURE__ */ h("span", {}),
|
|
101
|
+
/* @__PURE__ */ h("span", {})
|
|
101
102
|
]
|
|
102
|
-
}),
|
|
103
|
+
}), H.thinking]
|
|
103
104
|
})
|
|
104
105
|
]
|
|
105
|
-
}), se && /* @__PURE__ */
|
|
106
|
+
}), se && /* @__PURE__ */ h("button", {
|
|
106
107
|
type: "button",
|
|
107
|
-
className:
|
|
108
|
+
className: c.jump,
|
|
108
109
|
onClick: oe,
|
|
109
|
-
"aria-label":
|
|
110
|
-
title:
|
|
111
|
-
children: /* @__PURE__ */
|
|
110
|
+
"aria-label": H.jumpToLatest,
|
|
111
|
+
title: H.jumpToLatest,
|
|
112
|
+
children: /* @__PURE__ */ h(_, {
|
|
112
113
|
size: 16,
|
|
113
114
|
"aria-hidden": !0
|
|
114
115
|
})
|
|
115
116
|
})]
|
|
116
117
|
}),
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
actions: P,
|
|
130
|
-
footer: F,
|
|
131
|
-
disabled: I,
|
|
132
|
-
maxRows: L,
|
|
133
|
-
onError: R
|
|
118
|
+
b && /* @__PURE__ */ h(l, {
|
|
119
|
+
ref: I ?? q,
|
|
120
|
+
onSend: b,
|
|
121
|
+
onStop: x,
|
|
122
|
+
generating: W,
|
|
123
|
+
locale: N,
|
|
124
|
+
placeholder: P,
|
|
125
|
+
actions: F,
|
|
126
|
+
footer: L,
|
|
127
|
+
disabled: re,
|
|
128
|
+
maxRows: R,
|
|
129
|
+
onError: z
|
|
134
130
|
})
|
|
135
131
|
]
|
|
136
132
|
});
|
|
137
133
|
}
|
|
138
134
|
//#endregion
|
|
139
|
-
export {
|
|
135
|
+
export { y as AIChat };
|
|
140
136
|
|
|
141
137
|
//# sourceMappingURL=AIChat.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AIChat.js","names":[],"sources":["../../../src/components/AIChat/AIChat.tsx"],"sourcesContent":["import { ArrowDown } from \"lucide-react\";\nimport {\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type HTMLAttributes,\n type ReactNode,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { EmptyState } from \"../EmptyState\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n aiChatStrings,\n isGenerating,\n lastAssistantId,\n tailSignature,\n visibleTurns,\n type AIChatMessage,\n type AIChatVote,\n} from \"./ai-chat-turns\";\nimport { AIChatComposer, type AIChatComposerHandle } from \"./AIChatComposer\";\nimport { AIChatTurn } from \"./AIChatTurn\";\nimport styles from \"./AIChat.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onSubmit\";\n\nexport interface AIChatProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The transcript, **oldest first**. Never reordered by the component. */\n messages: readonly AIChatMessage[];\n /** Renders the composer when given. Receives the trimmed prompt. */\n onSend?: (text: string) => void | Promise<void>;\n /** Abort the turn in flight. Shows the stop button while generating. */\n onStop?: () => void;\n /** Ask again for the newest assistant turn. */\n onRegenerate?: (message: AIChatMessage) => void;\n /** Re-submit an edited user turn. The app decides what to drop after it. */\n onEditSubmit?: (message: AIChatMessage, text: string) => void | Promise<void>;\n /** Rating on an assistant turn. */\n onFeedback?: (message: AIChatMessage, vote: AIChatVote) => void;\n /** Retry a turn that carries an `error`. */\n onRetry?: (message: AIChatMessage) => void;\n /**\n * The request is out and nothing has arrived yet.\n *\n * Distinct from a turn with `streaming: true`: apps that only push a message\n * once the first token lands need somewhere to say \"we asked\", and without it the\n * screen is frozen for however long the model takes to start.\n */\n pending?: boolean;\n /** Prompts offered on an empty transcript. Clicking one sends it. */\n suggestions?: readonly string[];\n /** Avatar for a turn — an `<Avatar>`, an `<Icon>`, a logo. */\n renderAvatar?: (message: AIChatMessage) => ReactNode;\n /** Render a body yourself — a tool-call card, a chart, a citation list. */\n renderContent?: (message: AIChatMessage) => ReactNode;\n /** Ratings to show as pressed, by message id. Omit to keep them local. */\n votes?: Readonly<Record<string, AIChatVote>>;\n /** Rendered above the transcript, inside the panel. */\n header?: ReactNode;\n /** Shown when there are no turns and no suggestions. */\n emptyState?: ReactNode;\n /** Show `\"system\"` turns. Default `false`. */\n showSystem?: boolean;\n /** Reasoning blocks start expanded. Default `false`. */\n defaultReasoningOpen?: boolean;\n /** Show line numbers in fenced code. Default `false`. */\n showLineNumbers?: boolean;\n /** Locale for labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Placeholder for the composer. */\n placeholder?: string;\n /** Extra controls inside the composer, before the send button. */\n composerActions?: ReactNode;\n /** Under the composer field — token count, model name, a disclaimer. */\n composerFooter?: ReactNode;\n /** Disable the composer — no credits, conversation archived, offline. */\n composerDisabled?: boolean;\n /** Largest height the composer grows to, in lines. Default 8. */\n maxRows?: number;\n /**\n * Called when `onSend` **or** `onEditSubmit` rejects. The draft stays in the field\n * either way.\n */\n onSendError?: (error: unknown) => void;\n}\n\n/** How close to the bottom still counts as \"reading the newest turn\", in pixels. */\nconst BOTTOM_SLACK = 48;\n\n/**\n * A conversation with a model: role-based turns, Markdown answers, a reasoning\n * block, a streaming caret, per-turn actions and a composer that turns into a stop\n * button while a turn is generating.\n *\n * This is the shape ChatGPT, Claude and DeepSeek converged on, and it is a different\n * component from {@link Chat}, not a variant of it. A human thread is addressed by\n * author and cares about delivery state; a model transcript is addressed by role,\n * has no delivery state at all, and needs three things a human thread never does —\n * partial output, reasoning separate from the answer, and re-asking.\n *\n * Presentational and controlled, like the rest of the SDK: it takes a list and emits\n * intent (`onSend`, `onStop`, `onRegenerate`, `onEditSubmit`, `onFeedback`). The\n * transport stays with the app, because \"how do I stream from my backend\" has a\n * different answer per provider — the SDK's `createEventStream` covers SSE, `fetch`\n * with a `ReadableStream` covers the rest, and either way the app owns the\n * `AbortController` it hands to `onStop`.\n *\n * @example\n * <AIChat\n * messages={turns}\n * pending={pending}\n * onSend={(text) => ask(text)}\n * onStop={() => controller.current?.abort()}\n * onRegenerate={(turn) => reask(turn)}\n * onFeedback={(turn, vote) => track(\"answer_rated\", { id: turn.id, vote })}\n * suggestions={[\"Resuma o último relatório\", \"Quais pedidos atrasaram?\"]}\n * />\n */\nexport function AIChat({\n messages,\n onSend,\n onStop,\n onRegenerate,\n onEditSubmit,\n onFeedback,\n onRetry,\n pending = false,\n suggestions = [],\n renderAvatar,\n renderContent,\n votes,\n header,\n emptyState,\n showSystem = false,\n defaultReasoningOpen = false,\n showLineNumbers = false,\n locale = \"pt-BR\",\n placeholder,\n composerActions,\n composerFooter,\n composerDisabled,\n maxRows,\n onSendError,\n className,\n ...rest\n}: AIChatProps) {\n const strings = aiChatStrings(locale);\n const turns = visibleTurns({ messages, showSystem });\n const generating = pending || isGenerating(messages);\n const newestAssistant = lastAssistantId(messages);\n const signature = tailSignature(messages);\n\n const composer = useRef<AIChatComposerHandle | null>(null);\n const thread = useRef<HTMLDivElement | null>(null);\n const stuckToBottom = useRef(true);\n const wasGenerating = useRef(false);\n const [atBottom, setAtBottom] = useState(true);\n const [announcement, setAnnouncement] = useState(\"\");\n\n /**\n * Remember whether the reader is at the bottom, before the next tokens land.\n *\n * A transcript that always scrolls to the newest text yanks somebody out of the\n * answer they were re-reading — and with a streaming answer that would happen\n * dozens of times per second. So the jump only happens when they were already at\n * the bottom. The slack covers a partially visible last line.\n */\n const trackPosition = (): void => {\n const node = thread.current;\n if (!node) return;\n const next = node.scrollHeight - node.scrollTop - node.clientHeight < BOTTOM_SLACK;\n stuckToBottom.current = next;\n setAtBottom(next);\n };\n\n const jumpToLatest = (): void => {\n const node = thread.current;\n if (!node) return;\n stuckToBottom.current = true;\n setAtBottom(true);\n node.scrollTop = node.scrollHeight;\n };\n\n useLayoutEffect(() => {\n const node = thread.current;\n if (!node || !stuckToBottom.current) return;\n node.scrollTop = node.scrollHeight;\n }, [signature, pending]);\n\n useEffect(() => {\n const node = thread.current;\n if (!node) return;\n node.scrollTop = node.scrollHeight;\n // Mount lands on the newest turn; from then on `trackPosition` decides.\n }, []);\n\n /**\n * Announce the start and the end of a generation, and nothing in between.\n *\n * The transcript is a `role=\"log\"` **without** `aria-live`: a live region over\n * streaming text makes a screen reader read the answer again on every token,\n * which is unusable. The two moments that matter are announced here instead, and\n * the finished answer is read from the log at the reader's own pace.\n */\n useEffect(() => {\n if (generating) {\n setAnnouncement(strings.generating);\n wasGenerating.current = true;\n return;\n }\n if (wasGenerating.current) {\n setAnnouncement(strings.done);\n wasGenerating.current = false;\n }\n }, [generating, strings.generating, strings.done]);\n\n const showSuggestions = turns.length === 0 && suggestions.length > 0 && onSend !== undefined;\n const detached = !atBottom && turns.length > 0;\n\n return (\n <div className={cn(styles.panel, className)} {...rest}>\n {header && <header className={styles.header}>{header}</header>}\n\n <div className={styles.threadWrapper}>\n <div\n ref={thread}\n className={styles.thread}\n onScroll={trackPosition}\n role=\"log\"\n tabIndex={0}\n aria-label={strings.thread}\n >\n {turns.length === 0 && !showSuggestions\n ? (emptyState ?? (\n <EmptyState title={strings.empty} description={strings.emptyHint} />\n ))\n : turns.map((message) => (\n <AIChatTurn\n key={message.id}\n message={message}\n locale={locale}\n canRegenerate={\n message.id === newestAssistant && message.streaming !== true\n }\n onRegenerate={onRegenerate}\n onFeedback={onFeedback}\n onEditSubmit={onEditSubmit}\n onEditError={onSendError}\n onRetry={onRetry}\n renderAvatar={renderAvatar}\n renderContent={renderContent}\n vote={votes?.[message.id]}\n defaultReasoningOpen={defaultReasoningOpen}\n showLineNumbers={showLineNumbers}\n />\n ))}\n\n {showSuggestions && (\n <div className={styles.suggestions}>\n <p className={styles.suggestionsTitle}>{strings.emptyHint}</p>\n {suggestions.map((suggestion) => (\n <button\n key={suggestion}\n type=\"button\"\n className={styles.suggestion}\n onClick={() => void onSend?.(suggestion)}\n >\n {suggestion}\n </button>\n ))}\n </div>\n )}\n\n {pending && !isGenerating(messages) && (\n <p className={styles.thinking}>\n <span className={styles.dots} aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n {strings.thinking}\n </p>\n )}\n </div>\n\n {detached && (\n <button\n type=\"button\"\n className={styles.jump}\n onClick={jumpToLatest}\n aria-label={strings.jumpToLatest}\n title={strings.jumpToLatest}\n >\n <ArrowDown size={16} aria-hidden />\n </button>\n )}\n </div>\n\n <VisuallyHidden>\n <div role=\"status\" aria-live=\"polite\">\n {announcement}\n </div>\n </VisuallyHidden>\n\n {onSend && (\n <AIChatComposer\n ref={composer}\n onSend={onSend}\n onStop={onStop}\n generating={generating}\n locale={locale}\n placeholder={placeholder}\n actions={composerActions}\n footer={composerFooter}\n disabled={composerDisabled}\n maxRows={maxRows}\n onError={onSendError}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AIChat.js","names":[],"sources":["../../../src/components/AIChat/AIChat.tsx"],"sourcesContent":["import { ArrowDown } from \"lucide-react\";\nimport {\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type HTMLAttributes,\n type ReactNode,\n type Ref,\n} from \"react\";\n\nimport { useAnnounce } from \"@/hooks/use-announce\";\nimport { cn } from \"@/utils/cn\";\n\nimport { EmptyState } from \"../EmptyState\";\nimport {\n aiChatStrings,\n isGenerating,\n lastAssistantId,\n tailSignature,\n visibleTurns,\n type AIChatMessage,\n type AIChatVote,\n} from \"./ai-chat-turns\";\nimport { AIChatComposer, type AIChatComposerHandle } from \"./AIChatComposer\";\nimport { AIChatTurn } from \"./AIChatTurn\";\nimport styles from \"./AIChat.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onSubmit\";\n\nexport interface AIChatProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The transcript, **oldest first**. Never reordered by the component. */\n messages: readonly AIChatMessage[];\n /** Renders the composer when given. Receives the trimmed prompt. */\n onSend?: (text: string) => void | Promise<void>;\n /** Abort the turn in flight. Shows the stop button while generating. */\n onStop?: () => void;\n /** Ask again for the newest assistant turn. */\n onRegenerate?: (message: AIChatMessage) => void;\n /** Re-submit an edited user turn. The app decides what to drop after it. */\n onEditSubmit?: (message: AIChatMessage, text: string) => void | Promise<void>;\n /** Rating on an assistant turn. */\n onFeedback?: (message: AIChatMessage, vote: AIChatVote) => void;\n /** Retry a turn that carries an `error`. */\n onRetry?: (message: AIChatMessage) => void;\n /**\n * The request is out and nothing has arrived yet.\n *\n * Distinct from a turn with `streaming: true`: apps that only push a message\n * once the first token lands need somewhere to say \"we asked\", and without it the\n * screen is frozen for however long the model takes to start.\n */\n pending?: boolean;\n /** Prompts offered on an empty transcript. Clicking one sends it. */\n suggestions?: readonly string[];\n /** Avatar for a turn — an `<Avatar>`, an `<Icon>`, a logo. */\n renderAvatar?: (message: AIChatMessage) => ReactNode;\n /** Render a body yourself — a tool-call card, a chart, a citation list. */\n renderContent?: (message: AIChatMessage) => ReactNode;\n /** Ratings to show as pressed, by message id. Omit to keep them local. */\n votes?: Readonly<Record<string, AIChatVote>>;\n /** Rendered above the transcript, inside the panel. */\n header?: ReactNode;\n /** Shown when there are no turns and no suggestions. */\n emptyState?: ReactNode;\n /** Show `\"system\"` turns. Default `false`. */\n showSystem?: boolean;\n /** Reasoning blocks start expanded. Default `false`. */\n defaultReasoningOpen?: boolean;\n /** Show line numbers in fenced code. Default `false`. */\n showLineNumbers?: boolean;\n /** Locale for labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Placeholder for the composer. */\n placeholder?: string;\n /** Extra controls inside the composer, before the send button. */\n composerActions?: ReactNode;\n /**\n * Reach the composer imperatively — `focus()`, `getValue()`, `setValue()`.\n *\n * What makes dictation (or a slash-command menu, or \"edit and resend\") possible\n * without this component knowing anything about them: pair it with\n * `composerActions` and the button you put in the composer can write into the\n * field. Speech recognition is **not** wired in here on purpose — it would make\n * every consumer of `AIChat` pay for an API that streams audio to a third party.\n */\n composerRef?: Ref<AIChatComposerHandle>;\n /** Under the composer field — token count, model name, a disclaimer. */\n composerFooter?: ReactNode;\n /** Disable the composer — no credits, conversation archived, offline. */\n composerDisabled?: boolean;\n /** Largest height the composer grows to, in lines. Default 8. */\n maxRows?: number;\n /**\n * Called when `onSend` **or** `onEditSubmit` rejects. The draft stays in the field\n * either way.\n */\n onSendError?: (error: unknown) => void;\n}\n\n/** How close to the bottom still counts as \"reading the newest turn\", in pixels. */\nconst BOTTOM_SLACK = 48;\n\n/**\n * A conversation with a model: role-based turns, Markdown answers, a reasoning\n * block, a streaming caret, per-turn actions and a composer that turns into a stop\n * button while a turn is generating.\n *\n * This is the shape ChatGPT, Claude and DeepSeek converged on, and it is a different\n * component from {@link Chat}, not a variant of it. A human thread is addressed by\n * author and cares about delivery state; a model transcript is addressed by role,\n * has no delivery state at all, and needs three things a human thread never does —\n * partial output, reasoning separate from the answer, and re-asking.\n *\n * Presentational and controlled, like the rest of the SDK: it takes a list and emits\n * intent (`onSend`, `onStop`, `onRegenerate`, `onEditSubmit`, `onFeedback`). The\n * transport stays with the app, because \"how do I stream from my backend\" has a\n * different answer per provider — the SDK's `createEventStream` covers SSE, `fetch`\n * with a `ReadableStream` covers the rest, and either way the app owns the\n * `AbortController` it hands to `onStop`.\n *\n * @example\n * <AIChat\n * messages={turns}\n * pending={pending}\n * onSend={(text) => ask(text)}\n * onStop={() => controller.current?.abort()}\n * onRegenerate={(turn) => reask(turn)}\n * onFeedback={(turn, vote) => track(\"answer_rated\", { id: turn.id, vote })}\n * suggestions={[\"Resuma o último relatório\", \"Quais pedidos atrasaram?\"]}\n * />\n */\nexport function AIChat({\n messages,\n onSend,\n onStop,\n onRegenerate,\n onEditSubmit,\n onFeedback,\n onRetry,\n pending = false,\n suggestions = [],\n renderAvatar,\n renderContent,\n votes,\n header,\n emptyState,\n showSystem = false,\n defaultReasoningOpen = false,\n showLineNumbers = false,\n locale = \"pt-BR\",\n placeholder,\n composerActions,\n composerRef,\n composerFooter,\n composerDisabled,\n maxRows,\n onSendError,\n className,\n ...rest\n}: AIChatProps) {\n const strings = aiChatStrings(locale);\n const turns = visibleTurns({ messages, showSystem });\n const generating = pending || isGenerating(messages);\n const newestAssistant = lastAssistantId(messages);\n const signature = tailSignature(messages);\n\n const composer = useRef<AIChatComposerHandle | null>(null);\n const thread = useRef<HTMLDivElement | null>(null);\n const stuckToBottom = useRef(true);\n const wasGenerating = useRef(false);\n const [atBottom, setAtBottom] = useState(true);\n const announce = useAnnounce();\n\n /**\n * Remember whether the reader is at the bottom, before the next tokens land.\n *\n * A transcript that always scrolls to the newest text yanks somebody out of the\n * answer they were re-reading — and with a streaming answer that would happen\n * dozens of times per second. So the jump only happens when they were already at\n * the bottom. The slack covers a partially visible last line.\n */\n const trackPosition = (): void => {\n const node = thread.current;\n if (!node) return;\n const next = node.scrollHeight - node.scrollTop - node.clientHeight < BOTTOM_SLACK;\n stuckToBottom.current = next;\n setAtBottom(next);\n };\n\n const jumpToLatest = (): void => {\n const node = thread.current;\n if (!node) return;\n stuckToBottom.current = true;\n setAtBottom(true);\n node.scrollTop = node.scrollHeight;\n };\n\n useLayoutEffect(() => {\n const node = thread.current;\n if (!node || !stuckToBottom.current) return;\n node.scrollTop = node.scrollHeight;\n }, [signature, pending]);\n\n useEffect(() => {\n const node = thread.current;\n if (!node) return;\n node.scrollTop = node.scrollHeight;\n // Mount lands on the newest turn; from then on `trackPosition` decides.\n }, []);\n\n /**\n * Announce the start and the end of a generation, and nothing in between.\n *\n * The transcript is a `role=\"log\"` **without** `aria-live`: a live region over\n * streaming text makes a screen reader read the answer again on every token,\n * which is unusable. The two moments that matter are announced here instead —\n * through the shared `useAnnounce` region rather than a private one, so a page\n * holding a chat plus a table plus toasts still has exactly one live region per\n * politeness — and the finished answer is read from the log at the reader's own\n * pace.\n */\n useEffect(() => {\n if (generating) {\n announce(strings.generating);\n wasGenerating.current = true;\n return;\n }\n if (wasGenerating.current) {\n announce(strings.done);\n wasGenerating.current = false;\n }\n }, [generating, strings.generating, strings.done, announce]);\n\n const showSuggestions = turns.length === 0 && suggestions.length > 0 && onSend !== undefined;\n const detached = !atBottom && turns.length > 0;\n\n return (\n <div className={cn(styles.panel, className)} {...rest}>\n {header && <header className={styles.header}>{header}</header>}\n\n <div className={styles.threadWrapper}>\n <div\n ref={thread}\n className={styles.thread}\n onScroll={trackPosition}\n role=\"log\"\n tabIndex={0}\n aria-label={strings.thread}\n >\n {turns.length === 0 && !showSuggestions\n ? (emptyState ?? (\n <EmptyState title={strings.empty} description={strings.emptyHint} />\n ))\n : turns.map((message) => (\n <AIChatTurn\n key={message.id}\n message={message}\n locale={locale}\n canRegenerate={\n message.id === newestAssistant && message.streaming !== true\n }\n onRegenerate={onRegenerate}\n onFeedback={onFeedback}\n onEditSubmit={onEditSubmit}\n onEditError={onSendError}\n onRetry={onRetry}\n renderAvatar={renderAvatar}\n renderContent={renderContent}\n vote={votes?.[message.id]}\n defaultReasoningOpen={defaultReasoningOpen}\n showLineNumbers={showLineNumbers}\n />\n ))}\n\n {showSuggestions && (\n <div className={styles.suggestions}>\n <p className={styles.suggestionsTitle}>{strings.emptyHint}</p>\n {suggestions.map((suggestion) => (\n <button\n key={suggestion}\n type=\"button\"\n className={styles.suggestion}\n onClick={() => void onSend?.(suggestion)}\n >\n {suggestion}\n </button>\n ))}\n </div>\n )}\n\n {pending && !isGenerating(messages) && (\n <p className={styles.thinking}>\n <span className={styles.dots} aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n {strings.thinking}\n </p>\n )}\n </div>\n\n {detached && (\n <button\n type=\"button\"\n className={styles.jump}\n onClick={jumpToLatest}\n aria-label={strings.jumpToLatest}\n title={strings.jumpToLatest}\n >\n <ArrowDown size={16} aria-hidden />\n </button>\n )}\n </div>\n\n {onSend && (\n <AIChatComposer\n ref={composerRef ?? composer}\n onSend={onSend}\n onStop={onStop}\n generating={generating}\n locale={locale}\n placeholder={placeholder}\n actions={composerActions}\n footer={composerFooter}\n disabled={composerDisabled}\n maxRows={maxRows}\n onError={onSendError}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../utils/cn.cjs"),t=require("./ai-chat-turns.cjs"),n=require("./AIChat.module.cjs");let r=require("react"),i=require("react/jsx-runtime");var a=(0,r.forwardRef)(function({onSend:a,onStop:o,generating:s=!1,locale:c=`pt-BR`,actions:l,footer:u,maxRows:d=8,onError:f,className:p,disabled:m,placeholder:h,onKeyDown:g,onChange:_,...v},y){let b=t.aiChatStrings(c),x=(0,r.useRef)(null),[S,C]=(0,r.useState)(``),[w,T]=(0,r.useState)(!1);(0,r.useImperativeHandle)(y,()=>({focus:()=>x.current?.focus(),setValue:e=>{C(e),x.current?.focus()}})),(0,r.useLayoutEffect)(()=>{let e=x.current;if(!e)return;e.style.height=`auto`;let t=(Number.parseFloat(getComputedStyle(e).lineHeight)||20)*d;e.style.height=`${Math.min(e.scrollHeight,t)}px`,e.style.overflowY=e.scrollHeight>t?`auto`:`hidden`},[S,d]);let E=async()=>{let e=S.trim();if(!(!e||w||m||s)){T(!0);try{await a(e),C(``)}catch(e){f?.(e)}finally{T(!1)}}},D=e=>{e.preventDefault(),E()},O=e=>{if(g?.(e),!e.defaultPrevented){if(e.key===`Escape`&&s&&o){e.preventDefault(),o();return}e.key!==`Enter`||e.shiftKey||e.nativeEvent.isComposing||e.keyCode===229||(e.preventDefault(),E())}},k=s&&o!==void 0;return(0,i.jsxs)(`form`,{className:e.cn(n.default.composer,p),onSubmit:D,children:[(0,i.jsxs)(`div`,{className:n.default.composerBox,children:[(0,i.jsx)(`textarea`,{...v,ref:x,className:n.default.field,rows:1,value:S,disabled:m,placeholder:h??b.placeholder,onChange:e=>{C(e.target.value),_?.(e)},onKeyDown:O}),(0,i.jsxs)(`div`,{className:n.default.composerActions,children:[l,k?(0,i.jsxs)(`button`,{type:`button`,className:n.default.stop,onClick:o,children:[(0,i.jsx)(`span`,{"aria-hidden":`true`,className:n.default.stopGlyph}),b.stop]}):(0,i.jsx)(`button`,{type:`submit`,className:n.default.send,disabled:m||w||s||S.trim()===``,children:b.send})]})]}),u&&(0,i.jsx)(`div`,{className:n.default.composerFooter,children:u})]})});exports.AIChatComposer=a;
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("./ai-chat-turns.cjs"),n=require("./AIChat.module.cjs");let r=require("react"),i=require("react/jsx-runtime");var a=(0,r.forwardRef)(function({onSend:a,onStop:o,generating:s=!1,locale:c=`pt-BR`,actions:l,footer:u,maxRows:d=8,onError:f,className:p,disabled:m,placeholder:h,onKeyDown:g,onChange:_,...v},y){let b=t.aiChatStrings(c),x=(0,r.useRef)(null),[S,C]=(0,r.useState)(``),[w,T]=(0,r.useState)(!1);(0,r.useImperativeHandle)(y,()=>({focus:()=>x.current?.focus(),setValue:e=>{C(e),x.current?.focus()},getValue:()=>S})),(0,r.useLayoutEffect)(()=>{let e=x.current;if(!e)return;e.style.height=`auto`;let t=(Number.parseFloat(getComputedStyle(e).lineHeight)||20)*d;e.style.height=`${Math.min(e.scrollHeight,t)}px`,e.style.overflowY=e.scrollHeight>t?`auto`:`hidden`},[S,d]);let E=async()=>{let e=S.trim();if(!(!e||w||m||s)){T(!0);try{await a(e),C(``)}catch(e){f?.(e)}finally{T(!1)}}},D=e=>{e.preventDefault(),E()},O=e=>{if(g?.(e),!e.defaultPrevented){if(e.key===`Escape`&&s&&o){e.preventDefault(),o();return}e.key!==`Enter`||e.shiftKey||e.nativeEvent.isComposing||e.keyCode===229||(e.preventDefault(),E())}},k=s&&o!==void 0;return(0,i.jsxs)(`form`,{className:e.cn(n.default.composer,p),onSubmit:D,children:[(0,i.jsxs)(`div`,{className:n.default.composerBox,children:[(0,i.jsx)(`textarea`,{...v,ref:x,className:n.default.field,rows:1,value:S,disabled:m,placeholder:h??b.placeholder,onChange:e=>{C(e.target.value),_?.(e)},onKeyDown:O}),(0,i.jsxs)(`div`,{className:n.default.composerActions,children:[l,k?(0,i.jsxs)(`button`,{type:`button`,className:n.default.stop,onClick:o,children:[(0,i.jsx)(`span`,{"aria-hidden":`true`,className:n.default.stopGlyph}),b.stop]}):(0,i.jsx)(`button`,{type:`submit`,className:n.default.send,disabled:m||w||s||S.trim()===``,children:b.send})]})]}),u&&(0,i.jsx)(`div`,{className:n.default.composerFooter,children:u})]})});exports.AIChatComposer=a;
|
|
2
2
|
//# sourceMappingURL=AIChatComposer.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AIChatComposer.cjs","names":[],"sources":["../../../src/components/AIChat/AIChatComposer.tsx"],"sourcesContent":["import {\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n useState,\n type FormEvent,\n type KeyboardEvent,\n type ReactNode,\n type TextareaHTMLAttributes,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { aiChatStrings } from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\n/** DOM attributes the composer redefines. */\ntype OverriddenDomProps = \"onSubmit\" | \"value\" | \"defaultValue\" | \"rows\";\n\nexport interface AIChatComposerProps extends Omit<\n TextareaHTMLAttributes<HTMLTextAreaElement>,\n OverriddenDomProps\n> {\n /** Called with the trimmed prompt. The field clears only when this does not throw. */\n onSend: (text: string) => void | Promise<void>;\n /**\n * Abort the turn in flight.\n *\n * When given together with `generating`, the send button becomes a stop button\n * and `Escape` aborts too.\n */\n onStop?: () => void;\n /** A turn is being generated. Replaces send with stop and refuses to send. */\n generating?: boolean;\n /** Locale for the placeholder and the button labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Left of the send button — an attach control, a model picker, a tool toggle. */\n actions?: ReactNode;\n /** Under the field — a token count, the model name, a disclaimer. */\n footer?: ReactNode;\n /** Largest height the field grows to, in lines. Default 8. */\n maxRows?: number;\n /**\n * Called when `onSend` rejects. The draft is kept either way.\n *\n * Without it the rejection is swallowed after the draft is preserved: re-throwing\n * out of a DOM event handler surfaces as an unhandled promise rejection, which is\n * console noise for the developer and nothing the user can act on. The visible\n * signal is the prompt still sitting in the field; wire this to a toast to say why.\n */\n onError?: (error: unknown) => void;\n}\n\n/** Imperative handle, so a thread can focus or refill the field. */\nexport interface AIChatComposerHandle {\n focus: () => void;\n /** Replace the draft — used to put a prompt back in the field. */\n setValue: (text: string) => void;\n}\n\n/**\n * The prompt field of a conversation with a model: a textarea that grows with its\n * content, sends on `Enter`, keeps `Shift+Enter` for a newline, and turns into a\n * stop button while a turn is streaming.\n *\n * Uncontrolled on purpose. A draft changes on every keystroke, and lifting that into\n * app state re-renders the whole transcript per character — with a streaming answer\n * above, that is the one place where \"controlled by default\" costs something\n * visible. Apps that need the draft (a persisted composer, a slash-command menu)\n * read it from `onChange` or drive it through the ref.\n *\n * @example\n * <AIChatComposer\n * generating={generating}\n * onSend={(text) => ask(text)}\n * onStop={() => controller.abort()}\n * footer={<small>Claude Opus 5 · pode errar</small>}\n * />\n */\nexport const AIChatComposer = forwardRef<AIChatComposerHandle, AIChatComposerProps>(\n function AIChatComposer(\n {\n onSend,\n onStop,\n generating = false,\n locale = \"pt-BR\",\n actions,\n footer,\n maxRows = 8,\n onError,\n className,\n disabled,\n placeholder,\n onKeyDown,\n onChange,\n ...rest\n },\n ref,\n ) {\n const strings = aiChatStrings(locale);\n const textarea = useRef<HTMLTextAreaElement | null>(null);\n const [value, setValue] = useState(\"\");\n const [busy, setBusy] = useState(false);\n\n useImperativeHandle(ref, () => ({\n focus: () => textarea.current?.focus(),\n setValue: (text: string) => {\n setValue(text);\n textarea.current?.focus();\n },\n }));\n\n /**\n * Grow the field to fit its content, up to `maxRows`.\n *\n * Measured from `scrollHeight` after resetting the height, because\n * `scrollHeight` on an element that is already tall enough reports the current\n * height and the field would never shrink back.\n */\n useLayoutEffect(() => {\n const node = textarea.current;\n if (!node) return;\n node.style.height = \"auto\";\n const lineHeight = Number.parseFloat(getComputedStyle(node).lineHeight) || 20;\n const max = lineHeight * maxRows;\n node.style.height = `${Math.min(node.scrollHeight, max)}px`;\n node.style.overflowY = node.scrollHeight > max ? \"auto\" : \"hidden\";\n }, [value, maxRows]);\n\n const submit = async (): Promise<void> => {\n const text = value.trim();\n if (!text || busy || disabled || generating) return;\n setBusy(true);\n try {\n await onSend(text);\n setValue(\"\");\n } catch (error) {\n onError?.(error);\n } finally {\n setBusy(false);\n }\n };\n\n const handleSubmit = (event: FormEvent): void => {\n event.preventDefault();\n void submit();\n };\n\n /**\n * `Enter` sends, `Shift+Enter` breaks the line, `Escape` aborts a turn in\n * flight.\n *\n * The IME check is not optional: while composing Japanese or Korean, `Enter`\n * confirms the candidate word and `keyCode === 229` marks that keystroke.\n * Sending there would post half a word and eat the confirmation.\n */\n const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>): void => {\n onKeyDown?.(event);\n if (event.defaultPrevented) return;\n if (event.key === \"Escape\" && generating && onStop) {\n event.preventDefault();\n onStop();\n return;\n }\n if (event.key !== \"Enter\" || event.shiftKey) return;\n if (event.nativeEvent.isComposing || event.keyCode === 229) return;\n event.preventDefault();\n void submit();\n };\n\n const showStop = generating && onStop !== undefined;\n\n return (\n <form className={cn(styles.composer, className)} onSubmit={handleSubmit}>\n <div className={styles.composerBox}>\n <textarea\n {...rest}\n ref={textarea}\n className={styles.field}\n rows={1}\n value={value}\n disabled={disabled}\n placeholder={placeholder ?? strings.placeholder}\n onChange={(event) => {\n setValue(event.target.value);\n onChange?.(event);\n }}\n onKeyDown={handleKeyDown}\n />\n <div className={styles.composerActions}>\n {actions}\n {showStop ? (\n <button type=\"button\" className={styles.stop} onClick={onStop}>\n <span aria-hidden=\"true\" className={styles.stopGlyph} />\n {strings.stop}\n </button>\n ) : (\n <button\n type=\"submit\"\n className={styles.send}\n disabled={disabled || busy || generating || value.trim() === \"\"}\n >\n {strings.send}\n </button>\n )}\n </div>\n </div>\n {footer && <div className={styles.composerFooter}>{footer}</div>}\n </form>\n );\n },\n);\n"],"mappings":"8JAgFA,IAAa,GAAA,EAAA,EAAA,WAAA,CACT,SACI,CACI,SACA,SACA,aAAa,GACb,SAAS,QACT,UACA,SACA,UAAU,EACV,UACA,YACA,WACA,cACA,YACA,WACA,GAAG,GAEP,EACF,CACE,IAAM,EAAU,EAAA,cAAc,CAAM,EAC9B,GAAA,EAAA,EAAA,OAAA,CAA8C,IAAI,EAClD,CAAC,EAAO,IAAA,EAAA,EAAA,SAAA,CAAqB,EAAE,EAC/B,CAAC,EAAM,IAAA,EAAA,EAAA,SAAA,CAAoB,EAAK,GAEtC,EAAA,EAAA,oBAAA,CAAoB,OAAY,CAC5B,UAAa,EAAS,SAAS,MAAM,EACrC,SAAW,GAAiB,CACxB,EAAS,CAAI,EACb,EAAS,SAAS,MAAM,CAC5B,CACJ,EAAE,GASF,EAAA,EAAA,gBAAA,KAAsB,CAClB,IAAM,EAAO,EAAS,QACtB,GAAI,CAAC,EAAM,OACX,EAAK,MAAM,OAAS,OAEpB,IAAM,GADa,OAAO,WAAW,iBAAiB,CAAI,CAAC,CAAC,UAAU,GAAK,IAClD,EACzB,EAAK,MAAM,OAAS,GAAG,KAAK,IAAI,EAAK,aAAc,CAAG,EAAE,IACxD,EAAK,MAAM,UAAY,EAAK,aAAe,EAAM,OAAS,QAC9D,EAAG,CAAC,EAAO,CAAO,CAAC,EAEnB,IAAM,EAAS,SAA2B,CACtC,IAAM,EAAO,EAAM,KAAK,EACpB,MAAC,GAAQ,GAAQ,GAAY,GACjC,GAAQ,EAAI,EACZ,GAAI,CACA,MAAM,EAAO,CAAI,EACjB,EAAS,EAAE,CACf,OAAS,EAAO,CACZ,IAAU,CAAK,CACnB,QAAU,CACN,EAAQ,EAAK,CACjB,CARY,CAShB,EAEM,EAAgB,GAA2B,CAC7C,EAAM,eAAe,EACrB,EAAY,CAChB,EAUM,EAAiB,GAAoD,CACvE,OAAY,CAAK,EACb,GAAM,iBACV,IAAI,EAAM,MAAQ,UAAY,GAAc,EAAQ,CAChD,EAAM,eAAe,EACrB,EAAO,EACP,MACJ,CACI,EAAM,MAAQ,SAAW,EAAM,UAC/B,EAAM,YAAY,aAAe,EAAM,UAAY,MACvD,EAAM,eAAe,EACrB,EAAY,EAJZ,CAKJ,EAEM,EAAW,GAAc,IAAW,IAAA,GAE1C,OACI,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAW,EAAA,GAAG,EAAA,QAAO,SAAU,CAAS,EAAG,SAAU,WAA3D,EACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,qBAAvB,EACI,EAAA,EAAA,IAAA,CAAC,WAAD,CACI,GAAI,EACJ,IAAK,EACL,UAAW,EAAA,QAAO,MAClB,KAAM,EACC,QACG,WACV,YAAa,GAAe,EAAQ,YACpC,SAAW,GAAU,CACjB,EAAS,EAAM,OAAO,KAAK,EAC3B,IAAW,CAAK,CACpB,EACA,UAAW,CACd,CAAA,GACD,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,yBAAvB,CACK,EACA,GACG,EAAA,EAAA,KAAA,CAAC,SAAD,CAAQ,KAAK,SAAS,UAAW,EAAA,QAAO,KAAM,QAAS,WAAvD,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,cAAY,OAAO,UAAW,EAAA,QAAO,SAAY,CAAA,EACtD,EAAQ,IACL,KAER,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,KAClB,SAAU,GAAY,GAAQ,GAAc,EAAM,KAAK,IAAM,YAE5D,EAAQ,IACL,CAAA,CAEX,GACJ,IACJ,IAAU,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,wBAAiB,CAAY,CAAA,CAC7D,GAEd,CACJ"}
|
|
1
|
+
{"version":3,"file":"AIChatComposer.cjs","names":[],"sources":["../../../src/components/AIChat/AIChatComposer.tsx"],"sourcesContent":["import {\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n useState,\n type FormEvent,\n type KeyboardEvent,\n type ReactNode,\n type TextareaHTMLAttributes,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { aiChatStrings } from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\n/** DOM attributes the composer redefines. */\ntype OverriddenDomProps = \"onSubmit\" | \"value\" | \"defaultValue\" | \"rows\";\n\nexport interface AIChatComposerProps extends Omit<\n TextareaHTMLAttributes<HTMLTextAreaElement>,\n OverriddenDomProps\n> {\n /** Called with the trimmed prompt. The field clears only when this does not throw. */\n onSend: (text: string) => void | Promise<void>;\n /**\n * Abort the turn in flight.\n *\n * When given together with `generating`, the send button becomes a stop button\n * and `Escape` aborts too.\n */\n onStop?: () => void;\n /** A turn is being generated. Replaces send with stop and refuses to send. */\n generating?: boolean;\n /** Locale for the placeholder and the button labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Left of the send button — an attach control, a model picker, a tool toggle. */\n actions?: ReactNode;\n /** Under the field — a token count, the model name, a disclaimer. */\n footer?: ReactNode;\n /** Largest height the field grows to, in lines. Default 8. */\n maxRows?: number;\n /**\n * Called when `onSend` rejects. The draft is kept either way.\n *\n * Without it the rejection is swallowed after the draft is preserved: re-throwing\n * out of a DOM event handler surfaces as an unhandled promise rejection, which is\n * console noise for the developer and nothing the user can act on. The visible\n * signal is the prompt still sitting in the field; wire this to a toast to say why.\n */\n onError?: (error: unknown) => void;\n}\n\n/** Imperative handle, so a thread can focus, read or refill the field. */\nexport interface AIChatComposerHandle {\n focus: () => void;\n /** Replace the draft — used to put a prompt back in the field. */\n setValue: (text: string) => void;\n /**\n * The current draft.\n *\n * The counterpart `setValue` needs to be usable for anything **additive**. The\n * field is uncontrolled, so without this the only way to append to a draft — a\n * dictated phrase, a picked slash-command, a pasted citation — is to shadow the\n * whole value in app state through `onChange` and hope the two never drift.\n */\n getValue: () => string;\n}\n\n/**\n * The prompt field of a conversation with a model: a textarea that grows with its\n * content, sends on `Enter`, keeps `Shift+Enter` for a newline, and turns into a\n * stop button while a turn is streaming.\n *\n * Uncontrolled on purpose. A draft changes on every keystroke, and lifting that into\n * app state re-renders the whole transcript per character — with a streaming answer\n * above, that is the one place where \"controlled by default\" costs something\n * visible. Apps that need the draft (a persisted composer, a slash-command menu)\n * read it from `onChange` or drive it through the ref.\n *\n * @example\n * <AIChatComposer\n * generating={generating}\n * onSend={(text) => ask(text)}\n * onStop={() => controller.abort()}\n * footer={<small>Claude Opus 5 · pode errar</small>}\n * />\n */\nexport const AIChatComposer = forwardRef<AIChatComposerHandle, AIChatComposerProps>(\n function AIChatComposer(\n {\n onSend,\n onStop,\n generating = false,\n locale = \"pt-BR\",\n actions,\n footer,\n maxRows = 8,\n onError,\n className,\n disabled,\n placeholder,\n onKeyDown,\n onChange,\n ...rest\n },\n ref,\n ) {\n const strings = aiChatStrings(locale);\n const textarea = useRef<HTMLTextAreaElement | null>(null);\n const [value, setValue] = useState(\"\");\n const [busy, setBusy] = useState(false);\n\n useImperativeHandle(ref, () => ({\n focus: () => textarea.current?.focus(),\n setValue: (text: string) => {\n setValue(text);\n textarea.current?.focus();\n },\n getValue: () => value,\n }));\n\n /**\n * Grow the field to fit its content, up to `maxRows`.\n *\n * Measured from `scrollHeight` after resetting the height, because\n * `scrollHeight` on an element that is already tall enough reports the current\n * height and the field would never shrink back.\n */\n useLayoutEffect(() => {\n const node = textarea.current;\n if (!node) return;\n node.style.height = \"auto\";\n const lineHeight = Number.parseFloat(getComputedStyle(node).lineHeight) || 20;\n const max = lineHeight * maxRows;\n node.style.height = `${Math.min(node.scrollHeight, max)}px`;\n node.style.overflowY = node.scrollHeight > max ? \"auto\" : \"hidden\";\n }, [value, maxRows]);\n\n const submit = async (): Promise<void> => {\n const text = value.trim();\n if (!text || busy || disabled || generating) return;\n setBusy(true);\n try {\n await onSend(text);\n setValue(\"\");\n } catch (error) {\n onError?.(error);\n } finally {\n setBusy(false);\n }\n };\n\n const handleSubmit = (event: FormEvent): void => {\n event.preventDefault();\n void submit();\n };\n\n /**\n * `Enter` sends, `Shift+Enter` breaks the line, `Escape` aborts a turn in\n * flight.\n *\n * The IME check is not optional: while composing Japanese or Korean, `Enter`\n * confirms the candidate word and `keyCode === 229` marks that keystroke.\n * Sending there would post half a word and eat the confirmation.\n */\n const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>): void => {\n onKeyDown?.(event);\n if (event.defaultPrevented) return;\n if (event.key === \"Escape\" && generating && onStop) {\n event.preventDefault();\n onStop();\n return;\n }\n if (event.key !== \"Enter\" || event.shiftKey) return;\n if (event.nativeEvent.isComposing || event.keyCode === 229) return;\n event.preventDefault();\n void submit();\n };\n\n const showStop = generating && onStop !== undefined;\n\n return (\n <form className={cn(styles.composer, className)} onSubmit={handleSubmit}>\n <div className={styles.composerBox}>\n <textarea\n {...rest}\n ref={textarea}\n className={styles.field}\n rows={1}\n value={value}\n disabled={disabled}\n placeholder={placeholder ?? strings.placeholder}\n onChange={(event) => {\n setValue(event.target.value);\n onChange?.(event);\n }}\n onKeyDown={handleKeyDown}\n />\n <div className={styles.composerActions}>\n {actions}\n {showStop ? (\n <button type=\"button\" className={styles.stop} onClick={onStop}>\n <span aria-hidden=\"true\" className={styles.stopGlyph} />\n {strings.stop}\n </button>\n ) : (\n <button\n type=\"submit\"\n className={styles.send}\n disabled={disabled || busy || generating || value.trim() === \"\"}\n >\n {strings.send}\n </button>\n )}\n </div>\n </div>\n {footer && <div className={styles.composerFooter}>{footer}</div>}\n </form>\n );\n },\n);\n"],"mappings":"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"}
|
|
@@ -10,7 +10,8 @@ var u = r(function({ onSend: r, onStop: u, generating: d = !1, locale: f = "pt-B
|
|
|
10
10
|
focus: () => T.current?.focus(),
|
|
11
11
|
setValue: (e) => {
|
|
12
12
|
D(e), T.current?.focus();
|
|
13
|
-
}
|
|
13
|
+
},
|
|
14
|
+
getValue: () => E
|
|
14
15
|
})), a(() => {
|
|
15
16
|
let e = T.current;
|
|
16
17
|
if (!e) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AIChatComposer.js","names":[],"sources":["../../../src/components/AIChat/AIChatComposer.tsx"],"sourcesContent":["import {\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n useState,\n type FormEvent,\n type KeyboardEvent,\n type ReactNode,\n type TextareaHTMLAttributes,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { aiChatStrings } from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\n/** DOM attributes the composer redefines. */\ntype OverriddenDomProps = \"onSubmit\" | \"value\" | \"defaultValue\" | \"rows\";\n\nexport interface AIChatComposerProps extends Omit<\n TextareaHTMLAttributes<HTMLTextAreaElement>,\n OverriddenDomProps\n> {\n /** Called with the trimmed prompt. The field clears only when this does not throw. */\n onSend: (text: string) => void | Promise<void>;\n /**\n * Abort the turn in flight.\n *\n * When given together with `generating`, the send button becomes a stop button\n * and `Escape` aborts too.\n */\n onStop?: () => void;\n /** A turn is being generated. Replaces send with stop and refuses to send. */\n generating?: boolean;\n /** Locale for the placeholder and the button labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Left of the send button — an attach control, a model picker, a tool toggle. */\n actions?: ReactNode;\n /** Under the field — a token count, the model name, a disclaimer. */\n footer?: ReactNode;\n /** Largest height the field grows to, in lines. Default 8. */\n maxRows?: number;\n /**\n * Called when `onSend` rejects. The draft is kept either way.\n *\n * Without it the rejection is swallowed after the draft is preserved: re-throwing\n * out of a DOM event handler surfaces as an unhandled promise rejection, which is\n * console noise for the developer and nothing the user can act on. The visible\n * signal is the prompt still sitting in the field; wire this to a toast to say why.\n */\n onError?: (error: unknown) => void;\n}\n\n/** Imperative handle, so a thread can focus or refill the field. */\nexport interface AIChatComposerHandle {\n focus: () => void;\n /** Replace the draft — used to put a prompt back in the field. */\n setValue: (text: string) => void;\n}\n\n/**\n * The prompt field of a conversation with a model: a textarea that grows with its\n * content, sends on `Enter`, keeps `Shift+Enter` for a newline, and turns into a\n * stop button while a turn is streaming.\n *\n * Uncontrolled on purpose. A draft changes on every keystroke, and lifting that into\n * app state re-renders the whole transcript per character — with a streaming answer\n * above, that is the one place where \"controlled by default\" costs something\n * visible. Apps that need the draft (a persisted composer, a slash-command menu)\n * read it from `onChange` or drive it through the ref.\n *\n * @example\n * <AIChatComposer\n * generating={generating}\n * onSend={(text) => ask(text)}\n * onStop={() => controller.abort()}\n * footer={<small>Claude Opus 5 · pode errar</small>}\n * />\n */\nexport const AIChatComposer = forwardRef<AIChatComposerHandle, AIChatComposerProps>(\n function AIChatComposer(\n {\n onSend,\n onStop,\n generating = false,\n locale = \"pt-BR\",\n actions,\n footer,\n maxRows = 8,\n onError,\n className,\n disabled,\n placeholder,\n onKeyDown,\n onChange,\n ...rest\n },\n ref,\n ) {\n const strings = aiChatStrings(locale);\n const textarea = useRef<HTMLTextAreaElement | null>(null);\n const [value, setValue] = useState(\"\");\n const [busy, setBusy] = useState(false);\n\n useImperativeHandle(ref, () => ({\n focus: () => textarea.current?.focus(),\n setValue: (text: string) => {\n setValue(text);\n textarea.current?.focus();\n },\n }));\n\n /**\n * Grow the field to fit its content, up to `maxRows`.\n *\n * Measured from `scrollHeight` after resetting the height, because\n * `scrollHeight` on an element that is already tall enough reports the current\n * height and the field would never shrink back.\n */\n useLayoutEffect(() => {\n const node = textarea.current;\n if (!node) return;\n node.style.height = \"auto\";\n const lineHeight = Number.parseFloat(getComputedStyle(node).lineHeight) || 20;\n const max = lineHeight * maxRows;\n node.style.height = `${Math.min(node.scrollHeight, max)}px`;\n node.style.overflowY = node.scrollHeight > max ? \"auto\" : \"hidden\";\n }, [value, maxRows]);\n\n const submit = async (): Promise<void> => {\n const text = value.trim();\n if (!text || busy || disabled || generating) return;\n setBusy(true);\n try {\n await onSend(text);\n setValue(\"\");\n } catch (error) {\n onError?.(error);\n } finally {\n setBusy(false);\n }\n };\n\n const handleSubmit = (event: FormEvent): void => {\n event.preventDefault();\n void submit();\n };\n\n /**\n * `Enter` sends, `Shift+Enter` breaks the line, `Escape` aborts a turn in\n * flight.\n *\n * The IME check is not optional: while composing Japanese or Korean, `Enter`\n * confirms the candidate word and `keyCode === 229` marks that keystroke.\n * Sending there would post half a word and eat the confirmation.\n */\n const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>): void => {\n onKeyDown?.(event);\n if (event.defaultPrevented) return;\n if (event.key === \"Escape\" && generating && onStop) {\n event.preventDefault();\n onStop();\n return;\n }\n if (event.key !== \"Enter\" || event.shiftKey) return;\n if (event.nativeEvent.isComposing || event.keyCode === 229) return;\n event.preventDefault();\n void submit();\n };\n\n const showStop = generating && onStop !== undefined;\n\n return (\n <form className={cn(styles.composer, className)} onSubmit={handleSubmit}>\n <div className={styles.composerBox}>\n <textarea\n {...rest}\n ref={textarea}\n className={styles.field}\n rows={1}\n value={value}\n disabled={disabled}\n placeholder={placeholder ?? strings.placeholder}\n onChange={(event) => {\n setValue(event.target.value);\n onChange?.(event);\n }}\n onKeyDown={handleKeyDown}\n />\n <div className={styles.composerActions}>\n {actions}\n {showStop ? (\n <button type=\"button\" className={styles.stop} onClick={onStop}>\n <span aria-hidden=\"true\" className={styles.stopGlyph} />\n {strings.stop}\n </button>\n ) : (\n <button\n type=\"submit\"\n className={styles.send}\n disabled={disabled || busy || generating || value.trim() === \"\"}\n >\n {strings.send}\n </button>\n )}\n </div>\n </div>\n {footer && <div className={styles.composerFooter}>{footer}</div>}\n </form>\n );\n },\n);\n"],"mappings":";;;;;;AAgFA,IAAa,IAAiB,EAC1B,SACI,EACI,WACA,WACA,gBAAa,IACb,YAAS,SACT,YACA,WACA,aAAU,GACV,YACA,cACA,aACA,gBACA,cACA,aACA,GAAG,KAEP,GACF;CACE,IAAM,IAAU,EAAc,CAAM,GAC9B,IAAW,EAAmC,IAAI,GAClD,CAAC,GAAO,KAAY,EAAS,EAAE,GAC/B,CAAC,GAAM,KAAW,EAAS,EAAK;CAiBtC,AAfA,EAAoB,UAAY;EAC5B,aAAa,EAAS,SAAS,MAAM;EACrC,WAAW,MAAiB;GAExB,AADA,EAAS,CAAI,GACb,EAAS,SAAS,MAAM;EAC5B;CACJ,EAAE,GASF,QAAsB;EAClB,IAAM,IAAO,EAAS;EACtB,IAAI,CAAC,GAAM;EACX,EAAK,MAAM,SAAS;EAEpB,IAAM,KADa,OAAO,WAAW,iBAAiB,CAAI,CAAC,CAAC,UAAU,KAAK,MAClD;EAEzB,AADA,EAAK,MAAM,SAAS,GAAG,KAAK,IAAI,EAAK,cAAc,CAAG,EAAE,KACxD,EAAK,MAAM,YAAY,EAAK,eAAe,IAAM,SAAS;CAC9D,GAAG,CAAC,GAAO,CAAO,CAAC;CAEnB,IAAM,IAAS,YAA2B;EACtC,IAAM,IAAO,EAAM,KAAK;EACpB,OAAC,KAAQ,KAAQ,KAAY,IACjC;KAAQ,EAAI;GACZ,IAAI;IAEA,AADA,MAAM,EAAO,CAAI,GACjB,EAAS,EAAE;GACf,SAAS,GAAO;IACZ,IAAU,CAAK;GACnB,UAAU;IACN,EAAQ,EAAK;GACjB;EARY;CAShB,GAEM,KAAgB,MAA2B;EAE7C,AADA,EAAM,eAAe,GACrB,EAAY;CAChB,GAUM,KAAiB,MAAoD;EACvE,QAAY,CAAK,GACb,GAAM,kBACV;OAAI,EAAM,QAAQ,YAAY,KAAc,GAAQ;IAEhD,AADA,EAAM,eAAe,GACrB,EAAO;IACP;GACJ;GACI,EAAM,QAAQ,WAAW,EAAM,YAC/B,EAAM,YAAY,eAAe,EAAM,YAAY,QACvD,EAAM,eAAe,GACrB,EAAY;EAJZ;CAKJ,GAEM,IAAW,KAAc,MAAW,KAAA;CAE1C,OACI,kBAAC,QAAD;EAAM,WAAW,EAAG,EAAO,UAAU,CAAS;EAAG,UAAU;YAA3D,CACI,kBAAC,OAAD;GAAK,WAAW,EAAO;aAAvB,CACI,kBAAC,YAAD;IACI,GAAI;IACJ,KAAK;IACL,WAAW,EAAO;IAClB,MAAM;IACC;IACG;IACV,aAAa,KAAe,EAAQ;IACpC,WAAW,MAAU;KAEjB,AADA,EAAS,EAAM,OAAO,KAAK,GAC3B,IAAW,CAAK;IACpB;IACA,WAAW;GACd,CAAA,GACD,kBAAC,OAAD;IAAK,WAAW,EAAO;cAAvB,CACK,GACA,IACG,kBAAC,UAAD;KAAQ,MAAK;KAAS,WAAW,EAAO;KAAM,SAAS;eAAvD,CACI,kBAAC,QAAD;MAAM,eAAY;MAAO,WAAW,EAAO;KAAY,CAAA,GACtD,EAAQ,IACL;SAER,kBAAC,UAAD;KACI,MAAK;KACL,WAAW,EAAO;KAClB,UAAU,KAAY,KAAQ,KAAc,EAAM,KAAK,MAAM;eAE5D,EAAQ;IACL,CAAA,CAEX;KACJ;MACJ,KAAU,kBAAC,OAAD;GAAK,WAAW,EAAO;aAAiB;EAAY,CAAA,CAC7D;;AAEd,CACJ"}
|
|
1
|
+
{"version":3,"file":"AIChatComposer.js","names":[],"sources":["../../../src/components/AIChat/AIChatComposer.tsx"],"sourcesContent":["import {\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n useState,\n type FormEvent,\n type KeyboardEvent,\n type ReactNode,\n type TextareaHTMLAttributes,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { aiChatStrings } from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\n/** DOM attributes the composer redefines. */\ntype OverriddenDomProps = \"onSubmit\" | \"value\" | \"defaultValue\" | \"rows\";\n\nexport interface AIChatComposerProps extends Omit<\n TextareaHTMLAttributes<HTMLTextAreaElement>,\n OverriddenDomProps\n> {\n /** Called with the trimmed prompt. The field clears only when this does not throw. */\n onSend: (text: string) => void | Promise<void>;\n /**\n * Abort the turn in flight.\n *\n * When given together with `generating`, the send button becomes a stop button\n * and `Escape` aborts too.\n */\n onStop?: () => void;\n /** A turn is being generated. Replaces send with stop and refuses to send. */\n generating?: boolean;\n /** Locale for the placeholder and the button labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Left of the send button — an attach control, a model picker, a tool toggle. */\n actions?: ReactNode;\n /** Under the field — a token count, the model name, a disclaimer. */\n footer?: ReactNode;\n /** Largest height the field grows to, in lines. Default 8. */\n maxRows?: number;\n /**\n * Called when `onSend` rejects. The draft is kept either way.\n *\n * Without it the rejection is swallowed after the draft is preserved: re-throwing\n * out of a DOM event handler surfaces as an unhandled promise rejection, which is\n * console noise for the developer and nothing the user can act on. The visible\n * signal is the prompt still sitting in the field; wire this to a toast to say why.\n */\n onError?: (error: unknown) => void;\n}\n\n/** Imperative handle, so a thread can focus, read or refill the field. */\nexport interface AIChatComposerHandle {\n focus: () => void;\n /** Replace the draft — used to put a prompt back in the field. */\n setValue: (text: string) => void;\n /**\n * The current draft.\n *\n * The counterpart `setValue` needs to be usable for anything **additive**. The\n * field is uncontrolled, so without this the only way to append to a draft — a\n * dictated phrase, a picked slash-command, a pasted citation — is to shadow the\n * whole value in app state through `onChange` and hope the two never drift.\n */\n getValue: () => string;\n}\n\n/**\n * The prompt field of a conversation with a model: a textarea that grows with its\n * content, sends on `Enter`, keeps `Shift+Enter` for a newline, and turns into a\n * stop button while a turn is streaming.\n *\n * Uncontrolled on purpose. A draft changes on every keystroke, and lifting that into\n * app state re-renders the whole transcript per character — with a streaming answer\n * above, that is the one place where \"controlled by default\" costs something\n * visible. Apps that need the draft (a persisted composer, a slash-command menu)\n * read it from `onChange` or drive it through the ref.\n *\n * @example\n * <AIChatComposer\n * generating={generating}\n * onSend={(text) => ask(text)}\n * onStop={() => controller.abort()}\n * footer={<small>Claude Opus 5 · pode errar</small>}\n * />\n */\nexport const AIChatComposer = forwardRef<AIChatComposerHandle, AIChatComposerProps>(\n function AIChatComposer(\n {\n onSend,\n onStop,\n generating = false,\n locale = \"pt-BR\",\n actions,\n footer,\n maxRows = 8,\n onError,\n className,\n disabled,\n placeholder,\n onKeyDown,\n onChange,\n ...rest\n },\n ref,\n ) {\n const strings = aiChatStrings(locale);\n const textarea = useRef<HTMLTextAreaElement | null>(null);\n const [value, setValue] = useState(\"\");\n const [busy, setBusy] = useState(false);\n\n useImperativeHandle(ref, () => ({\n focus: () => textarea.current?.focus(),\n setValue: (text: string) => {\n setValue(text);\n textarea.current?.focus();\n },\n getValue: () => value,\n }));\n\n /**\n * Grow the field to fit its content, up to `maxRows`.\n *\n * Measured from `scrollHeight` after resetting the height, because\n * `scrollHeight` on an element that is already tall enough reports the current\n * height and the field would never shrink back.\n */\n useLayoutEffect(() => {\n const node = textarea.current;\n if (!node) return;\n node.style.height = \"auto\";\n const lineHeight = Number.parseFloat(getComputedStyle(node).lineHeight) || 20;\n const max = lineHeight * maxRows;\n node.style.height = `${Math.min(node.scrollHeight, max)}px`;\n node.style.overflowY = node.scrollHeight > max ? \"auto\" : \"hidden\";\n }, [value, maxRows]);\n\n const submit = async (): Promise<void> => {\n const text = value.trim();\n if (!text || busy || disabled || generating) return;\n setBusy(true);\n try {\n await onSend(text);\n setValue(\"\");\n } catch (error) {\n onError?.(error);\n } finally {\n setBusy(false);\n }\n };\n\n const handleSubmit = (event: FormEvent): void => {\n event.preventDefault();\n void submit();\n };\n\n /**\n * `Enter` sends, `Shift+Enter` breaks the line, `Escape` aborts a turn in\n * flight.\n *\n * The IME check is not optional: while composing Japanese or Korean, `Enter`\n * confirms the candidate word and `keyCode === 229` marks that keystroke.\n * Sending there would post half a word and eat the confirmation.\n */\n const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>): void => {\n onKeyDown?.(event);\n if (event.defaultPrevented) return;\n if (event.key === \"Escape\" && generating && onStop) {\n event.preventDefault();\n onStop();\n return;\n }\n if (event.key !== \"Enter\" || event.shiftKey) return;\n if (event.nativeEvent.isComposing || event.keyCode === 229) return;\n event.preventDefault();\n void submit();\n };\n\n const showStop = generating && onStop !== undefined;\n\n return (\n <form className={cn(styles.composer, className)} onSubmit={handleSubmit}>\n <div className={styles.composerBox}>\n <textarea\n {...rest}\n ref={textarea}\n className={styles.field}\n rows={1}\n value={value}\n disabled={disabled}\n placeholder={placeholder ?? strings.placeholder}\n onChange={(event) => {\n setValue(event.target.value);\n onChange?.(event);\n }}\n onKeyDown={handleKeyDown}\n />\n <div className={styles.composerActions}>\n {actions}\n {showStop ? (\n <button type=\"button\" className={styles.stop} onClick={onStop}>\n <span aria-hidden=\"true\" className={styles.stopGlyph} />\n {strings.stop}\n </button>\n ) : (\n <button\n type=\"submit\"\n className={styles.send}\n disabled={disabled || busy || generating || value.trim() === \"\"}\n >\n {strings.send}\n </button>\n )}\n </div>\n </div>\n {footer && <div className={styles.composerFooter}>{footer}</div>}\n </form>\n );\n },\n);\n"],"mappings":";;;;;;AAyFA,IAAa,IAAiB,EAC1B,SACI,EACI,WACA,WACA,gBAAa,IACb,YAAS,SACT,YACA,WACA,aAAU,GACV,YACA,cACA,aACA,gBACA,cACA,aACA,GAAG,KAEP,GACF;CACE,IAAM,IAAU,EAAc,CAAM,GAC9B,IAAW,EAAmC,IAAI,GAClD,CAAC,GAAO,KAAY,EAAS,EAAE,GAC/B,CAAC,GAAM,KAAW,EAAS,EAAK;CAkBtC,AAhBA,EAAoB,UAAY;EAC5B,aAAa,EAAS,SAAS,MAAM;EACrC,WAAW,MAAiB;GAExB,AADA,EAAS,CAAI,GACb,EAAS,SAAS,MAAM;EAC5B;EACA,gBAAgB;CACpB,EAAE,GASF,QAAsB;EAClB,IAAM,IAAO,EAAS;EACtB,IAAI,CAAC,GAAM;EACX,EAAK,MAAM,SAAS;EAEpB,IAAM,KADa,OAAO,WAAW,iBAAiB,CAAI,CAAC,CAAC,UAAU,KAAK,MAClD;EAEzB,AADA,EAAK,MAAM,SAAS,GAAG,KAAK,IAAI,EAAK,cAAc,CAAG,EAAE,KACxD,EAAK,MAAM,YAAY,EAAK,eAAe,IAAM,SAAS;CAC9D,GAAG,CAAC,GAAO,CAAO,CAAC;CAEnB,IAAM,IAAS,YAA2B;EACtC,IAAM,IAAO,EAAM,KAAK;EACpB,OAAC,KAAQ,KAAQ,KAAY,IACjC;KAAQ,EAAI;GACZ,IAAI;IAEA,AADA,MAAM,EAAO,CAAI,GACjB,EAAS,EAAE;GACf,SAAS,GAAO;IACZ,IAAU,CAAK;GACnB,UAAU;IACN,EAAQ,EAAK;GACjB;EARY;CAShB,GAEM,KAAgB,MAA2B;EAE7C,AADA,EAAM,eAAe,GACrB,EAAY;CAChB,GAUM,KAAiB,MAAoD;EACvE,QAAY,CAAK,GACb,GAAM,kBACV;OAAI,EAAM,QAAQ,YAAY,KAAc,GAAQ;IAEhD,AADA,EAAM,eAAe,GACrB,EAAO;IACP;GACJ;GACI,EAAM,QAAQ,WAAW,EAAM,YAC/B,EAAM,YAAY,eAAe,EAAM,YAAY,QACvD,EAAM,eAAe,GACrB,EAAY;EAJZ;CAKJ,GAEM,IAAW,KAAc,MAAW,KAAA;CAE1C,OACI,kBAAC,QAAD;EAAM,WAAW,EAAG,EAAO,UAAU,CAAS;EAAG,UAAU;YAA3D,CACI,kBAAC,OAAD;GAAK,WAAW,EAAO;aAAvB,CACI,kBAAC,YAAD;IACI,GAAI;IACJ,KAAK;IACL,WAAW,EAAO;IAClB,MAAM;IACC;IACG;IACV,aAAa,KAAe,EAAQ;IACpC,WAAW,MAAU;KAEjB,AADA,EAAS,EAAM,OAAO,KAAK,GAC3B,IAAW,CAAK;IACpB;IACA,WAAW;GACd,CAAA,GACD,kBAAC,OAAD;IAAK,WAAW,EAAO;cAAvB,CACK,GACA,IACG,kBAAC,UAAD;KAAQ,MAAK;KAAS,WAAW,EAAO;KAAM,SAAS;eAAvD,CACI,kBAAC,QAAD;MAAM,eAAY;MAAO,WAAW,EAAO;KAAY,CAAA,GACtD,EAAQ,IACL;SAER,kBAAC,UAAD;KACI,MAAK;KACL,WAAW,EAAO;KAClB,UAAU,KAAY,KAAQ,KAAc,EAAM,KAAK,MAAM;eAE5D,EAAQ;IACL,CAAA,CAEX;KACJ;MACJ,KAAU,kBAAC,OAAD;GAAK,WAAW,EAAO;aAAiB;EAAY,CAAA,CAC7D;;AAEd,CACJ"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
const e=require("../../utils/cn.cjs"),t=require("
|
|
2
|
-
`).length+1,10),onChange:e=>R(e.target.value)}),(0,l.jsxs)(`div`,{className:o.default.editActions,children:[(0,l.jsx)(`button`,{type:`button`,className:o.default.ghost,onClick:()=>{R(E),I(!1)},children:w.cancel}),(0,l.jsx)(`button`,{type:`button`,className:o.default.primary,disabled:z||L.trim()===``,onClick:()=>void G(),children:w.save})]})]}):(0,l.jsxs)(`div`,{className:e.cn(o.default.body,O&&o.default.streamingBody),children:[W,O===!0&&E===``&&(0,l.jsxs)(`span`,{className:o.default.dots,"aria-hidden":`true`,children:[(0,l.jsx)(`span`,{}),(0,l.jsx)(`span`,{}),(0,l.jsx)(`span`,{})]})]}),k&&(0,l.jsxs)(`p`,{className:o.default.error,role:`alert`,children:[(0,l.jsx)(`span`,{children:k}),v&&(0,l.jsx)(`button`,{type:`button`,className:o.default.retry,onClick:()=>v(d),children:w.retry})]}),q&&(0,l.jsxs)(`div`,{className:o.default.actions,"aria-label":w.turnActions,role:`group`,children:[(0,l.jsx)(
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("../VisuallyHidden/VisuallyHidden.cjs"),n=require("../../utils/numbers.cjs"),r=require("../CopyButton/CopyButton.cjs"),i=require("../Collapsible/Collapsible.cjs"),a=require("./ai-chat-turns.cjs"),o=require("./AIChat.module.cjs"),s=require("../Markdown/Markdown.cjs");let c=require("react"),l=require("react/jsx-runtime"),u=require("lucide-react");function d({message:d,locale:f=`pt-BR`,canRegenerate:p=!1,onRegenerate:m,onFeedback:h,onEditSubmit:g,onEditError:_,onRetry:v,renderAvatar:y,renderContent:b,vote:x,defaultReasoningOpen:S=!1,showLineNumbers:C=!1}){let w=a.aiChatStrings(f),{role:T,content:E,reasoning:D,streaming:O,error:k,model:A,createdAt:j,attachments:M}=d,[N,P]=(0,c.useState)(null),[F,I]=(0,c.useState)(!1),[L,R]=(0,c.useState)(E),[z,B]=(0,c.useState)(!1),V=x??N,H=T===`user`,U=(0,c.useMemo)(()=>(0,l.jsx)(s.Markdown,{source:E,showLineNumbers:C,className:o.default.markdownRoot}),[E,C]),W=b?b(d):H?(0,l.jsx)(`div`,{className:o.default.plain,children:E}):E===``?null:U,G=async()=>{let e=L.trim();if(!(!e||z||!g)){B(!0);try{await g(d,e),I(!1)}catch(e){_?.(e)}finally{B(!1)}}},K=e=>{V!==e&&(P(e),h?.(d,e))},q=!O&&!F,J=h!==void 0||p&&m!==void 0;return(0,l.jsxs)(`article`,{className:e.cn(o.default.turn,H&&o.default.userTurn,T===`system`&&o.default.systemTurn,O&&o.default.streamingTurn),"aria-busy":O||void 0,"data-role":T,children:[(0,l.jsxs)(`header`,{className:o.default.turnHeader,children:[y&&(0,l.jsx)(`span`,{className:o.default.avatar,"aria-hidden":`true`,children:y(d)}),(0,l.jsx)(`span`,{className:e.cn(o.default.role,H&&o.default.visuallyHiddenRole),children:a.roleLabel(T,w)}),A&&!H&&(0,l.jsx)(`span`,{className:o.default.model,children:A}),j!==void 0&&(0,l.jsx)(`time`,{className:o.default.time,dateTime:new Date(j).toISOString(),children:a.turnTime(j,f)})]}),M&&M.length>0&&(0,l.jsx)(`ul`,{className:o.default.attachments,children:M.map(e=>(0,l.jsx)(`li`,{className:o.default.attachment,children:e.url?(0,l.jsx)(`img`,{className:o.default.thumb,src:e.url,alt:e.name}):(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(`span`,{className:o.default.attachmentName,children:e.name}),(0,l.jsx)(`span`,{className:o.default.attachmentMeta,children:e.size===void 0?e.mimeType:n.formatBytes(e.size)})]})},e.id))}),D&&(0,l.jsx)(i.Collapsible,{className:o.default.reasoning,defaultOpen:S||O===!0&&E===``,trigger:(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(u.ChevronRight,{className:o.default.reasoningChevron,size:14,"aria-hidden":!0}),w.reasoning]}),children:(0,l.jsx)(`div`,{className:o.default.reasoningBody,children:D})}),F?(0,l.jsxs)(`div`,{className:o.default.editor,children:[(0,l.jsx)(t.VisuallyHidden,{children:(0,l.jsx)(`label`,{htmlFor:`${d.id}-edit`,children:w.editing})}),(0,l.jsx)(`textarea`,{id:`${d.id}-edit`,className:o.default.editField,value:L,rows:Math.min(L.split(`
|
|
2
|
+
`).length+1,10),onChange:e=>R(e.target.value)}),(0,l.jsxs)(`div`,{className:o.default.editActions,children:[(0,l.jsx)(`button`,{type:`button`,className:o.default.ghost,onClick:()=>{R(E),I(!1)},children:w.cancel}),(0,l.jsx)(`button`,{type:`button`,className:o.default.primary,disabled:z||L.trim()===``,onClick:()=>void G(),children:w.save})]})]}):(0,l.jsxs)(`div`,{className:e.cn(o.default.body,O&&o.default.streamingBody),children:[W,O===!0&&E===``&&(0,l.jsxs)(`span`,{className:o.default.dots,"aria-hidden":`true`,children:[(0,l.jsx)(`span`,{}),(0,l.jsx)(`span`,{}),(0,l.jsx)(`span`,{})]})]}),k&&(0,l.jsxs)(`p`,{className:o.default.error,role:`alert`,children:[(0,l.jsx)(`span`,{children:k}),v&&(0,l.jsx)(`button`,{type:`button`,className:o.default.retry,onClick:()=>v(d),children:w.retry})]}),q&&(0,l.jsxs)(`div`,{className:o.default.actions,"aria-label":w.turnActions,role:`group`,children:[(0,l.jsx)(r.CopyButton,{className:o.default.action,value:E,"aria-label":w.copy,title:w.copy,children:(0,l.jsx)(u.Copy,{size:14,"aria-hidden":!0})}),H&&g&&(0,l.jsx)(`button`,{type:`button`,className:o.default.action,"aria-label":w.edit,title:w.edit,onClick:()=>{R(E),I(!0)},children:(0,l.jsx)(u.Pencil,{size:14,"aria-hidden":!0})}),!H&&J&&(0,l.jsxs)(l.Fragment,{children:[p&&m&&(0,l.jsx)(`button`,{type:`button`,className:o.default.action,"aria-label":w.regenerate,title:w.regenerate,onClick:()=>m(d),children:(0,l.jsx)(u.RefreshCw,{size:14,"aria-hidden":!0})}),h&&(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(`button`,{type:`button`,className:e.cn(o.default.action,V===`up`&&o.default.actionActive),"aria-label":w.good,title:w.good,"aria-pressed":V===`up`,onClick:()=>K(`up`),children:(0,l.jsx)(u.ThumbsUp,{size:14,"aria-hidden":!0})}),(0,l.jsx)(`button`,{type:`button`,className:e.cn(o.default.action,V===`down`&&o.default.actionActive),"aria-label":w.bad,title:w.bad,"aria-pressed":V===`down`,onClick:()=>K(`down`),children:(0,l.jsx)(u.ThumbsDown,{size:14,"aria-hidden":!0})})]})]})]})]})}exports.AIChatTurn=d;
|
|
3
3
|
//# sourceMappingURL=AIChatTurn.cjs.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cn as e } from "../../utils/cn.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { VisuallyHidden as t } from "../VisuallyHidden/VisuallyHidden.js";
|
|
3
|
+
import { formatBytes as n } from "../../utils/numbers.js";
|
|
4
|
+
import { CopyButton as ee } from "../CopyButton/CopyButton.js";
|
|
5
5
|
import { Collapsible as r } from "../Collapsible/Collapsible.js";
|
|
6
6
|
import { aiChatStrings as i, roleLabel as a, turnTime as o } from "./ai-chat-turns.js";
|
|
7
7
|
import s from "./AIChat.module.js";
|
|
@@ -74,7 +74,7 @@ function b({ message: b, locale: x = "pt-BR", canRegenerate: S = !1, onRegenerat
|
|
|
74
74
|
children: e.name
|
|
75
75
|
}), /* @__PURE__ */ f("span", {
|
|
76
76
|
className: s.attachmentMeta,
|
|
77
|
-
children: e.size === void 0 ? e.mimeType :
|
|
77
|
+
children: e.size === void 0 ? e.mimeType : n(e.size)
|
|
78
78
|
})] })
|
|
79
79
|
}, e.id))
|
|
80
80
|
}),
|
|
@@ -94,7 +94,7 @@ function b({ message: b, locale: x = "pt-BR", canRegenerate: S = !1, onRegenerat
|
|
|
94
94
|
W ? /* @__PURE__ */ p("div", {
|
|
95
95
|
className: s.editor,
|
|
96
96
|
children: [
|
|
97
|
-
/* @__PURE__ */ f(
|
|
97
|
+
/* @__PURE__ */ f(t, { children: /* @__PURE__ */ f("label", {
|
|
98
98
|
htmlFor: `${b.id}-edit`,
|
|
99
99
|
children: N.editing
|
|
100
100
|
}) }),
|
|
@@ -150,7 +150,7 @@ function b({ message: b, locale: x = "pt-BR", canRegenerate: S = !1, onRegenerat
|
|
|
150
150
|
"aria-label": N.turnActions,
|
|
151
151
|
role: "group",
|
|
152
152
|
children: [
|
|
153
|
-
/* @__PURE__ */ f(
|
|
153
|
+
/* @__PURE__ */ f(ee, {
|
|
154
154
|
className: s.action,
|
|
155
155
|
value: F,
|
|
156
156
|
"aria-label": N.copy,
|