phoenix_duskmoon 10.0.11 → 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.
@@ -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 || [], event),
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 { ...state, needsSync: true };
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 { ...state, needsSync: true };
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: event.admission ?? state.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.11",
3
+ "version": "10.1.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./priv/static/assets/js/phoenix_duskmoon.js",
@@ -37,20 +37,34 @@
37
37
  "registry": "https://registry.npmjs.org/"
38
38
  },
39
39
  "dependencies": {
40
- "@duskmoon-dev/art-elements": "1.9.3",
41
- "@duskmoon-dev/components": "0.5.2",
42
- "@duskmoon-dev/core": "1.20.6",
43
- "@duskmoon-dev/css-art": "1.20.6",
44
- "@duskmoon-dev/elements": "1.9.3",
45
- "react": "19.3.0",
46
- "react-dom": "19.3.0"
40
+ "@duskmoon-dev/art-elements": "1.10.2",
41
+ "@duskmoon-dev/components": "0.6.0",
42
+ "@duskmoon-dev/core": "1.21.0",
43
+ "@duskmoon-dev/css-art": "1.21.0",
44
+ "@duskmoon-dev/elements": "1.10.2",
45
+ "react": "19.3.1",
46
+ "react-dom": "19.3.1"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@mdi/svg": "7.4.47",
50
50
  "bootstrap-icons": "1.13.2",
51
- "postcss": "8.5.29"
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
  }