phoenix_duskmoon 9.16.13 → 9.16.15
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/CHANGELOG.md +27 -0
- package/assets/js/react_chat/chat.js +222 -110
- package/assets/js/react_chat/index.js +122 -96
- package/assets/js/react_chat/state.js +8 -2
- package/assets/js/react_form/component.js +17 -6
- package/assets/js/react_form/index.js +29 -15
- package/assets/js/react_form/values.js +22 -8
- package/package.json +8 -8
- package/priv/static/assets/css/app.css +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
### Added
|
|
4
4
|
|
|
5
|
+
- Public React Chat hook factory, custom composer and renderer integration, and
|
|
6
|
+
application metadata, runtime and admission state across snapshots and events.
|
|
7
|
+
- Opt-in browser constraint validation for classic React forms with
|
|
8
|
+
`novalidate={false}`; schema forms retain JSON Schema validation.
|
|
9
|
+
- Independent classic React form layout through `form_class`, preserving outer
|
|
10
|
+
wrapper `class`; document the upstream schema-form styling limitation.
|
|
5
11
|
- Native Core components: carousel, countdown, diff, keyboard keys, radial progress,
|
|
6
12
|
floating actions, swap, filter group, hero, indicator, join, mask, sidebar layout,
|
|
7
13
|
stack and megamenu, with Storybook examples.
|
|
@@ -9,6 +15,10 @@
|
|
|
9
15
|
|
|
10
16
|
### Changed
|
|
11
17
|
|
|
18
|
+
- Update Core/CSS Art to 1.20.4, Elements/Art Elements to 1.9.1 and React Components
|
|
19
|
+
to 0.5.0, with current supporting JavaScript and Hex dependencies.
|
|
20
|
+
- Support HTTP 0.21.x alongside 0.20.x; the umbrella selects 0.21.3. Update MDEx
|
|
21
|
+
Mermaid to 0.4.0, which escapes diagram source and preserves explicit raw HTML options.
|
|
12
22
|
- Update Elements, Art Elements and declared individual element packages to 1.9.0,
|
|
13
23
|
including Chat Scroll timeline fixes and Markdown's DOMPurify update. Remove the
|
|
14
24
|
retired `el-pin-input` theme selector; native `dm_pin_input` remains supported.
|
|
@@ -26,6 +36,23 @@
|
|
|
26
36
|
|
|
27
37
|
### Fixed
|
|
28
38
|
|
|
39
|
+
- Forward React form field attributes to their interactive controls and preserve
|
|
40
|
+
application-disabled fields when the LiveView connection recovers.
|
|
41
|
+
- Preserve selected-value arrays for native checkbox groups named with `[]`,
|
|
42
|
+
including nested paths, empty selections and per-checkbox reset state.
|
|
43
|
+
- Read current native controls before React form submission, including
|
|
44
|
+
programmatically changed hidden inputs, and delegate events for replaced or
|
|
45
|
+
newly added controls while preserving React-owned values.
|
|
46
|
+
- Resolve production chunk imports by their importing module, preventing identical
|
|
47
|
+
relative paths in unrelated packages from becoming missing browser assets.
|
|
48
|
+
- Preserve lazy custom-element registration and resolve preload URLs relative to
|
|
49
|
+
the emitted bundle when Storybook pages use nested routes.
|
|
50
|
+
- Preserve shared development vendor runtimes and CommonJS exports during cache
|
|
51
|
+
recovery and late imports, with separate vendor caches for each Mix environment
|
|
52
|
+
and a shared initial static graph. Keep vendor URLs stable during discovery and
|
|
53
|
+
refresh cached URLs when Phoenix initializes its asset plug or restarts.
|
|
54
|
+
- Complete required and optional transitive dependencies of nested npm packages,
|
|
55
|
+
and repair incomplete existing lockfiles before reporting successful installation.
|
|
29
56
|
- Restore amd64/arm64 Alpine support across the native build toolchain, with
|
|
30
57
|
published musl binaries and checksums plus Alpine release validation.
|
|
31
58
|
- Code Engine's temporary stylesheet fallback now works after minification and
|
|
@@ -4,8 +4,18 @@ import { Input } from "@duskmoon-dev/components/input";
|
|
|
4
4
|
import { Button } from "@duskmoon-dev/components/button";
|
|
5
5
|
import { Chat } from "@duskmoon-dev/components/chat";
|
|
6
6
|
const h = React.createElement;
|
|
7
|
+
const defaultOptions = {};
|
|
7
8
|
|
|
8
|
-
const Message = memo(function Message(
|
|
9
|
+
const Message = memo(function Message(props) {
|
|
10
|
+
const {
|
|
11
|
+
message,
|
|
12
|
+
bridge,
|
|
13
|
+
targetId,
|
|
14
|
+
timeline,
|
|
15
|
+
options,
|
|
16
|
+
connected,
|
|
17
|
+
synchronizing,
|
|
18
|
+
} = props;
|
|
9
19
|
const [notice, setNotice] = useState("");
|
|
10
20
|
return h(
|
|
11
21
|
Chat,
|
|
@@ -16,7 +26,9 @@ const Message = memo(function Message({ message, bridge, targetId, timeline }) {
|
|
|
16
26
|
timeline,
|
|
17
27
|
"data-message-id": message.id,
|
|
18
28
|
},
|
|
19
|
-
|
|
29
|
+
options.renderHeader
|
|
30
|
+
? h(options.renderHeader, props)
|
|
31
|
+
: h(Chat.Header, null, message.author || message.role),
|
|
20
32
|
h(
|
|
21
33
|
Chat.Bubble,
|
|
22
34
|
{ streaming: message.status === "streaming" },
|
|
@@ -24,114 +36,229 @@ const Message = memo(function Message({ message, bridge, targetId, timeline }) {
|
|
|
24
36
|
h(
|
|
25
37
|
Chat.Tool,
|
|
26
38
|
{ key: tool.id, status: tool.status || "running" },
|
|
27
|
-
h(
|
|
39
|
+
h(
|
|
40
|
+
Chat.ToolHeader,
|
|
41
|
+
null,
|
|
42
|
+
`${tool.name || tool.id}: ${tool.status || "running"}`,
|
|
43
|
+
),
|
|
28
44
|
h(
|
|
29
45
|
tool.result == null ? Chat.ToolCall : Chat.ToolResult,
|
|
30
46
|
null,
|
|
31
|
-
h(
|
|
47
|
+
h(
|
|
48
|
+
"pre",
|
|
49
|
+
null,
|
|
50
|
+
JSON.stringify(tool.result ?? tool.input ?? {}, null, 2),
|
|
51
|
+
),
|
|
32
52
|
),
|
|
33
53
|
),
|
|
34
54
|
),
|
|
35
|
-
h(Markdown, {
|
|
55
|
+
h(Markdown, {
|
|
56
|
+
className: "chat-bubble-content",
|
|
57
|
+
markdown: message.content || "",
|
|
58
|
+
}),
|
|
36
59
|
),
|
|
37
60
|
message.status === "streaming" &&
|
|
38
|
-
h(Chat.Status, { role: "status" }, h(Chat.StatusItem, null, "Generating…")),
|
|
39
|
-
message.error && h("p", { role: "alert" }, message.error),
|
|
40
|
-
h(
|
|
41
|
-
Chat.Footer,
|
|
42
|
-
null,
|
|
43
61
|
h(
|
|
44
|
-
Chat.
|
|
45
|
-
|
|
46
|
-
h(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
setNotice("Copied");
|
|
55
|
-
} catch {
|
|
56
|
-
setNotice("Copy failed");
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
"Copy",
|
|
61
|
-
),
|
|
62
|
-
["error", "cancelled"].includes(message.status) &&
|
|
63
|
-
h(
|
|
64
|
-
Button,
|
|
65
|
-
{
|
|
66
|
-
type: "button",
|
|
67
|
-
appearance: "ghost",
|
|
68
|
-
onClick: () =>
|
|
69
|
-
bridge
|
|
70
|
-
.send("retry", { message_id: message.id })
|
|
71
|
-
.catch((error) => setNotice(error.message)),
|
|
72
|
-
},
|
|
73
|
-
"Retry",
|
|
74
|
-
),
|
|
75
|
-
...(message.actions || []).map((action) =>
|
|
62
|
+
Chat.Status,
|
|
63
|
+
{ role: "status" },
|
|
64
|
+
h(Chat.StatusItem, null, "Generating…"),
|
|
65
|
+
),
|
|
66
|
+
message.error && h("p", { role: "alert" }, message.error),
|
|
67
|
+
options.renderFooter
|
|
68
|
+
? h(options.renderFooter, props)
|
|
69
|
+
: h(
|
|
70
|
+
Chat.Footer,
|
|
71
|
+
null,
|
|
76
72
|
h(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
73
|
+
Chat.Actions,
|
|
74
|
+
null,
|
|
75
|
+
h(
|
|
76
|
+
Button,
|
|
77
|
+
{
|
|
78
|
+
type: "button",
|
|
79
|
+
appearance: "ghost",
|
|
80
|
+
onClick: async () => {
|
|
81
|
+
try {
|
|
82
|
+
await navigator.clipboard.writeText(message.content || "");
|
|
83
|
+
setNotice("Copied");
|
|
84
|
+
} catch {
|
|
85
|
+
setNotice("Copy failed");
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
"Copy",
|
|
90
|
+
),
|
|
91
|
+
["error", "cancelled"].includes(message.status) &&
|
|
92
|
+
h(
|
|
93
|
+
Button,
|
|
94
|
+
{
|
|
95
|
+
type: "button",
|
|
96
|
+
appearance: "ghost",
|
|
97
|
+
disabled: !connected || synchronizing,
|
|
98
|
+
onClick: () =>
|
|
99
|
+
bridge
|
|
100
|
+
.send("retry", { message_id: message.id })
|
|
101
|
+
.catch((error) => setNotice(error.message)),
|
|
102
|
+
},
|
|
103
|
+
"Retry",
|
|
104
|
+
),
|
|
105
|
+
...(message.actions || []).map((action) =>
|
|
106
|
+
h(
|
|
107
|
+
Button,
|
|
108
|
+
{
|
|
109
|
+
key: action.id,
|
|
110
|
+
disabled:
|
|
111
|
+
action.disabled ||
|
|
112
|
+
action.eligible === false ||
|
|
113
|
+
!connected ||
|
|
114
|
+
synchronizing,
|
|
115
|
+
type: "button",
|
|
116
|
+
appearance: "ghost",
|
|
117
|
+
onClick: () =>
|
|
118
|
+
bridge
|
|
119
|
+
.send("action", {
|
|
120
|
+
message_id: message.id,
|
|
121
|
+
action: action.id,
|
|
122
|
+
})
|
|
123
|
+
.catch((error) => setNotice(error.message)),
|
|
124
|
+
},
|
|
125
|
+
action.label,
|
|
126
|
+
),
|
|
127
|
+
),
|
|
88
128
|
),
|
|
89
129
|
),
|
|
90
|
-
),
|
|
91
|
-
),
|
|
92
130
|
h("span", { role: "status" }, notice),
|
|
93
131
|
);
|
|
94
132
|
});
|
|
95
133
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const [draft, setDraft] = useState("");
|
|
134
|
+
// A composer owns its draft/attachments. This helper only owns admission and UI requests.
|
|
135
|
+
export function useChatSubmission({ bridge, canSubmit }) {
|
|
99
136
|
const [busy, setBusy] = useState(false);
|
|
100
137
|
const [notice, setNotice] = useState("");
|
|
101
138
|
const pending = useRef(false);
|
|
102
139
|
const alive = useRef(true);
|
|
103
|
-
const scroll = useRef(null);
|
|
104
|
-
const follow = useRef(true);
|
|
105
|
-
useEffect(
|
|
106
|
-
() => () => {
|
|
107
|
-
alive.current = false;
|
|
108
|
-
},
|
|
109
|
-
[],
|
|
110
|
-
);
|
|
111
140
|
useEffect(() => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (!draft.trim() || pending.current || streaming || !connected || state.needsSync) return;
|
|
121
|
-
const text = draft;
|
|
141
|
+
alive.current = true;
|
|
142
|
+
return () => {
|
|
143
|
+
alive.current = false;
|
|
144
|
+
};
|
|
145
|
+
}, []);
|
|
146
|
+
async function submit(payload) {
|
|
147
|
+
if (!canSubmit || pending.current)
|
|
148
|
+
throw new Error("Chat is not accepting messages");
|
|
122
149
|
pending.current = true;
|
|
123
150
|
setBusy(true);
|
|
124
151
|
setNotice("");
|
|
125
152
|
try {
|
|
126
|
-
await bridge.send("send", {
|
|
127
|
-
|
|
153
|
+
return await bridge.send("send", {
|
|
154
|
+
request_id: crypto.randomUUID(),
|
|
155
|
+
...payload,
|
|
156
|
+
});
|
|
128
157
|
} catch (error) {
|
|
129
158
|
if (alive.current) setNotice(error.message);
|
|
159
|
+
throw error;
|
|
130
160
|
} finally {
|
|
131
161
|
pending.current = false;
|
|
132
162
|
if (alive.current) setBusy(false);
|
|
133
163
|
}
|
|
134
164
|
}
|
|
165
|
+
return { submit, busy, notice };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function DefaultComposer(props) {
|
|
169
|
+
const { bridge, connected, active, canSubmit } = props;
|
|
170
|
+
const [draft, setDraft] = useState("");
|
|
171
|
+
const { submit, busy, notice } = useChatSubmission(props);
|
|
172
|
+
const [stopNotice, setStopNotice] = useState("");
|
|
173
|
+
async function onSubmit(event) {
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
if (!draft.trim() || busy || !canSubmit) return;
|
|
176
|
+
const text = draft;
|
|
177
|
+
try {
|
|
178
|
+
await submit({ text });
|
|
179
|
+
setDraft((current) => (current === text ? "" : current));
|
|
180
|
+
} catch {
|
|
181
|
+
/* The helper preserves the draft and renders the rejection. */
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return h(
|
|
185
|
+
React.Fragment,
|
|
186
|
+
null,
|
|
187
|
+
(notice || stopNotice) && h("p", { role: "alert" }, notice || stopNotice),
|
|
188
|
+
h(
|
|
189
|
+
"form",
|
|
190
|
+
{ className: "flex gap-2", onSubmit },
|
|
191
|
+
h(Input, {
|
|
192
|
+
value: draft,
|
|
193
|
+
onChange: (event) => setDraft(event.target.value),
|
|
194
|
+
"aria-label": "Message",
|
|
195
|
+
placeholder: "Write a message",
|
|
196
|
+
}),
|
|
197
|
+
h(
|
|
198
|
+
Button,
|
|
199
|
+
{ type: "submit", disabled: busy || !canSubmit || !draft.trim() },
|
|
200
|
+
"Send",
|
|
201
|
+
),
|
|
202
|
+
active &&
|
|
203
|
+
h(
|
|
204
|
+
Button,
|
|
205
|
+
{
|
|
206
|
+
type: "button",
|
|
207
|
+
disabled: !connected,
|
|
208
|
+
onClick: () =>
|
|
209
|
+
bridge
|
|
210
|
+
.send("stop", {})
|
|
211
|
+
.catch((error) => setStopNotice(error.message)),
|
|
212
|
+
},
|
|
213
|
+
"Stop",
|
|
214
|
+
),
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function ReactChat({
|
|
220
|
+
state,
|
|
221
|
+
bridge,
|
|
222
|
+
connected,
|
|
223
|
+
label,
|
|
224
|
+
options = defaultOptions,
|
|
225
|
+
}) {
|
|
226
|
+
const transcriptId = useId();
|
|
227
|
+
const scroll = useRef(null);
|
|
228
|
+
const follow = useRef(true);
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (follow.current && scroll.current)
|
|
231
|
+
scroll.current.scrollTop = scroll.current.scrollHeight;
|
|
232
|
+
}, [state.messages]);
|
|
233
|
+
const streaming = state.messages.some(
|
|
234
|
+
(message) => message.status === "streaming",
|
|
235
|
+
);
|
|
236
|
+
const active = state.runtime.active ?? streaming;
|
|
237
|
+
const canSubmit =
|
|
238
|
+
connected &&
|
|
239
|
+
!state.needsSync &&
|
|
240
|
+
state.admission.canSubmit !== false &&
|
|
241
|
+
(!active || state.admission.allowWhileStreaming === true);
|
|
242
|
+
const context = {
|
|
243
|
+
state,
|
|
244
|
+
bridge,
|
|
245
|
+
connected,
|
|
246
|
+
synchronizing: state.needsSync,
|
|
247
|
+
metadata: state.metadata,
|
|
248
|
+
runtime: state.runtime,
|
|
249
|
+
admission: state.admission,
|
|
250
|
+
streaming,
|
|
251
|
+
active,
|
|
252
|
+
canSubmit,
|
|
253
|
+
};
|
|
254
|
+
const replies = state.messages
|
|
255
|
+
.filter((message) => message.role === "assistant")
|
|
256
|
+
.slice(-24);
|
|
257
|
+
const timelines = new Map(
|
|
258
|
+
replies.map((message, index) => [message.id, index + 1]),
|
|
259
|
+
);
|
|
260
|
+
const targetId = (message) =>
|
|
261
|
+
`${transcriptId}-message-${encodeURIComponent(message.id)}`;
|
|
135
262
|
return h(
|
|
136
263
|
"section",
|
|
137
264
|
{ className: "flex flex-col gap-3 h-full", "aria-label": label },
|
|
@@ -143,7 +270,8 @@ export function ReactChat({ state, bridge, connected, label }) {
|
|
|
143
270
|
"data-chat-messages": true,
|
|
144
271
|
onScroll: () => {
|
|
145
272
|
const el = scroll.current;
|
|
146
|
-
follow.current =
|
|
273
|
+
follow.current =
|
|
274
|
+
el.scrollHeight - el.scrollTop - el.clientHeight < 48;
|
|
147
275
|
},
|
|
148
276
|
},
|
|
149
277
|
h(
|
|
@@ -164,7 +292,13 @@ export function ReactChat({ state, bridge, connected, label }) {
|
|
|
164
292
|
Chat.ScrollBody,
|
|
165
293
|
{ className: "gap-3" },
|
|
166
294
|
state.messages.map((message) =>
|
|
167
|
-
h(Message, {
|
|
295
|
+
h(options.renderMessage || Message, {
|
|
296
|
+
...(options.renderMessage ||
|
|
297
|
+
options.renderHeader ||
|
|
298
|
+
options.renderFooter
|
|
299
|
+
? context
|
|
300
|
+
: { connected, synchronizing: state.needsSync }),
|
|
301
|
+
options,
|
|
168
302
|
key: message.id,
|
|
169
303
|
message,
|
|
170
304
|
bridge,
|
|
@@ -190,36 +324,14 @@ export function ReactChat({ state, bridge, connected, label }) {
|
|
|
190
324
|
h(
|
|
191
325
|
"p",
|
|
192
326
|
{ role: "status" },
|
|
193
|
-
connected
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
h(
|
|
197
|
-
"form",
|
|
198
|
-
{ className: "flex gap-2", onSubmit: submit },
|
|
199
|
-
h(Input, {
|
|
200
|
-
value: draft,
|
|
201
|
-
onChange: (event) => setDraft(event.target.value),
|
|
202
|
-
"aria-label": "Message",
|
|
203
|
-
placeholder: "Write a message",
|
|
204
|
-
}),
|
|
205
|
-
h(
|
|
206
|
-
Button,
|
|
207
|
-
{
|
|
208
|
-
type: "submit",
|
|
209
|
-
disabled: busy || streaming || !connected || state.needsSync || !draft.trim(),
|
|
210
|
-
},
|
|
211
|
-
"Send",
|
|
327
|
+
connected
|
|
328
|
+
? "Synchronizing conversation…"
|
|
329
|
+
: "Disconnected. Draft preserved.",
|
|
212
330
|
),
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
disabled: !connected,
|
|
219
|
-
onClick: () => bridge.send("stop", {}).catch((error) => setNotice(error.message)),
|
|
220
|
-
},
|
|
221
|
-
"Stop",
|
|
222
|
-
),
|
|
223
|
-
),
|
|
331
|
+
options.renderStatus && h(options.renderStatus, context),
|
|
332
|
+
h(options.Composer || DefaultComposer, {
|
|
333
|
+
...context,
|
|
334
|
+
key: state.conversationId,
|
|
335
|
+
}),
|
|
224
336
|
);
|
|
225
337
|
}
|
|
@@ -3,103 +3,129 @@ import { createRoot } from "react-dom/client";
|
|
|
3
3
|
import { ReactChat } from "./chat.js";
|
|
4
4
|
import { initialState, reduceChat } from "./state.js";
|
|
5
5
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
this.state = reduceChat(this.state, event);
|
|
18
|
-
if (previous === this.state) return;
|
|
19
|
-
if (this.state.needsSync && !previous.needsSync) this.requestSync();
|
|
20
|
-
this.scheduleRender();
|
|
21
|
-
});
|
|
22
|
-
this.renderChat();
|
|
23
|
-
},
|
|
24
|
-
sendChat(action, payload) {
|
|
25
|
-
if (!this.connected || this.dead)
|
|
26
|
-
return Promise.reject(new Error("Chat is disconnected"));
|
|
27
|
-
return new Promise((resolve, reject) => {
|
|
28
|
-
const finish = (error, reply) => {
|
|
29
|
-
if (!this.requests.delete(request)) return;
|
|
30
|
-
clearTimeout(request.timer);
|
|
31
|
-
error ? reject(error) : resolve(reply);
|
|
32
|
-
};
|
|
33
|
-
const request = {
|
|
34
|
-
cancel: () =>
|
|
35
|
-
finish(new Error("Chat disconnected; check status before retrying")),
|
|
36
|
-
};
|
|
37
|
-
this.requests.add(request);
|
|
38
|
-
request.timer = setTimeout(
|
|
39
|
-
() =>
|
|
40
|
-
finish(
|
|
41
|
-
new Error("Chat request timed out; check status before retrying"),
|
|
42
|
-
),
|
|
43
|
-
15000,
|
|
6
|
+
export { ReactChat, useChatSubmission } from "./chat.js";
|
|
7
|
+
export { initialState, reduceChat } from "./state.js";
|
|
8
|
+
|
|
9
|
+
export function createDuskmoonReactChat(options = {}) {
|
|
10
|
+
return {
|
|
11
|
+
mounted() {
|
|
12
|
+
this.config = JSON.parse(this.el.dataset.chat);
|
|
13
|
+
this.state = initialState(
|
|
14
|
+
this.config.conversationId,
|
|
15
|
+
this.config.messages,
|
|
16
|
+
this.config,
|
|
44
17
|
);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
18
|
+
this.root = createRoot(this.el);
|
|
19
|
+
this.connected = true;
|
|
20
|
+
this.requests = new Set();
|
|
21
|
+
this.bridge = {
|
|
22
|
+
send: (action, payload) => this.sendChat(action, payload),
|
|
49
23
|
};
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
24
|
+
this.eventRef = this.handleEvent(this.config.event, (event) => {
|
|
25
|
+
if (event.id !== this.el.id) return;
|
|
26
|
+
const previous = this.state;
|
|
27
|
+
this.state = reduceChat(this.state, event);
|
|
28
|
+
if (previous === this.state) return;
|
|
29
|
+
if (this.state.needsSync && !previous.needsSync) this.requestSync();
|
|
30
|
+
this.scheduleRender();
|
|
31
|
+
});
|
|
32
|
+
this.renderChat();
|
|
33
|
+
},
|
|
34
|
+
sendChat(action, payload) {
|
|
35
|
+
if (!this.connected || this.dead)
|
|
36
|
+
return Promise.reject(new Error("Chat is disconnected"));
|
|
37
|
+
if (action === "send" && this.state.needsSync)
|
|
38
|
+
return Promise.reject(new Error("Chat is synchronizing"));
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const finish = (error, reply) => {
|
|
41
|
+
if (!this.requests.delete(request)) return;
|
|
42
|
+
clearTimeout(request.timer);
|
|
43
|
+
error ? reject(error) : resolve(reply);
|
|
44
|
+
};
|
|
45
|
+
const request = {
|
|
46
|
+
cancel: () =>
|
|
47
|
+
finish(
|
|
48
|
+
new Error("Chat disconnected; check status before retrying"),
|
|
49
|
+
),
|
|
50
|
+
};
|
|
51
|
+
this.requests.add(request);
|
|
52
|
+
request.timer = setTimeout(
|
|
53
|
+
() =>
|
|
54
|
+
finish(
|
|
55
|
+
new Error("Chat request timed out; check status before retrying"),
|
|
56
|
+
),
|
|
57
|
+
15000,
|
|
56
58
|
);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
this.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
59
|
+
const body = {
|
|
60
|
+
...payload,
|
|
61
|
+
id: this.el.id,
|
|
62
|
+
conversation_id: this.state.conversationId,
|
|
63
|
+
};
|
|
64
|
+
const reply = (response) => {
|
|
65
|
+
const error =
|
|
66
|
+
response?.status === "ok"
|
|
67
|
+
? null
|
|
68
|
+
: new Error(response?.message || "Chat request rejected");
|
|
69
|
+
if (error) error.response = response;
|
|
70
|
+
finish(error, response);
|
|
71
|
+
};
|
|
72
|
+
try {
|
|
73
|
+
const event = this.config.actions[action];
|
|
74
|
+
if (this.config.target || this.el.dataset.target)
|
|
75
|
+
this.pushEventTo(
|
|
76
|
+
this.config.target || this.el.dataset.target,
|
|
77
|
+
event,
|
|
78
|
+
body,
|
|
79
|
+
reply,
|
|
80
|
+
);
|
|
81
|
+
else this.pushEvent(event, body, reply);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
finish(error);
|
|
84
|
+
}
|
|
75
85
|
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
86
|
+
},
|
|
87
|
+
requestSync() {
|
|
88
|
+
this.sendChat("sync", { sequences: this.state.sequences }).catch(
|
|
89
|
+
() => {},
|
|
90
|
+
);
|
|
91
|
+
},
|
|
92
|
+
scheduleRender() {
|
|
93
|
+
if (!this.frame)
|
|
94
|
+
this.frame = requestAnimationFrame(() => {
|
|
95
|
+
this.frame = null;
|
|
96
|
+
if (!this.dead) this.renderChat();
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
renderChat() {
|
|
100
|
+
this.root.render(
|
|
101
|
+
React.createElement(ReactChat, {
|
|
102
|
+
state: this.state,
|
|
103
|
+
bridge: this.bridge,
|
|
104
|
+
connected: this.connected,
|
|
105
|
+
label: this.config.label,
|
|
106
|
+
options,
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
},
|
|
110
|
+
disconnected() {
|
|
111
|
+
this.connected = false;
|
|
112
|
+
this.requests.forEach((request) => request.cancel());
|
|
113
|
+
this.renderChat();
|
|
114
|
+
},
|
|
115
|
+
reconnected() {
|
|
116
|
+
this.connected = true;
|
|
117
|
+
this.state = { ...this.state, needsSync: true };
|
|
118
|
+
this.renderChat();
|
|
119
|
+
this.requestSync();
|
|
120
|
+
},
|
|
121
|
+
destroyed() {
|
|
122
|
+
this.dead = true;
|
|
123
|
+
cancelAnimationFrame(this.frame);
|
|
124
|
+
this.removeHandleEvent(this.eventRef);
|
|
125
|
+
this.requests.forEach((request) => request.cancel());
|
|
126
|
+
this.root.unmount();
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const DuskmoonReactChat = createDuskmoonReactChat();
|