@lowdefy/blocks-antd-x 6.0.0 → 7.0.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/dist/blocks/AgentChat/AgentChat.js +38 -774
- package/dist/blocks/AgentChat/AgentChat.lazy.js +933 -0
- package/dist/blocks/AgentChat/DrawerLauncher.js +32 -0
- package/dist/blocks/AgentChat/DrawerWrapper.js +9 -8
- package/dist/blocks/AgentChat/LowdefyChatTransport.js +8 -2
- package/dist/blocks/AgentChat/MessageBubble.js +105 -40
- package/dist/blocks/AgentChat/MessageList.js +20 -4
- package/dist/blocks/AgentChat/formatToolResult.js +2 -1
- package/dist/blocks/AgentChat/meta.js +17 -1
- package/dist/blocks/AgentChat/replayableMessages.js +60 -0
- package/dist/blocks/AgentChat/schema.json +36 -2
- package/dist/blocks/AgentChat/tests/AgentChat.e2e.yaml +237 -0
- package/dist/blocks/AgentConversations/AgentConversations.js +18 -3
- package/dist/blocks/AgentConversations/meta.js +5 -1
- package/dist/blocks/AgentConversations/schema.json +8 -0
- package/dist/blocks/AgentConversations/tests/AgentConversations.e2e.yaml +69 -0
- package/dist/blocks/IntentInput/IntentInput.js +230 -0
- package/dist/blocks/IntentInput/IntentInput.module.css +159 -0
- package/dist/blocks/IntentInput/answerScores.js +34 -0
- package/dist/blocks/IntentInput/gallery.yaml +81 -0
- package/dist/blocks/IntentInput/meta.js +164 -0
- package/dist/blocks/IntentInput/nextIntentState.js +62 -0
- package/dist/blocks.js +1 -0
- package/dist/metas.js +1 -0
- package/dist/types.js +3 -18
- package/package.json +16 -13
|
@@ -119,3 +119,240 @@ blocks:
|
|
|
119
119
|
parts:
|
|
120
120
|
- type: text
|
|
121
121
|
text: '[in-app link](/agent-chat-target) and [external link](https://example.com)'
|
|
122
|
+
|
|
123
|
+
# A history stored after a reply was cut off mid-stream: c2's tool call never got its result,
|
|
124
|
+
# c3 is nothing but that, and c4 is a call that completed and must be left alone.
|
|
125
|
+
- id: chat_cutoff
|
|
126
|
+
type: AgentChat
|
|
127
|
+
properties:
|
|
128
|
+
agentId: test_agent
|
|
129
|
+
height: 300
|
|
130
|
+
conversationId: cutoff
|
|
131
|
+
messages:
|
|
132
|
+
- id: c1
|
|
133
|
+
role: user
|
|
134
|
+
parts:
|
|
135
|
+
- type: text
|
|
136
|
+
text: What is in this repo?
|
|
137
|
+
- id: c2
|
|
138
|
+
role: assistant
|
|
139
|
+
parts:
|
|
140
|
+
- type: step-start
|
|
141
|
+
- type: text
|
|
142
|
+
text: Let me look.
|
|
143
|
+
- type: tool-search
|
|
144
|
+
toolCallId: tc1
|
|
145
|
+
state: input-available
|
|
146
|
+
input: {}
|
|
147
|
+
- id: c3
|
|
148
|
+
role: assistant
|
|
149
|
+
parts:
|
|
150
|
+
- type: step-start
|
|
151
|
+
- type: tool-fetch
|
|
152
|
+
toolCallId: tc2
|
|
153
|
+
state: input-available
|
|
154
|
+
input: {}
|
|
155
|
+
- id: c4
|
|
156
|
+
role: assistant
|
|
157
|
+
parts:
|
|
158
|
+
- type: tool-lookup
|
|
159
|
+
toolCallId: tc3
|
|
160
|
+
state: output-available
|
|
161
|
+
input: {}
|
|
162
|
+
output:
|
|
163
|
+
ok: true
|
|
164
|
+
- type: text
|
|
165
|
+
text: Done looking.
|
|
166
|
+
|
|
167
|
+
# onBeforeSend that takes as long as a quota request might, counts how often it ran, then
|
|
168
|
+
# refuses the send so nothing ever calls the agent.
|
|
169
|
+
- id: chat_guarded
|
|
170
|
+
type: AgentChat
|
|
171
|
+
properties:
|
|
172
|
+
agentId: test_agent
|
|
173
|
+
height: 300
|
|
174
|
+
conversationId: guarded
|
|
175
|
+
messages:
|
|
176
|
+
- id: g1
|
|
177
|
+
role: assistant
|
|
178
|
+
parts:
|
|
179
|
+
- type: text
|
|
180
|
+
text: Ask me something.
|
|
181
|
+
events:
|
|
182
|
+
onBeforeSend:
|
|
183
|
+
- id: count_before_send
|
|
184
|
+
type: SetState
|
|
185
|
+
params:
|
|
186
|
+
before_send_count:
|
|
187
|
+
_sum:
|
|
188
|
+
- _if_none:
|
|
189
|
+
- _state: before_send_count
|
|
190
|
+
- 0
|
|
191
|
+
- 1
|
|
192
|
+
- id: slow_check
|
|
193
|
+
type: Wait
|
|
194
|
+
params:
|
|
195
|
+
ms: 1500
|
|
196
|
+
- id: refuse
|
|
197
|
+
type: Throw
|
|
198
|
+
params:
|
|
199
|
+
throw: true
|
|
200
|
+
message: Refused by the test.
|
|
201
|
+
|
|
202
|
+
- id: guarded_readout
|
|
203
|
+
type: Html
|
|
204
|
+
properties:
|
|
205
|
+
html:
|
|
206
|
+
_nunjucks:
|
|
207
|
+
template: '<span id="readout_before_send">{{ n or 0 }}</span>'
|
|
208
|
+
on:
|
|
209
|
+
n:
|
|
210
|
+
_state: before_send_count
|
|
211
|
+
|
|
212
|
+
- id: chat_styled
|
|
213
|
+
type: AgentChat
|
|
214
|
+
properties:
|
|
215
|
+
agentId: test_agent
|
|
216
|
+
height: 300
|
|
217
|
+
conversationId: styled
|
|
218
|
+
messageDisplay:
|
|
219
|
+
roles:
|
|
220
|
+
assistant:
|
|
221
|
+
styles:
|
|
222
|
+
content:
|
|
223
|
+
backgroundColor: 'rgb(1, 2, 3)'
|
|
224
|
+
user:
|
|
225
|
+
classNames:
|
|
226
|
+
content: styled-user-content
|
|
227
|
+
sender:
|
|
228
|
+
styles:
|
|
229
|
+
root:
|
|
230
|
+
borderColor: 'rgb(4, 5, 6)'
|
|
231
|
+
messages:
|
|
232
|
+
- id: y1
|
|
233
|
+
role: user
|
|
234
|
+
parts:
|
|
235
|
+
- type: text
|
|
236
|
+
text: Styled question
|
|
237
|
+
- id: y2
|
|
238
|
+
role: assistant
|
|
239
|
+
parts:
|
|
240
|
+
- type: text
|
|
241
|
+
text: Styled answer
|
|
242
|
+
|
|
243
|
+
# Two conversations with the SAME message count and the same last id (''), the shape
|
|
244
|
+
# every persisted transcript had before assistant messages were given ids. Switching
|
|
245
|
+
# between them has to re-sync the transcript from `messages`.
|
|
246
|
+
- id: switch_to_a
|
|
247
|
+
type: Button
|
|
248
|
+
properties:
|
|
249
|
+
title: Switch to A
|
|
250
|
+
events:
|
|
251
|
+
onClick:
|
|
252
|
+
- id: set_a
|
|
253
|
+
type: SetState
|
|
254
|
+
params:
|
|
255
|
+
switch_id: conv-a
|
|
256
|
+
- id: switch_to_b
|
|
257
|
+
type: Button
|
|
258
|
+
properties:
|
|
259
|
+
title: Switch to B
|
|
260
|
+
events:
|
|
261
|
+
onClick:
|
|
262
|
+
- id: set_b
|
|
263
|
+
type: SetState
|
|
264
|
+
params:
|
|
265
|
+
switch_id: conv-b
|
|
266
|
+
- id: chat_switch
|
|
267
|
+
type: AgentChat
|
|
268
|
+
properties:
|
|
269
|
+
agentId: test_agent
|
|
270
|
+
height: 300
|
|
271
|
+
conversationId:
|
|
272
|
+
_if_none:
|
|
273
|
+
- _state: switch_id
|
|
274
|
+
- conv-a
|
|
275
|
+
messages:
|
|
276
|
+
_if:
|
|
277
|
+
test:
|
|
278
|
+
_eq:
|
|
279
|
+
- _if_none:
|
|
280
|
+
- _state: switch_id
|
|
281
|
+
- conv-a
|
|
282
|
+
- conv-b
|
|
283
|
+
then:
|
|
284
|
+
- id: b-user
|
|
285
|
+
role: user
|
|
286
|
+
parts:
|
|
287
|
+
- type: text
|
|
288
|
+
text: Question in B
|
|
289
|
+
- id: ''
|
|
290
|
+
role: assistant
|
|
291
|
+
parts:
|
|
292
|
+
- type: text
|
|
293
|
+
text: Answer from conversation B.
|
|
294
|
+
else:
|
|
295
|
+
- id: a-user
|
|
296
|
+
role: user
|
|
297
|
+
parts:
|
|
298
|
+
- type: text
|
|
299
|
+
text: Question in A
|
|
300
|
+
- id: ''
|
|
301
|
+
role: assistant
|
|
302
|
+
parts:
|
|
303
|
+
- type: text
|
|
304
|
+
text: Answer from conversation A.
|
|
305
|
+
|
|
306
|
+
# The loading property: a skeleton in place of the transcript, composer disabled.
|
|
307
|
+
- id: toggle_loading
|
|
308
|
+
type: Button
|
|
309
|
+
properties:
|
|
310
|
+
title: Toggle loading
|
|
311
|
+
events:
|
|
312
|
+
onClick:
|
|
313
|
+
- id: flip_loading
|
|
314
|
+
type: SetState
|
|
315
|
+
params:
|
|
316
|
+
chat_loading:
|
|
317
|
+
_not:
|
|
318
|
+
_eq:
|
|
319
|
+
- _state: chat_loading
|
|
320
|
+
- true
|
|
321
|
+
- id: chat_loading
|
|
322
|
+
type: AgentChat
|
|
323
|
+
properties:
|
|
324
|
+
agentId: test_agent
|
|
325
|
+
height: 300
|
|
326
|
+
conversationId: loading
|
|
327
|
+
loading:
|
|
328
|
+
_eq:
|
|
329
|
+
- _state: chat_loading
|
|
330
|
+
- true
|
|
331
|
+
suggestions:
|
|
332
|
+
- key: s
|
|
333
|
+
label: A suggestion
|
|
334
|
+
messages:
|
|
335
|
+
- id: l1
|
|
336
|
+
role: assistant
|
|
337
|
+
parts:
|
|
338
|
+
- type: text
|
|
339
|
+
text: Loaded answer.
|
|
340
|
+
|
|
341
|
+
# Both post to the agent route, which the spec intercepts to read the request body.
|
|
342
|
+
- id: chat_shared_read_only
|
|
343
|
+
type: AgentChat
|
|
344
|
+
properties:
|
|
345
|
+
agentId: test_agent
|
|
346
|
+
height: 200
|
|
347
|
+
conversationId: shared-read-only
|
|
348
|
+
sharedStateReadOnly: true
|
|
349
|
+
sharedState:
|
|
350
|
+
space: PEL
|
|
351
|
+
- id: chat_shared_writable
|
|
352
|
+
type: AgentChat
|
|
353
|
+
properties:
|
|
354
|
+
agentId: test_agent
|
|
355
|
+
height: 200
|
|
356
|
+
conversationId: shared-writable
|
|
357
|
+
sharedState:
|
|
358
|
+
space: PEL
|
|
@@ -14,9 +14,24 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import React from 'react';
|
|
16
16
|
import { Conversations } from '@ant-design/x';
|
|
17
|
-
import { Button } from 'antd';
|
|
17
|
+
import { Button, Spin } from 'antd';
|
|
18
18
|
function AgentConversations({ blockId, methods, properties }) {
|
|
19
|
-
const { items, activeKey, menu, creation, groupable, width } = properties;
|
|
19
|
+
const { items, activeKey, loadingKey, menu, creation, groupable, width } = properties;
|
|
20
|
+
// Conversations has no loading state of its own, so a loading row swaps its icon slot
|
|
21
|
+
// for a spinner. aria-busy and data-loading reach the <li> through the item's DOM attrs.
|
|
22
|
+
const conversationItems = (items ?? []).map((item)=>{
|
|
23
|
+
if (item.loading !== true && item.key !== loadingKey) {
|
|
24
|
+
return item;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
...item,
|
|
28
|
+
icon: /*#__PURE__*/ React.createElement(Spin, {
|
|
29
|
+
size: "small"
|
|
30
|
+
}),
|
|
31
|
+
'aria-busy': true,
|
|
32
|
+
'data-loading': true
|
|
33
|
+
};
|
|
34
|
+
});
|
|
20
35
|
function handleNew() {
|
|
21
36
|
methods.triggerEvent({
|
|
22
37
|
name: 'onNew',
|
|
@@ -64,7 +79,7 @@ function AgentConversations({ blockId, methods, properties }) {
|
|
|
64
79
|
block: true,
|
|
65
80
|
onClick: handleNew
|
|
66
81
|
}, "+ New Chat")), /*#__PURE__*/ React.createElement(Conversations, {
|
|
67
|
-
items:
|
|
82
|
+
items: conversationItems,
|
|
68
83
|
activeKey: activeKey,
|
|
69
84
|
onActiveChange: (key)=>{
|
|
70
85
|
methods.triggerEvent({
|
|
@@ -26,11 +26,15 @@
|
|
|
26
26
|
properties: {
|
|
27
27
|
items: {
|
|
28
28
|
type: 'array',
|
|
29
|
-
description: 'Conversation list. Each item: { key, label, icon?, disabled?, timestamp?, group? }.'
|
|
29
|
+
description: 'Conversation list. Each item: { key, label, icon?, disabled?, loading?, timestamp?, group? }.'
|
|
30
30
|
},
|
|
31
31
|
activeKey: {
|
|
32
32
|
type: 'string',
|
|
33
33
|
description: 'Currently selected conversation key.'
|
|
34
|
+
},
|
|
35
|
+
loadingKey: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'Key of a conversation that is still loading. That row shows a spinner in place of its icon.'
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
}
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"label": { "type": "string" },
|
|
11
11
|
"icon": {},
|
|
12
12
|
"disabled": { "type": "boolean" },
|
|
13
|
+
"loading": {
|
|
14
|
+
"type": "boolean",
|
|
15
|
+
"description": "Show a spinner in place of this row's icon."
|
|
16
|
+
},
|
|
13
17
|
"timestamp": { "type": "number" },
|
|
14
18
|
"group": { "type": "string" }
|
|
15
19
|
}
|
|
@@ -17,6 +21,10 @@
|
|
|
17
21
|
"default": []
|
|
18
22
|
},
|
|
19
23
|
"activeKey": { "type": "string" },
|
|
24
|
+
"loadingKey": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Key of a conversation that is still loading, for example while its transcript is read after a select. That row shows a spinner in place of its icon."
|
|
27
|
+
},
|
|
20
28
|
"menu": {
|
|
21
29
|
"type": "array",
|
|
22
30
|
"items": {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Copyright 2020-2026 Lowdefy, Inc
|
|
2
|
+
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
id: agent-conversations
|
|
16
|
+
type: PageHeaderMenu
|
|
17
|
+
properties:
|
|
18
|
+
title: AgentConversations
|
|
19
|
+
events:
|
|
20
|
+
onInit:
|
|
21
|
+
- id: init
|
|
22
|
+
type: SetState
|
|
23
|
+
params:
|
|
24
|
+
active: conv-1
|
|
25
|
+
blocks:
|
|
26
|
+
# Selecting a row marks it as loading until finish_loading clears loadingKey, standing in
|
|
27
|
+
# for an app that reads the transcript on select.
|
|
28
|
+
- id: conversations
|
|
29
|
+
type: AgentConversations
|
|
30
|
+
properties:
|
|
31
|
+
activeKey:
|
|
32
|
+
_state: active
|
|
33
|
+
loadingKey:
|
|
34
|
+
_state: loading
|
|
35
|
+
items:
|
|
36
|
+
- key: conv-1
|
|
37
|
+
label: First conversation
|
|
38
|
+
- key: conv-2
|
|
39
|
+
label: Second conversation
|
|
40
|
+
- key: conv-3
|
|
41
|
+
label: Generating a title
|
|
42
|
+
loading: true
|
|
43
|
+
events:
|
|
44
|
+
onSelect:
|
|
45
|
+
- id: select
|
|
46
|
+
type: SetState
|
|
47
|
+
params:
|
|
48
|
+
active:
|
|
49
|
+
_event: key
|
|
50
|
+
loading:
|
|
51
|
+
_event: key
|
|
52
|
+
- id: finish_loading
|
|
53
|
+
type: Button
|
|
54
|
+
properties:
|
|
55
|
+
title: Finish loading
|
|
56
|
+
events:
|
|
57
|
+
onClick:
|
|
58
|
+
- id: clear_loading
|
|
59
|
+
type: SetState
|
|
60
|
+
params:
|
|
61
|
+
loading: null
|
|
62
|
+
- id: readout_active
|
|
63
|
+
type: Html
|
|
64
|
+
properties:
|
|
65
|
+
html:
|
|
66
|
+
_string.concat:
|
|
67
|
+
- _if_none:
|
|
68
|
+
- _state: active
|
|
69
|
+
- ''
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
16
|
+
import { theme } from 'antd';
|
|
17
|
+
import { type } from '@lowdefy/helpers';
|
|
18
|
+
import { withBlockDefaults } from '@lowdefy/block-utils';
|
|
19
|
+
import answerScores from './answerScores.js';
|
|
20
|
+
import nextIntentState from './nextIntentState.js';
|
|
21
|
+
import css from './IntentInput.module.css';
|
|
22
|
+
const IDLE = {
|
|
23
|
+
intent: null,
|
|
24
|
+
challenger: undefined,
|
|
25
|
+
mode: 'idle',
|
|
26
|
+
choices: []
|
|
27
|
+
};
|
|
28
|
+
const CLASSIFY_DELAY = 150;
|
|
29
|
+
const SETTLE_DELAY = 450;
|
|
30
|
+
// A text input that becomes what the user means. The app classifies the text on
|
|
31
|
+
// onClassify (typically a Decide request) and passes the answer back; each intent
|
|
32
|
+
// has a slot of the same name. The block only decides which slot to show and how
|
|
33
|
+
// sure to look, and animates between them.
|
|
34
|
+
function IntentInput({ blockId, classNames = {}, components: { Icon }, content, methods, properties, styles = {}, value }) {
|
|
35
|
+
const { token } = theme.useToken();
|
|
36
|
+
const intents = properties.intents ?? {};
|
|
37
|
+
const confidence = {
|
|
38
|
+
preview: properties.confidence?.preview ?? 0.3,
|
|
39
|
+
show: properties.confidence?.show ?? 0.6,
|
|
40
|
+
switch: properties.confidence?.switch ?? 0.85
|
|
41
|
+
};
|
|
42
|
+
const text = value?.text ?? '';
|
|
43
|
+
// Keep the last answer while the next one loads, so the slot does not drop out
|
|
44
|
+
// between keystrokes.
|
|
45
|
+
const heldAnswer = useRef(null);
|
|
46
|
+
if (!type.isNone(properties.answer)) heldAnswer.current = properties.answer;
|
|
47
|
+
const scores = answerScores({
|
|
48
|
+
answer: heldAnswer.current,
|
|
49
|
+
intents
|
|
50
|
+
});
|
|
51
|
+
const [state, setState] = useState(IDLE);
|
|
52
|
+
const [pinned, setPinned] = useState(null);
|
|
53
|
+
// A round runs each time the latest classification finishes, not when the scores
|
|
54
|
+
// change: an answer that repeats the last one (the same text typed again after a
|
|
55
|
+
// clear) still counts, and a classification a newer one replaced does not.
|
|
56
|
+
const [round, setRound] = useState(0);
|
|
57
|
+
const latestClassify = useRef(0);
|
|
58
|
+
useEffect(()=>{
|
|
59
|
+
latestClassify.current += 1;
|
|
60
|
+
if (!text) {
|
|
61
|
+
setState(IDLE);
|
|
62
|
+
setPinned(null);
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const classify = latestClassify.current;
|
|
66
|
+
const timer = setTimeout(async ()=>{
|
|
67
|
+
const result = await methods.triggerEvent({
|
|
68
|
+
name: 'onClassify',
|
|
69
|
+
event: {
|
|
70
|
+
text
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
if (classify === latestClassify.current && result.success) setRound((n)=>n + 1);
|
|
74
|
+
}, CLASSIFY_DELAY);
|
|
75
|
+
return ()=>clearTimeout(timer);
|
|
76
|
+
}, [
|
|
77
|
+
text
|
|
78
|
+
]);
|
|
79
|
+
useEffect(()=>{
|
|
80
|
+
if (!text) return;
|
|
81
|
+
setState((current)=>nextIntentState({
|
|
82
|
+
state: current,
|
|
83
|
+
scores,
|
|
84
|
+
confidence
|
|
85
|
+
}));
|
|
86
|
+
}, [
|
|
87
|
+
round
|
|
88
|
+
]);
|
|
89
|
+
// Typing restarts the pause, so a challenger only wins on a pause while the user
|
|
90
|
+
// is not typing.
|
|
91
|
+
useEffect(()=>{
|
|
92
|
+
if (state.challenger === undefined) return undefined;
|
|
93
|
+
const timer = setTimeout(()=>setState((current)=>nextIntentState({
|
|
94
|
+
state: current,
|
|
95
|
+
scores,
|
|
96
|
+
confidence
|
|
97
|
+
})), SETTLE_DELAY);
|
|
98
|
+
return ()=>clearTimeout(timer);
|
|
99
|
+
}, [
|
|
100
|
+
state.challenger,
|
|
101
|
+
round,
|
|
102
|
+
text
|
|
103
|
+
]);
|
|
104
|
+
// A picked choice holds until the scores commit to a different intent on their own.
|
|
105
|
+
useEffect(()=>{
|
|
106
|
+
if (pinned && state.mode === 'committed' && state.intent !== pinned) setPinned(null);
|
|
107
|
+
}, [
|
|
108
|
+
state
|
|
109
|
+
]);
|
|
110
|
+
const intent = pinned ?? state.intent;
|
|
111
|
+
const mode = pinned ? 'committed' : state.mode;
|
|
112
|
+
// onIntentChange fires when the shown intent or mode changes, not on mount.
|
|
113
|
+
const shown = useRef({
|
|
114
|
+
intent: null,
|
|
115
|
+
mode: 'idle'
|
|
116
|
+
});
|
|
117
|
+
useEffect(()=>{
|
|
118
|
+
if (value?.intent !== intent || value?.mode !== mode) methods.setValue({
|
|
119
|
+
text,
|
|
120
|
+
intent,
|
|
121
|
+
mode
|
|
122
|
+
});
|
|
123
|
+
if (shown.current.intent === intent && shown.current.mode === mode) return;
|
|
124
|
+
shown.current = {
|
|
125
|
+
intent,
|
|
126
|
+
mode
|
|
127
|
+
};
|
|
128
|
+
methods.triggerEvent({
|
|
129
|
+
name: 'onIntentChange',
|
|
130
|
+
event: {
|
|
131
|
+
intent,
|
|
132
|
+
mode
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}, [
|
|
136
|
+
intent,
|
|
137
|
+
mode
|
|
138
|
+
]);
|
|
139
|
+
const innerRef = useRef(null);
|
|
140
|
+
const [height, setHeight] = useState(0);
|
|
141
|
+
useLayoutEffect(()=>{
|
|
142
|
+
const observer = new ResizeObserver(([entry])=>setHeight(entry.target.offsetHeight));
|
|
143
|
+
observer.observe(innerRef.current);
|
|
144
|
+
return ()=>observer.disconnect();
|
|
145
|
+
}, []);
|
|
146
|
+
const accentOf = (key)=>intents[key]?.color ?? token.colorPrimary;
|
|
147
|
+
const labelOf = (key)=>intents[key]?.label ?? key;
|
|
148
|
+
return /*#__PURE__*/ React.createElement("div", {
|
|
149
|
+
id: blockId,
|
|
150
|
+
"data-mode": mode,
|
|
151
|
+
className: `${css.root} ${classNames.element ?? ''}`,
|
|
152
|
+
style: {
|
|
153
|
+
'--accent': intent ? accentOf(intent) : token.colorPrimary,
|
|
154
|
+
'--border': token.colorBorderSecondary,
|
|
155
|
+
'--bg': token.colorBgContainer,
|
|
156
|
+
'--text': token.colorText,
|
|
157
|
+
'--muted': token.colorTextTertiary,
|
|
158
|
+
fontFamily: token.fontFamily,
|
|
159
|
+
...styles.element
|
|
160
|
+
}
|
|
161
|
+
}, /*#__PURE__*/ React.createElement("div", {
|
|
162
|
+
className: css.field
|
|
163
|
+
}, /*#__PURE__*/ React.createElement("input", {
|
|
164
|
+
id: `${blockId}_input`,
|
|
165
|
+
className: css.input,
|
|
166
|
+
autoFocus: properties.autoFocus,
|
|
167
|
+
autoComplete: "off",
|
|
168
|
+
placeholder: properties.placeholder,
|
|
169
|
+
value: text,
|
|
170
|
+
onChange: (event)=>{
|
|
171
|
+
methods.setValue({
|
|
172
|
+
text: event.target.value,
|
|
173
|
+
intent,
|
|
174
|
+
mode
|
|
175
|
+
});
|
|
176
|
+
methods.triggerEvent({
|
|
177
|
+
name: 'onChange',
|
|
178
|
+
event: {
|
|
179
|
+
text: event.target.value
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
},
|
|
183
|
+
onKeyDown: (event)=>{
|
|
184
|
+
// Enter while an IME is composing picks a candidate; it must not submit.
|
|
185
|
+
if (event.key !== 'Enter' || event.nativeEvent.isComposing) return;
|
|
186
|
+
methods.triggerEvent({
|
|
187
|
+
name: 'onSubmit',
|
|
188
|
+
event: {
|
|
189
|
+
text,
|
|
190
|
+
intent,
|
|
191
|
+
mode
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}), intent && /*#__PURE__*/ React.createElement("span", {
|
|
196
|
+
key: intent,
|
|
197
|
+
className: css.badge
|
|
198
|
+
}, intents[intent]?.icon && /*#__PURE__*/ React.createElement(Icon, {
|
|
199
|
+
blockId: `${blockId}_badge_icon`,
|
|
200
|
+
properties: intents[intent].icon
|
|
201
|
+
}), labelOf(intent), mode !== 'committed' && '?')), /*#__PURE__*/ React.createElement("div", {
|
|
202
|
+
className: css.morph,
|
|
203
|
+
style: {
|
|
204
|
+
height
|
|
205
|
+
}
|
|
206
|
+
}, /*#__PURE__*/ React.createElement("div", {
|
|
207
|
+
ref: innerRef,
|
|
208
|
+
className: css.inner
|
|
209
|
+
}, mode === 'choose' && /*#__PURE__*/ React.createElement("div", {
|
|
210
|
+
className: css.choices
|
|
211
|
+
}, /*#__PURE__*/ React.createElement("span", {
|
|
212
|
+
className: css.choicesLabel
|
|
213
|
+
}, "Did you mean"), state.choices.map((key)=>/*#__PURE__*/ React.createElement("button", {
|
|
214
|
+
key: key,
|
|
215
|
+
type: "button",
|
|
216
|
+
className: css.choice,
|
|
217
|
+
style: {
|
|
218
|
+
'--accent': accentOf(key)
|
|
219
|
+
},
|
|
220
|
+
onClick: ()=>setPinned(key)
|
|
221
|
+
}, intents[key]?.icon && /*#__PURE__*/ React.createElement(Icon, {
|
|
222
|
+
blockId: `${blockId}_choice_${key}_icon`,
|
|
223
|
+
properties: intents[key].icon
|
|
224
|
+
}), labelOf(key)))), intent && content[intent] && /*#__PURE__*/ React.createElement("div", {
|
|
225
|
+
key: intent,
|
|
226
|
+
className: css.slot,
|
|
227
|
+
"data-preview": mode !== 'committed'
|
|
228
|
+
}, content[intent]()))));
|
|
229
|
+
}
|
|
230
|
+
export default withBlockDefaults(IntentInput);
|