phoenix_duskmoon 10.0.12 → 10.1.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/CHANGELOG.md +29 -0
- package/assets/js/react_chat/chat.js +15 -16
- package/assets/js/react_chat/index.js +451 -40
- package/assets/js/react_chat/protocol.js +125 -0
- package/assets/js/react_chat/state.js +134 -5
- package/assets/js/react_form/index.js +1 -1
- package/package.json +19 -5
- package/priv/static/assets/css/app-79cf21e84f298332a3addc11d02afa5c.css +2 -0
- package/priv/static/assets/css/app-79cf21e84f298332a3addc11d02afa5c.css.gz +0 -0
- package/priv/static/assets/css/app.css +2 -2
- package/priv/static/assets/css/app.css.gz +0 -0
- package/priv/static/assets/js/{phoenix_duskmoon-c22eb67823f82d7b0d27eba5dbd7be12.js → phoenix_duskmoon-7a95bc687b7d72a85fec06221386e515.js} +1 -1
- package/priv/static/assets/js/{phoenix_duskmoon-c22eb67823f82d7b0d27eba5dbd7be12.js.gz → phoenix_duskmoon-7a95bc687b7d72a85fec06221386e515.js.gz} +0 -0
- package/priv/static/assets/js/phoenix_duskmoon.js +1 -1
- package/priv/static/assets/js/phoenix_duskmoon.js.gz +0 -0
- package/priv/static/cache_manifest.json +2 -2
- package/priv/static/assets/css/app-4291d9872af4341865a2a4ff6537d5ea.css +0 -2
- package/priv/static/assets/css/app-4291d9872af4341865a2a4ff6537d5ea.css.gz +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ComposerUncertainTransportError,
|
|
3
|
+
snapshotComposerReceipt,
|
|
4
|
+
snapshotComposerSubmission,
|
|
5
|
+
} from "@duskmoon-dev/components/chat";
|
|
6
|
+
|
|
7
|
+
export function requestId() {
|
|
8
|
+
if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
9
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
10
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
11
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
12
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0"));
|
|
13
|
+
return [
|
|
14
|
+
hex.slice(0, 4),
|
|
15
|
+
hex.slice(4, 6),
|
|
16
|
+
hex.slice(6, 8),
|
|
17
|
+
hex.slice(8, 10),
|
|
18
|
+
hex.slice(10),
|
|
19
|
+
]
|
|
20
|
+
.map((part) => part.join(""))
|
|
21
|
+
.join("-");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Compatible with the upstream session controller's uncertain-transport boundary.
|
|
25
|
+
export class ChatTransportError extends ComposerUncertainTransportError {
|
|
26
|
+
constructor(message, correlation, cause) {
|
|
27
|
+
super(message, { cause });
|
|
28
|
+
this.name = "ChatTransportError";
|
|
29
|
+
this.outcome = "unknown";
|
|
30
|
+
Object.assign(this, correlation);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function unknownReceipt(snapshot, error) {
|
|
35
|
+
return Object.freeze({
|
|
36
|
+
protocolVersion: 1,
|
|
37
|
+
clientSubmissionId: snapshot.clientSubmissionId,
|
|
38
|
+
scope: snapshot.scope,
|
|
39
|
+
operation: snapshot.operation,
|
|
40
|
+
outcome: "unknown",
|
|
41
|
+
error: {
|
|
42
|
+
code: error.code || "uncertain_transport",
|
|
43
|
+
message: error.message,
|
|
44
|
+
...(error.request_id
|
|
45
|
+
? {
|
|
46
|
+
details: {
|
|
47
|
+
request_id: error.request_id,
|
|
48
|
+
instanceGeneration: error.instanceGeneration,
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
: {}),
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Reject binary handles before any JSON serialization, including the legacy path.
|
|
57
|
+
export function assertChatJSON(value, ancestors = new Set()) {
|
|
58
|
+
if (value == null || ["string", "boolean"].includes(typeof value)) return;
|
|
59
|
+
if (typeof value === "number" && Number.isFinite(value)) return;
|
|
60
|
+
if (typeof value !== "object" || ancestors.has(value))
|
|
61
|
+
throw new TypeError("Chat payload must contain portable JSON only");
|
|
62
|
+
if (
|
|
63
|
+
!Array.isArray(value) &&
|
|
64
|
+
Object.getPrototypeOf(value) !== Object.prototype &&
|
|
65
|
+
Object.getPrototypeOf(value) !== null
|
|
66
|
+
)
|
|
67
|
+
throw new TypeError(
|
|
68
|
+
"Upload File/Blob objects through an application adapter before submitting JSON",
|
|
69
|
+
);
|
|
70
|
+
ancestors.add(value);
|
|
71
|
+
for (const item of Object.values(value)) assertChatJSON(item, ancestors);
|
|
72
|
+
ancestors.delete(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function submissionSnapshot(payload, state) {
|
|
76
|
+
if (state.protocolVersion !== 1)
|
|
77
|
+
throw new TypeError("Structured composer protocol is not enabled");
|
|
78
|
+
const { request_id, ...submission } = payload;
|
|
79
|
+
assertChatJSON(submission);
|
|
80
|
+
const snapshot = snapshotComposerSubmission(submission);
|
|
81
|
+
if (
|
|
82
|
+
snapshot.scope.contextKey !== state.scope.contextKey ||
|
|
83
|
+
snapshot.scope.conversationId !== state.scope.conversationId
|
|
84
|
+
)
|
|
85
|
+
throw new TypeError("Submission scope does not match this chat instance");
|
|
86
|
+
return snapshot;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function structuredSnapshot(payload, state) {
|
|
90
|
+
const snapshot = submissionSnapshot(payload, state);
|
|
91
|
+
const caps = state.composer?.capabilities;
|
|
92
|
+
if (
|
|
93
|
+
caps?.protocolVersion !== 1 ||
|
|
94
|
+
!caps.documentVersions?.includes(snapshot.document.schemaVersion)
|
|
95
|
+
)
|
|
96
|
+
throw new TypeError("Unsupported composer document version");
|
|
97
|
+
if (
|
|
98
|
+
snapshot.operation === "command"
|
|
99
|
+
? caps.commands !== true
|
|
100
|
+
: !caps.intents?.includes(snapshot.intent)
|
|
101
|
+
)
|
|
102
|
+
throw new TypeError("Unsupported composer operation or intent");
|
|
103
|
+
if (snapshot.attachments.length && caps.attachments !== true)
|
|
104
|
+
throw new TypeError("Attachments are not supported by this application");
|
|
105
|
+
return snapshot;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function chatJSONKey(value) {
|
|
109
|
+
if (Array.isArray(value)) return `[${value.map(chatJSONKey).join(",")}]`;
|
|
110
|
+
if (value && typeof value === "object")
|
|
111
|
+
return `{${Object.keys(value)
|
|
112
|
+
.sort()
|
|
113
|
+
.map((key) => `${JSON.stringify(key)}:${chatJSONKey(value[key])}`)
|
|
114
|
+
.join(",")}}`;
|
|
115
|
+
return JSON.stringify(value);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function correlatedReceipt(response, snapshot, correlation) {
|
|
119
|
+
if (
|
|
120
|
+
response?.request_id !== correlation.request_id ||
|
|
121
|
+
response?.instanceGeneration !== correlation.instanceGeneration
|
|
122
|
+
)
|
|
123
|
+
throw new TypeError("Chat response correlation mismatch");
|
|
124
|
+
return snapshotComposerReceipt(response.receipt, snapshot);
|
|
125
|
+
}
|
|
@@ -1,6 +1,63 @@
|
|
|
1
|
+
export function sameChatScope(left, right) {
|
|
2
|
+
return (
|
|
3
|
+
typeof left?.conversationId === "string" &&
|
|
4
|
+
left.conversationId.length > 0 &&
|
|
5
|
+
typeof left.contextKey === "string" &&
|
|
6
|
+
left.contextKey.length > 0 &&
|
|
7
|
+
left.conversationId === right?.conversationId &&
|
|
8
|
+
left.contextKey === right?.contextKey
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function revision(value) {
|
|
13
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function requestSync(state) {
|
|
17
|
+
return state.needsSync ? state : { ...state, needsSync: true };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function validChatQueue(queue, scope) {
|
|
21
|
+
return (
|
|
22
|
+
queue?.protocolVersion === 1 &&
|
|
23
|
+
sameChatScope(queue.scope, scope) &&
|
|
24
|
+
revision(queue.revision) &&
|
|
25
|
+
Array.isArray(queue.items) &&
|
|
26
|
+
queue.items.every(
|
|
27
|
+
(item) => typeof item?.id === "string" && item.id.length > 0,
|
|
28
|
+
) &&
|
|
29
|
+
new Set(queue.items.map((item) => item.id)).size === queue.items.length
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function validProjection(composer, state) {
|
|
34
|
+
if (!composer || !revision(composer.revision)) return false;
|
|
35
|
+
const queue = composer.queue;
|
|
36
|
+
if (queue == null) return state.composer?.queue == null;
|
|
37
|
+
return (
|
|
38
|
+
validChatQueue(queue, state.scope) &&
|
|
39
|
+
queue.revision >= (state.composer?.queue?.revision ?? -1)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Each queue revision is immutable, independently of capabilities or runtime.
|
|
44
|
+
function nextProjection(composer, state) {
|
|
45
|
+
if (
|
|
46
|
+
state.composer?.queue &&
|
|
47
|
+
composer.queue?.revision === state.composer.queue.revision
|
|
48
|
+
) {
|
|
49
|
+
return { ...composer, queue: state.composer.queue };
|
|
50
|
+
}
|
|
51
|
+
return composer;
|
|
52
|
+
}
|
|
53
|
+
|
|
1
54
|
export function initialState(conversationId, messages = [], application = {}) {
|
|
2
55
|
return {
|
|
3
56
|
conversationId,
|
|
57
|
+
protocolVersion: application.protocolVersion ?? null,
|
|
58
|
+
scope: application.scope ?? null,
|
|
59
|
+
instanceGeneration: application.instanceGeneration ?? null,
|
|
60
|
+
composer: application.composer ?? null,
|
|
4
61
|
messages,
|
|
5
62
|
metadata: application.metadata || {},
|
|
6
63
|
runtime: application.runtime || {},
|
|
@@ -13,12 +70,82 @@ export function initialState(conversationId, messages = [], application = {}) {
|
|
|
13
70
|
|
|
14
71
|
export function reduceChat(state, event) {
|
|
15
72
|
if (event?.conversation_id !== state.conversationId) return state;
|
|
73
|
+
const structured = state.protocolVersion === 1;
|
|
74
|
+
if (
|
|
75
|
+
structured &&
|
|
76
|
+
(event.protocolVersion !== 1 ||
|
|
77
|
+
!sameChatScope(state.scope, event.scope) ||
|
|
78
|
+
state.scope.conversationId !== state.conversationId ||
|
|
79
|
+
!revision(state.instanceGeneration) ||
|
|
80
|
+
event.instanceGeneration !== state.instanceGeneration)
|
|
81
|
+
)
|
|
82
|
+
return state;
|
|
16
83
|
if (["chat.snapshot", "chat.reset"].includes(event.type)) {
|
|
84
|
+
if (structured && !revision(event.composer?.revision))
|
|
85
|
+
return requestSync(state);
|
|
86
|
+
const currentRevision = state.composer?.revision ?? -1;
|
|
87
|
+
if (structured && event.composer.revision < currentRevision) return state;
|
|
88
|
+
if (structured && !validProjection(event.composer, state))
|
|
89
|
+
return requestSync(state);
|
|
90
|
+
if (structured) {
|
|
91
|
+
const sequences = event.sequences ?? {};
|
|
92
|
+
if (
|
|
93
|
+
typeof sequences !== "object" ||
|
|
94
|
+
Array.isArray(sequences) ||
|
|
95
|
+
!Object.values(sequences).every(revision)
|
|
96
|
+
)
|
|
97
|
+
return requestSync(state);
|
|
98
|
+
// Transcript sequence counters and composer revisions are independent.
|
|
99
|
+
// Only a new explicit reset may discard known counters.
|
|
100
|
+
if (
|
|
101
|
+
(event.type !== "chat.reset" ||
|
|
102
|
+
event.composer.revision === currentRevision) &&
|
|
103
|
+
Object.entries(state.sequences).some(
|
|
104
|
+
([messageId, sequence]) => (sequences[messageId] ?? 0) < sequence,
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
return state;
|
|
108
|
+
}
|
|
109
|
+
const unchanged = structured && event.composer.revision === currentRevision;
|
|
17
110
|
return {
|
|
18
|
-
...initialState(state.conversationId, event.messages || [],
|
|
111
|
+
...initialState(state.conversationId, event.messages || [], {
|
|
112
|
+
...event,
|
|
113
|
+
// Protocol mode and application identity can change only at the hook boundary.
|
|
114
|
+
protocolVersion: state.protocolVersion,
|
|
115
|
+
scope: state.scope,
|
|
116
|
+
instanceGeneration: state.instanceGeneration,
|
|
117
|
+
composer: structured
|
|
118
|
+
? unchanged
|
|
119
|
+
? state.composer
|
|
120
|
+
: nextProjection(event.composer, state)
|
|
121
|
+
: state.composer,
|
|
122
|
+
...(unchanged
|
|
123
|
+
? { runtime: state.runtime, admission: state.admission }
|
|
124
|
+
: {}),
|
|
125
|
+
}),
|
|
19
126
|
sequences: event.sequences || {},
|
|
20
127
|
};
|
|
21
128
|
}
|
|
129
|
+
if (event.type === "chat.composer-state") {
|
|
130
|
+
if (!structured || state.needsSync) return state;
|
|
131
|
+
if (!revision(event.composer?.revision)) return requestSync(state);
|
|
132
|
+
const currentRevision = state.composer?.revision ?? -1;
|
|
133
|
+
if (event.composer.revision <= currentRevision) return state;
|
|
134
|
+
if (
|
|
135
|
+
(event.previousRevision !== undefined &&
|
|
136
|
+
(!revision(event.previousRevision) ||
|
|
137
|
+
event.previousRevision !== currentRevision)) ||
|
|
138
|
+
!validProjection(event.composer, state)
|
|
139
|
+
)
|
|
140
|
+
return requestSync(state);
|
|
141
|
+
return {
|
|
142
|
+
...state,
|
|
143
|
+
composer: nextProjection(event.composer, state),
|
|
144
|
+
metadata: event.metadata ?? state.metadata,
|
|
145
|
+
runtime: event.runtime ?? state.runtime,
|
|
146
|
+
admission: event.admission ?? state.admission,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
22
149
|
if (
|
|
23
150
|
!event.message_id ||
|
|
24
151
|
!Number.isSafeInteger(event.sequence) ||
|
|
@@ -27,7 +154,7 @@ export function reduceChat(state, event) {
|
|
|
27
154
|
return state;
|
|
28
155
|
const last = state.sequences[event.message_id] || 0;
|
|
29
156
|
if (event.sequence <= last) return state;
|
|
30
|
-
if (event.sequence !== last + 1) return
|
|
157
|
+
if (event.sequence !== last + 1) return requestSync(state);
|
|
31
158
|
if (state.needsSync) return state;
|
|
32
159
|
const index = state.messages.findIndex(
|
|
33
160
|
(message) => message.id === event.message_id,
|
|
@@ -40,7 +167,7 @@ export function reduceChat(state, event) {
|
|
|
40
167
|
? [...messages, event.message]
|
|
41
168
|
: messages.map((m, i) => (i === index ? event.message : m));
|
|
42
169
|
} else {
|
|
43
|
-
if (index < 0) return
|
|
170
|
+
if (index < 0) return requestSync(state);
|
|
44
171
|
const message = messages[index];
|
|
45
172
|
let next;
|
|
46
173
|
switch (event.type) {
|
|
@@ -83,8 +210,10 @@ export function reduceChat(state, event) {
|
|
|
83
210
|
...state,
|
|
84
211
|
messages,
|
|
85
212
|
metadata: event.metadata ?? state.metadata,
|
|
86
|
-
runtime: event.runtime ?? state.runtime,
|
|
87
|
-
admission:
|
|
213
|
+
runtime: structured ? state.runtime : (event.runtime ?? state.runtime),
|
|
214
|
+
admission: structured
|
|
215
|
+
? state.admission
|
|
216
|
+
: (event.admission ?? state.admission),
|
|
88
217
|
sequences: { ...state.sequences, [event.message_id]: event.sequence },
|
|
89
218
|
};
|
|
90
219
|
}
|
|
@@ -71,7 +71,7 @@ export const DuskmoonReactForm = {
|
|
|
71
71
|
nativeControls() {
|
|
72
72
|
const managedPaths = this.fields.map((element) => pathOf(parseJSON(element.dataset.dmReactField).name).map(String));
|
|
73
73
|
return [...this.form.querySelectorAll("input[name], select[name], textarea[name]")].filter((element) => {
|
|
74
|
-
if (element.closest("[data-dm-react-field]")) return false;
|
|
74
|
+
if (element.matches(":disabled") || element.closest("[data-dm-react-field]")) return false;
|
|
75
75
|
const path = pathOf(element.name).map(String);
|
|
76
76
|
return !managedPaths.some((managed) => managed.slice(0, Math.min(path.length, managed.length)).every((key, index) => key === path[index]));
|
|
77
77
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "phoenix_duskmoon",
|
|
3
|
-
"version": "10.0
|
|
3
|
+
"version": "10.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./priv/static/assets/js/phoenix_duskmoon.js",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@duskmoon-dev/art-elements": "1.10.2",
|
|
41
|
-
"@duskmoon-dev/components": "0.
|
|
42
|
-
"@duskmoon-dev/core": "1.
|
|
43
|
-
"@duskmoon-dev/css-art": "1.
|
|
41
|
+
"@duskmoon-dev/components": "0.6.0",
|
|
42
|
+
"@duskmoon-dev/core": "1.21.0",
|
|
43
|
+
"@duskmoon-dev/css-art": "1.21.0",
|
|
44
44
|
"@duskmoon-dev/elements": "1.10.2",
|
|
45
45
|
"react": "19.3.1",
|
|
46
46
|
"react-dom": "19.3.1"
|
|
@@ -51,6 +51,20 @@
|
|
|
51
51
|
"postcss": "8.5.32"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"tailwindcss": "^4.3.2"
|
|
54
|
+
"tailwindcss": "^4.3.2",
|
|
55
|
+
"lexical": "0.53.0",
|
|
56
|
+
"@lexical/history": "0.53.0",
|
|
57
|
+
"@lexical/plain-text": "0.53.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"lexical": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"@lexical/history": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"@lexical/plain-text": {
|
|
67
|
+
"optional": true
|
|
68
|
+
}
|
|
55
69
|
}
|
|
56
70
|
}
|