@lowdefy/blocks-antd-x 6.1.0 → 7.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/dist/blocks/AgentChat/AgentChat.js +38 -889
- 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 +16 -4
- package/dist/blocks/AgentChat/formatToolResult.js +2 -1
- package/dist/blocks/AgentChat/meta.js +17 -1
- package/dist/blocks/AgentChat/schema.json +7 -2
- package/dist/blocks/AgentChat/tests/AgentChat.e2e.yaml +19 -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 +14 -12
|
@@ -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);
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
border: 1px solid var(--border);
|
|
3
|
+
border-radius: 20px;
|
|
4
|
+
background: var(--bg);
|
|
5
|
+
box-shadow:
|
|
6
|
+
0 1px 2px rgb(0 0 0 / 4%),
|
|
7
|
+
0 10px 30px -14px rgb(0 0 0 / 18%);
|
|
8
|
+
transition:
|
|
9
|
+
border-color 320ms ease,
|
|
10
|
+
box-shadow 320ms ease;
|
|
11
|
+
overflow: hidden;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.root[data-mode='committed'] {
|
|
15
|
+
border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
|
|
16
|
+
box-shadow:
|
|
17
|
+
0 0 0 4px color-mix(in srgb, var(--accent) 12%, transparent),
|
|
18
|
+
0 14px 36px -18px color-mix(in srgb, var(--accent) 55%, transparent);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.field {
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
gap: 10px;
|
|
25
|
+
padding: 16px 20px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.input {
|
|
29
|
+
flex: 1;
|
|
30
|
+
min-width: 0;
|
|
31
|
+
border: 0;
|
|
32
|
+
outline: 0;
|
|
33
|
+
background: transparent;
|
|
34
|
+
color: var(--text);
|
|
35
|
+
font: inherit;
|
|
36
|
+
font-size: 18px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.input::placeholder {
|
|
40
|
+
color: var(--muted);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.badge {
|
|
44
|
+
display: inline-flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: 6px;
|
|
47
|
+
padding: 3px 10px;
|
|
48
|
+
border: 1px dashed var(--border);
|
|
49
|
+
border-radius: 999px;
|
|
50
|
+
color: var(--muted);
|
|
51
|
+
font-size: 12px;
|
|
52
|
+
white-space: nowrap;
|
|
53
|
+
animation: pop 220ms cubic-bezier(0.2, 0.9, 0.3, 1.25);
|
|
54
|
+
transition:
|
|
55
|
+
color 260ms ease,
|
|
56
|
+
background 260ms ease,
|
|
57
|
+
border-color 260ms ease;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.root[data-mode='committed'] .badge {
|
|
61
|
+
border-color: transparent;
|
|
62
|
+
background: color-mix(in srgb, var(--accent) 16%, transparent);
|
|
63
|
+
color: color-mix(in srgb, var(--accent) 60%, var(--text));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.morph {
|
|
67
|
+
overflow: hidden;
|
|
68
|
+
transition: height 360ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.inner:not(:empty) {
|
|
72
|
+
border-top: 1px solid var(--border);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.choices {
|
|
76
|
+
display: flex;
|
|
77
|
+
flex-wrap: wrap;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: 8px;
|
|
80
|
+
padding: 12px 20px 0;
|
|
81
|
+
animation: rise 220ms ease;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.choicesLabel {
|
|
85
|
+
color: var(--muted);
|
|
86
|
+
font-size: 13px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.choice {
|
|
90
|
+
display: inline-flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
gap: 6px;
|
|
93
|
+
padding: 4px 12px;
|
|
94
|
+
border: 1px solid var(--border);
|
|
95
|
+
border-radius: 999px;
|
|
96
|
+
background: transparent;
|
|
97
|
+
color: var(--text);
|
|
98
|
+
font: inherit;
|
|
99
|
+
font-size: 13px;
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
transition:
|
|
102
|
+
border-color 160ms ease,
|
|
103
|
+
color 160ms ease;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.choice:hover {
|
|
107
|
+
border-color: var(--accent);
|
|
108
|
+
color: color-mix(in srgb, var(--accent) 60%, var(--text));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.slot {
|
|
112
|
+
padding: 16px 20px 20px;
|
|
113
|
+
animation: rise 340ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
114
|
+
transform-origin: top center;
|
|
115
|
+
transition:
|
|
116
|
+
opacity 280ms ease,
|
|
117
|
+
filter 280ms ease,
|
|
118
|
+
transform 280ms ease;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.slot[data-preview='true'] {
|
|
122
|
+
opacity: 0.42;
|
|
123
|
+
filter: saturate(0.2) blur(0.6px);
|
|
124
|
+
transform: scale(0.985);
|
|
125
|
+
pointer-events: none;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@keyframes rise {
|
|
129
|
+
from {
|
|
130
|
+
opacity: 0;
|
|
131
|
+
transform: translateY(8px) scale(0.985);
|
|
132
|
+
}
|
|
133
|
+
to {
|
|
134
|
+
opacity: 1;
|
|
135
|
+
transform: none;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@keyframes pop {
|
|
140
|
+
from {
|
|
141
|
+
opacity: 0;
|
|
142
|
+
transform: scale(0.85);
|
|
143
|
+
}
|
|
144
|
+
to {
|
|
145
|
+
opacity: 1;
|
|
146
|
+
transform: none;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@media (prefers-reduced-motion: reduce) {
|
|
151
|
+
.root,
|
|
152
|
+
.badge,
|
|
153
|
+
.morph,
|
|
154
|
+
.choices,
|
|
155
|
+
.slot {
|
|
156
|
+
animation: none;
|
|
157
|
+
transition: none;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
*/ // A Decide choice answer ({ choice, confidence, probabilities }) as a score per
|
|
16
|
+
// intent. Options without an intent (such as a `none` escape option) drop out.
|
|
17
|
+
// The provider's calibrated confidence, where it reports one, is the chosen
|
|
18
|
+
// intent's score, and the other intents keep their ratio to it: mixing calibrated
|
|
19
|
+
// and raw scores could rank a rival above the intent the model chose.
|
|
20
|
+
function answerScores({ answer, intents }) {
|
|
21
|
+
const probabilities = answer?.probabilities ?? {};
|
|
22
|
+
const chosen = Object.hasOwn(intents, answer?.choice ?? '') ? answer.choice : null;
|
|
23
|
+
let scale = 1;
|
|
24
|
+
if (chosen && Number.isFinite(answer.confidence) && probabilities[chosen] > 0) {
|
|
25
|
+
scale = answer.confidence / probabilities[chosen];
|
|
26
|
+
}
|
|
27
|
+
const scores = {};
|
|
28
|
+
Object.keys(intents).forEach((key)=>{
|
|
29
|
+
if (Number.isFinite(probabilities[key])) scores[key] = probabilities[key] * scale;
|
|
30
|
+
});
|
|
31
|
+
if (chosen && Number.isFinite(answer.confidence)) scores[chosen] = answer.confidence;
|
|
32
|
+
return scores;
|
|
33
|
+
}
|
|
34
|
+
export default answerScores;
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
# A keyword classifier stands in for a Decide request so the example runs
|
|
16
|
+
# without a model. Try "fix the login bug", "sync with Sam friday 2pm" or
|
|
17
|
+
# "standup".
|
|
18
|
+
- title: Capture box
|
|
19
|
+
blocks:
|
|
20
|
+
- id: capture_demo
|
|
21
|
+
type: IntentInput
|
|
22
|
+
properties:
|
|
23
|
+
placeholder: Try "fix the login bug" or "sync with Sam friday 2pm"
|
|
24
|
+
intents:
|
|
25
|
+
task:
|
|
26
|
+
label: Task
|
|
27
|
+
icon: CircleCheck
|
|
28
|
+
color: '#6366f1'
|
|
29
|
+
meeting:
|
|
30
|
+
label: Meeting
|
|
31
|
+
icon: Calendar
|
|
32
|
+
color: '#0ea5e9'
|
|
33
|
+
answer:
|
|
34
|
+
_state: capture_demo_answer
|
|
35
|
+
events:
|
|
36
|
+
onClassify:
|
|
37
|
+
- id: classify
|
|
38
|
+
type: SetState
|
|
39
|
+
params:
|
|
40
|
+
capture_demo_answer:
|
|
41
|
+
_js:
|
|
42
|
+
fn: |
|
|
43
|
+
const text = args.text.toLowerCase();
|
|
44
|
+
const probabilities = { task: 0.05, meeting: 0.05 };
|
|
45
|
+
if (/^(fix|add|build|update|remove)\b/.test(text)) probabilities.task = 0.9;
|
|
46
|
+
if (/\b(sync|meeting|call|standup)\b/.test(text)) {
|
|
47
|
+
probabilities.meeting = /\d|today|tomorrow|day\b/.test(text) ? 0.9 : 0.4;
|
|
48
|
+
}
|
|
49
|
+
return { probabilities };
|
|
50
|
+
args:
|
|
51
|
+
text:
|
|
52
|
+
_event: text
|
|
53
|
+
slots:
|
|
54
|
+
task:
|
|
55
|
+
blocks:
|
|
56
|
+
- id: capture_demo_task
|
|
57
|
+
type: Paragraph
|
|
58
|
+
properties:
|
|
59
|
+
content:
|
|
60
|
+
_string.concat:
|
|
61
|
+
- 'New task: '
|
|
62
|
+
- _state: capture_demo.text
|
|
63
|
+
- id: capture_demo_create
|
|
64
|
+
type: Button
|
|
65
|
+
properties:
|
|
66
|
+
title: Create task
|
|
67
|
+
type: primary
|
|
68
|
+
meeting:
|
|
69
|
+
blocks:
|
|
70
|
+
- id: capture_demo_meeting
|
|
71
|
+
type: Paragraph
|
|
72
|
+
properties:
|
|
73
|
+
content:
|
|
74
|
+
_string.concat:
|
|
75
|
+
- 'New meeting: '
|
|
76
|
+
- _state: capture_demo.text
|
|
77
|
+
- id: capture_demo_invite
|
|
78
|
+
type: Button
|
|
79
|
+
properties:
|
|
80
|
+
title: Send invite
|
|
81
|
+
type: primary
|
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
*/ const level = {
|
|
16
|
+
type: 'number',
|
|
17
|
+
minimum: 0,
|
|
18
|
+
maximum: 1
|
|
19
|
+
};
|
|
20
|
+
const mode = '`idle` (nothing recognised), `preview`, `choose` or `committed`.';
|
|
21
|
+
export default {
|
|
22
|
+
// An input with slots: its value is { text, intent, mode }, and each intent shows the
|
|
23
|
+
// slot named after it, so slot names are authored and cannot be enumerated.
|
|
24
|
+
category: 'input-container',
|
|
25
|
+
icons: [],
|
|
26
|
+
valueType: 'object',
|
|
27
|
+
slots: false,
|
|
28
|
+
cssKeys: {
|
|
29
|
+
element: 'The IntentInput element.'
|
|
30
|
+
},
|
|
31
|
+
events: {
|
|
32
|
+
onChange: {
|
|
33
|
+
description: 'Trigger actions when the user types.',
|
|
34
|
+
event: {
|
|
35
|
+
text: 'The current text.'
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
onClassify: {
|
|
39
|
+
description: 'Trigger actions shortly after the text changes, to classify it. Typically runs a Decide request whose answer is passed back as `answer`.',
|
|
40
|
+
event: {
|
|
41
|
+
text: 'The text to classify.'
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
onIntentChange: {
|
|
45
|
+
description: 'Trigger actions when the shown intent or its mode changes.',
|
|
46
|
+
event: {
|
|
47
|
+
intent: 'The shown intent, or null.',
|
|
48
|
+
mode
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
onSubmit: {
|
|
52
|
+
description: 'Trigger actions when Enter is pressed.',
|
|
53
|
+
event: {
|
|
54
|
+
text: 'The current text.',
|
|
55
|
+
intent: 'The shown intent, or null.',
|
|
56
|
+
mode
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
properties: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
additionalProperties: false,
|
|
63
|
+
properties: {
|
|
64
|
+
autoFocus: {
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
description: 'Focus the input on mount.'
|
|
67
|
+
},
|
|
68
|
+
placeholder: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: 'Placeholder text.'
|
|
71
|
+
},
|
|
72
|
+
intents: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
description: 'The intents the text can become, by name. Each name is also the slot shown for it. The same object can be the options of the Decide question that classifies the text.',
|
|
75
|
+
docs: {
|
|
76
|
+
displayType: 'yaml'
|
|
77
|
+
},
|
|
78
|
+
additionalProperties: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
additionalProperties: false,
|
|
81
|
+
properties: {
|
|
82
|
+
label: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description: 'Name shown on the badge and choices.'
|
|
85
|
+
},
|
|
86
|
+
icon: {
|
|
87
|
+
type: [
|
|
88
|
+
'string',
|
|
89
|
+
'object'
|
|
90
|
+
],
|
|
91
|
+
description: 'Icon name or Icon block properties for the badge and choices.'
|
|
92
|
+
},
|
|
93
|
+
color: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: 'Accent color of the edge and badge.'
|
|
96
|
+
},
|
|
97
|
+
description: {
|
|
98
|
+
type: 'string',
|
|
99
|
+
description: 'What the intent means. Read by the classifier, not shown.'
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
answer: {
|
|
105
|
+
type: [
|
|
106
|
+
'object',
|
|
107
|
+
'null'
|
|
108
|
+
],
|
|
109
|
+
description: 'The classification of the current text, in the shape of a Decide choice answer: `_request: classify.intent`. Options that are not intents, such as a `none` option, are ignored. While a new answer loads, the last one is kept.',
|
|
110
|
+
docs: {
|
|
111
|
+
displayType: 'yaml'
|
|
112
|
+
},
|
|
113
|
+
properties: {
|
|
114
|
+
choice: {
|
|
115
|
+
type: [
|
|
116
|
+
'string',
|
|
117
|
+
'null'
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
confidence: {
|
|
121
|
+
type: [
|
|
122
|
+
'number',
|
|
123
|
+
'null'
|
|
124
|
+
]
|
|
125
|
+
},
|
|
126
|
+
probabilities: {
|
|
127
|
+
type: [
|
|
128
|
+
'object',
|
|
129
|
+
'null'
|
|
130
|
+
],
|
|
131
|
+
additionalProperties: {
|
|
132
|
+
type: 'number'
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
confidence: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
additionalProperties: false,
|
|
140
|
+
description: 'How sure the answer must be before the slot changes how it looks.',
|
|
141
|
+
docs: {
|
|
142
|
+
displayType: 'yaml'
|
|
143
|
+
},
|
|
144
|
+
properties: {
|
|
145
|
+
preview: {
|
|
146
|
+
...level,
|
|
147
|
+
default: 0.3,
|
|
148
|
+
description: 'Show a faded preview of the slot from this confidence.'
|
|
149
|
+
},
|
|
150
|
+
show: {
|
|
151
|
+
...level,
|
|
152
|
+
default: 0.6,
|
|
153
|
+
description: 'Commit to the intent and show its slot solid from this confidence.'
|
|
154
|
+
},
|
|
155
|
+
switch: {
|
|
156
|
+
...level,
|
|
157
|
+
default: 0.85,
|
|
158
|
+
description: 'Replace the shown intent at once from this confidence. Below it, another intent must lead twice in a row, or hold the lead while the user pauses.'
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|