@budibase/frontend-core 3.46.0 → 3.48.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/package.json +2 -2
- package/src/components/Chatbox/EscalationCard.svelte +171 -25
- package/src/components/Chatbox/index.svelte +102 -46
- package/src/components/FilterUsers.svelte +62 -3
- package/src/fetch/DataFetch.ts +17 -0
- package/src/fetch/UserFetch.ts +10 -3
- package/src/utils/index.ts +1 -0
- package/src/utils/users.ts +37 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/frontend-core",
|
|
3
|
-
"version": "v3.
|
|
3
|
+
"version": "v3.48.0",
|
|
4
4
|
"description": "Budibase frontend core libraries used in builder and client",
|
|
5
5
|
"author": "Budibase",
|
|
6
6
|
"license": "MPL-2.0",
|
|
@@ -24,5 +24,5 @@
|
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"vitest": "^5.0.0"
|
|
26
26
|
},
|
|
27
|
-
"gitHead": "
|
|
27
|
+
"gitHead": "0d896a86074936025b8ef6e3400ab650faeb8d06"
|
|
28
28
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Body, Button, Icon } from "@budibase/bbui"
|
|
3
|
-
import
|
|
3
|
+
import { APPROVAL_REQUIRED_TITLE_PREFIX } from "@budibase/shared-core"
|
|
4
|
+
import type {
|
|
5
|
+
EscalationContextDoc,
|
|
6
|
+
EscalationReviewContext,
|
|
7
|
+
} from "@budibase/types"
|
|
4
8
|
|
|
5
9
|
interface Props {
|
|
6
10
|
title?: string
|
|
7
11
|
summary?: string
|
|
12
|
+
reviewContext?: EscalationReviewContext
|
|
8
13
|
// Live resolution from the poll (not the frozen tool output).
|
|
9
14
|
resolution: EscalationContextDoc["resolution"]
|
|
10
15
|
// Message relayed from resolve ("Response recorded." etc.) - set as soon as
|
|
@@ -20,6 +25,7 @@
|
|
|
20
25
|
let {
|
|
21
26
|
title,
|
|
22
27
|
summary,
|
|
28
|
+
reviewContext,
|
|
23
29
|
resolution,
|
|
24
30
|
statusMessage,
|
|
25
31
|
showApproval = false,
|
|
@@ -30,31 +36,102 @@
|
|
|
30
36
|
|
|
31
37
|
// Resolved either by this card's action (statusMessage) or elsewhere (poll).
|
|
32
38
|
let isResolved = $derived(!!statusMessage || resolution !== "pending")
|
|
39
|
+
let expanded = $state(true)
|
|
40
|
+
let showDetails = $derived(!showApproval || expanded)
|
|
41
|
+
// The card heading already reads "Approval required", so drop the matching
|
|
42
|
+
// prefix that operation escalations put on the title.
|
|
43
|
+
let displayTitle = $derived(
|
|
44
|
+
title?.startsWith(APPROVAL_REQUIRED_TITLE_PREFIX)
|
|
45
|
+
? title.slice(APPROVAL_REQUIRED_TITLE_PREFIX.length).trim()
|
|
46
|
+
: title
|
|
47
|
+
)
|
|
48
|
+
let hasSharedParameters = $derived(Boolean(reviewContext?.parameters?.length))
|
|
49
|
+
|
|
50
|
+
const toggleDetails = () => {
|
|
51
|
+
expanded = !expanded
|
|
52
|
+
}
|
|
33
53
|
</script>
|
|
34
54
|
|
|
35
55
|
<div class="escalation-card" aria-live="polite">
|
|
36
|
-
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
56
|
+
{#if showApproval}
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
class="escalation-card-header escalation-card-toggle"
|
|
60
|
+
aria-expanded={expanded}
|
|
61
|
+
aria-label={`${expanded ? "Collapse" : "Expand"} approval details${displayTitle ? ` for ${displayTitle}` : ""}`}
|
|
62
|
+
onclick={toggleDetails}
|
|
63
|
+
>
|
|
64
|
+
<Icon name={isResolved ? "check-circle" : "clock"} size="M" />
|
|
65
|
+
<span class="escalation-card-heading">Approval required</span>
|
|
66
|
+
<span class="escalation-card-header-end">
|
|
67
|
+
{#if !isResolved}
|
|
68
|
+
<span class="escalation-card-badge">Test mode</span>
|
|
69
|
+
{/if}
|
|
70
|
+
<span class="escalation-card-chevron" class:expanded>
|
|
71
|
+
<Icon name="caret-down" size="S" />
|
|
72
|
+
</span>
|
|
73
|
+
</span>
|
|
74
|
+
</button>
|
|
75
|
+
{:else}
|
|
76
|
+
<div class="escalation-card-header">
|
|
77
|
+
<Icon name={isResolved ? "check-circle" : "clock"} size="M" />
|
|
78
|
+
<span class="escalation-card-heading">Approval required</span>
|
|
79
|
+
</div>
|
|
80
|
+
{/if}
|
|
81
|
+
|
|
82
|
+
{#if showDetails}
|
|
83
|
+
{#if displayTitle}
|
|
84
|
+
<div class="escalation-card-title">{displayTitle}</div>
|
|
85
|
+
{/if}
|
|
86
|
+
|
|
87
|
+
{#if reviewContext}
|
|
88
|
+
<Body size="S" color="var(--spectrum-global-color-gray-700)">
|
|
89
|
+
{reviewContext.requestedBy} is requesting approval for
|
|
90
|
+
<strong>{reviewContext.action}</strong> as part of
|
|
91
|
+
<strong>{reviewContext.operation}</strong>.
|
|
92
|
+
</Body>
|
|
93
|
+
{/if}
|
|
94
|
+
|
|
95
|
+
{#if summary}
|
|
96
|
+
<Body size="S" color="var(--spectrum-global-color-gray-700)">
|
|
97
|
+
{summary}
|
|
98
|
+
</Body>
|
|
41
99
|
{/if}
|
|
42
|
-
</div>
|
|
43
100
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
101
|
+
{#if isResolved}
|
|
102
|
+
<Body size="XS" color="var(--spectrum-global-color-gray-600)">
|
|
103
|
+
{statusMessage || "Response recorded."}
|
|
104
|
+
</Body>
|
|
105
|
+
{:else if showApproval}
|
|
106
|
+
<Body size="XS" color="var(--spectrum-global-color-gray-600)">
|
|
107
|
+
Approve or reject here to simulate a reviewer response.
|
|
108
|
+
</Body>
|
|
109
|
+
{:else}
|
|
110
|
+
<Body size="XS" color="var(--spectrum-global-color-gray-600)">
|
|
111
|
+
Awaiting a human response.
|
|
112
|
+
</Body>
|
|
113
|
+
{/if}
|
|
114
|
+
|
|
115
|
+
{#if hasSharedParameters && reviewContext?.parameters}
|
|
116
|
+
<div class="escalation-card-divider"></div>
|
|
117
|
+
<div class="escalation-card-parameters-heading">
|
|
118
|
+
<span>Tool parameters</span>
|
|
119
|
+
{#if reviewContext.toolName}
|
|
120
|
+
<code>{reviewContext.toolName}</code>
|
|
121
|
+
{/if}
|
|
122
|
+
</div>
|
|
123
|
+
<div class="escalation-card-parameters-list">
|
|
124
|
+
{#each reviewContext.parameters as parameter (parameter.name)}
|
|
125
|
+
<div class="escalation-card-parameter">
|
|
126
|
+
<code>{parameter.name}</code>
|
|
127
|
+
<pre class="escalation-card-parameters">{parameter.value}</pre>
|
|
128
|
+
</div>
|
|
129
|
+
{/each}
|
|
130
|
+
</div>
|
|
131
|
+
{/if}
|
|
48
132
|
{/if}
|
|
49
133
|
|
|
50
|
-
{#if isResolved}
|
|
51
|
-
<Body size="XS" color="var(--spectrum-global-color-gray-600)">
|
|
52
|
-
{statusMessage || "Response recorded."}
|
|
53
|
-
</Body>
|
|
54
|
-
{:else if showApproval}
|
|
55
|
-
<Body size="XS" color="var(--spectrum-global-color-gray-600)">
|
|
56
|
-
Approve or reject here to simulate a reviewer response.
|
|
57
|
-
</Body>
|
|
134
|
+
{#if showApproval && !isResolved}
|
|
58
135
|
<div class="escalation-card-actions">
|
|
59
136
|
<Button cta disabled={resolving} on:click={() => onApprove?.()}>
|
|
60
137
|
Approve
|
|
@@ -63,10 +140,6 @@
|
|
|
63
140
|
Reject
|
|
64
141
|
</Button>
|
|
65
142
|
</div>
|
|
66
|
-
{:else}
|
|
67
|
-
<Body size="XS" color="var(--spectrum-global-color-gray-600)">
|
|
68
|
-
Awaiting a human response.
|
|
69
|
-
</Body>
|
|
70
143
|
{/if}
|
|
71
144
|
</div>
|
|
72
145
|
|
|
@@ -89,13 +162,34 @@
|
|
|
89
162
|
align-items: center;
|
|
90
163
|
gap: var(--spacing-s);
|
|
91
164
|
}
|
|
92
|
-
.escalation-card-
|
|
165
|
+
.escalation-card-toggle {
|
|
166
|
+
width: 100%;
|
|
167
|
+
margin: 0;
|
|
168
|
+
padding: 0;
|
|
169
|
+
border: none;
|
|
170
|
+
background: none;
|
|
171
|
+
cursor: pointer;
|
|
172
|
+
text-align: left;
|
|
173
|
+
color: inherit;
|
|
174
|
+
font: inherit;
|
|
175
|
+
}
|
|
176
|
+
.escalation-card-heading,
|
|
177
|
+
.escalation-card-title,
|
|
178
|
+
.escalation-card-parameters-heading {
|
|
93
179
|
font-weight: 600;
|
|
94
180
|
font-size: 14px;
|
|
95
181
|
color: var(--spectrum-global-color-gray-900);
|
|
96
182
|
}
|
|
97
|
-
.escalation-card-
|
|
183
|
+
.escalation-card-title {
|
|
184
|
+
margin-top: var(--spacing-xs);
|
|
185
|
+
}
|
|
186
|
+
.escalation-card-header-end {
|
|
187
|
+
display: flex;
|
|
188
|
+
align-items: center;
|
|
189
|
+
gap: var(--spacing-xs);
|
|
98
190
|
margin-left: auto;
|
|
191
|
+
}
|
|
192
|
+
.escalation-card-badge {
|
|
99
193
|
border-radius: 999px;
|
|
100
194
|
border: 1px solid var(--spectrum-global-color-blue-400);
|
|
101
195
|
color: var(--spectrum-global-color-blue-700);
|
|
@@ -105,9 +199,61 @@
|
|
|
105
199
|
line-height: 1;
|
|
106
200
|
padding: 4px 8px;
|
|
107
201
|
}
|
|
202
|
+
.escalation-card-chevron {
|
|
203
|
+
display: flex;
|
|
204
|
+
color: var(--spectrum-global-color-gray-700);
|
|
205
|
+
transition: transform 130ms ease-in-out;
|
|
206
|
+
}
|
|
207
|
+
.escalation-card-chevron.expanded {
|
|
208
|
+
transform: rotate(180deg);
|
|
209
|
+
}
|
|
108
210
|
.escalation-card-actions {
|
|
109
211
|
display: flex;
|
|
110
212
|
gap: var(--spacing-s);
|
|
111
213
|
margin-top: var(--spacing-xs);
|
|
112
214
|
}
|
|
215
|
+
.escalation-card-divider {
|
|
216
|
+
height: 1px;
|
|
217
|
+
margin: var(--spacing-xs) calc(-1 * var(--spacing-m));
|
|
218
|
+
background: var(--spectrum-global-color-gray-300);
|
|
219
|
+
}
|
|
220
|
+
.escalation-card-parameters-heading {
|
|
221
|
+
display: flex;
|
|
222
|
+
align-items: center;
|
|
223
|
+
gap: var(--spacing-xs);
|
|
224
|
+
}
|
|
225
|
+
.escalation-card-parameters-heading code {
|
|
226
|
+
min-width: 0;
|
|
227
|
+
padding: 2px 5px;
|
|
228
|
+
border-radius: 4px;
|
|
229
|
+
background: var(--spectrum-global-color-gray-200);
|
|
230
|
+
font-size: 12px;
|
|
231
|
+
font-weight: 400;
|
|
232
|
+
overflow-wrap: anywhere;
|
|
233
|
+
white-space: normal;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.escalation-card-parameters-list,
|
|
237
|
+
.escalation-card-parameter {
|
|
238
|
+
display: flex;
|
|
239
|
+
flex-direction: column;
|
|
240
|
+
gap: var(--spacing-xs);
|
|
241
|
+
}
|
|
242
|
+
.escalation-card-parameters {
|
|
243
|
+
box-sizing: content-box;
|
|
244
|
+
min-height: 12px;
|
|
245
|
+
max-height: 360px;
|
|
246
|
+
margin: 0;
|
|
247
|
+
padding: var(--spacing-s);
|
|
248
|
+
overflow: auto;
|
|
249
|
+
border: 1px solid var(--spectrum-global-color-gray-300);
|
|
250
|
+
border-radius: 4px;
|
|
251
|
+
background: var(--spectrum-global-color-gray-100);
|
|
252
|
+
color: var(--spectrum-global-color-gray-800);
|
|
253
|
+
font-family: monospace;
|
|
254
|
+
font-size: 12px;
|
|
255
|
+
line-height: 16px;
|
|
256
|
+
white-space: pre-wrap;
|
|
257
|
+
overflow-wrap: anywhere;
|
|
258
|
+
}
|
|
113
259
|
</style>
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
DraftChatConversation,
|
|
13
13
|
AgentMessageMetadata,
|
|
14
14
|
EscalationContextDoc,
|
|
15
|
+
EscalationReviewContext,
|
|
15
16
|
EscalationRespondResult,
|
|
16
17
|
} from "@budibase/types"
|
|
17
18
|
import { ApprovalToolResultStatus } from "@budibase/types"
|
|
@@ -93,7 +94,12 @@
|
|
|
93
94
|
// casts live here rather than cluttering the template.
|
|
94
95
|
const escalationCardProps = (part: { input?: unknown; output?: unknown }) => {
|
|
95
96
|
const output = part.output as
|
|
96
|
-
| {
|
|
97
|
+
| {
|
|
98
|
+
escalationId?: string
|
|
99
|
+
title?: string
|
|
100
|
+
summary?: string
|
|
101
|
+
reviewContext?: EscalationReviewContext
|
|
102
|
+
}
|
|
97
103
|
| undefined
|
|
98
104
|
const input = part.input as { title?: string; summary?: string } | undefined
|
|
99
105
|
const escalationId = output?.escalationId
|
|
@@ -101,6 +107,7 @@
|
|
|
101
107
|
escalationId,
|
|
102
108
|
title: output?.title ?? input?.title,
|
|
103
109
|
summary: output?.summary ?? input?.summary,
|
|
110
|
+
reviewContext: output?.reviewContext,
|
|
104
111
|
resolution:
|
|
105
112
|
(escalationId && escalationState?.[escalationId]?.resolution) ||
|
|
106
113
|
"pending",
|
|
@@ -266,7 +273,25 @@
|
|
|
266
273
|
|
|
267
274
|
let resolvedConversationId = $state<string | undefined>()
|
|
268
275
|
|
|
276
|
+
// The consumer must get plain objects, otherwise reading them keeps
|
|
277
|
+
// subscribing to the live chat state and every streamed token counts as a
|
|
278
|
+
// change. $state.snapshot cannot type UIMessage, hence the manual clone.
|
|
279
|
+
const snapshotMessages = (
|
|
280
|
+
nextMessages: UIMessage<AgentMessageMetadata>[]
|
|
281
|
+
): UIMessage<AgentMessageMetadata>[] =>
|
|
282
|
+
nextMessages.map(message => JSON.parse(JSON.stringify(message)))
|
|
283
|
+
|
|
284
|
+
const publishChatMessages = (
|
|
285
|
+
nextMessages: UIMessage<AgentMessageMetadata>[] = chatInstance.messages
|
|
286
|
+
) => {
|
|
287
|
+
chat = {
|
|
288
|
+
...chat,
|
|
289
|
+
messages: snapshotMessages(nextMessages),
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
269
293
|
const chatInstance = new Chat<UIMessage<AgentMessageMetadata>>({
|
|
294
|
+
messages: chat?.messages || [],
|
|
270
295
|
transport: new DefaultChatTransport({
|
|
271
296
|
headers: () => ({ [Header.WORKSPACE_ID]: workspaceId }),
|
|
272
297
|
prepareSendMessagesRequest: ({ messages }) => {
|
|
@@ -286,13 +311,13 @@
|
|
|
286
311
|
}
|
|
287
312
|
},
|
|
288
313
|
}),
|
|
289
|
-
messages: chat?.messages || [],
|
|
290
314
|
onFinish: async () => {
|
|
291
315
|
isPreparingResponse = false
|
|
292
|
-
|
|
316
|
+
publishChatMessages()
|
|
293
317
|
},
|
|
294
318
|
onError: error => {
|
|
295
319
|
resetPendingResponse()
|
|
320
|
+
publishChatMessages()
|
|
296
321
|
|
|
297
322
|
console.error(error)
|
|
298
323
|
let message = error.message || "Failed to send message"
|
|
@@ -350,6 +375,7 @@
|
|
|
350
375
|
message: UIMessage<AgentMessageMetadata>
|
|
351
376
|
) {
|
|
352
377
|
chatInstance.messages = [...chatInstance.messages, message]
|
|
378
|
+
publishChatMessages()
|
|
353
379
|
}
|
|
354
380
|
|
|
355
381
|
let lastAssistantUsage = $derived(
|
|
@@ -412,6 +438,10 @@
|
|
|
412
438
|
}
|
|
413
439
|
})
|
|
414
440
|
|
|
441
|
+
// Tool/reasoning toggles mutate the chat DOM; the observer below would
|
|
442
|
+
// otherwise pin the viewport to the bottom as height changes.
|
|
443
|
+
let skipAutoScroll = false
|
|
444
|
+
|
|
415
445
|
const scrollToBottom = async () => {
|
|
416
446
|
await tick()
|
|
417
447
|
if (chatAreaElement) {
|
|
@@ -421,6 +451,9 @@
|
|
|
421
451
|
|
|
422
452
|
$effect(() => {
|
|
423
453
|
if (messages?.length) {
|
|
454
|
+
if (skipAutoScroll) {
|
|
455
|
+
return
|
|
456
|
+
}
|
|
424
457
|
scrollToBottom()
|
|
425
458
|
}
|
|
426
459
|
})
|
|
@@ -483,7 +516,18 @@
|
|
|
483
516
|
inputValue = ""
|
|
484
517
|
promptHistoryIndex = undefined
|
|
485
518
|
onpromptsubmitted?.(text)
|
|
486
|
-
|
|
519
|
+
|
|
520
|
+
const userMessage: UIMessage<AgentMessageMetadata> = {
|
|
521
|
+
id: Helpers.uuid(),
|
|
522
|
+
role: "user",
|
|
523
|
+
parts: [{ type: "text", text }],
|
|
524
|
+
}
|
|
525
|
+
publishChatMessages([...(chat.messages ?? []), userMessage])
|
|
526
|
+
chatInstance.sendMessage({
|
|
527
|
+
id: userMessage.id,
|
|
528
|
+
role: "user",
|
|
529
|
+
parts: userMessage.parts,
|
|
530
|
+
})
|
|
487
531
|
isPreparingResponse = false
|
|
488
532
|
}
|
|
489
533
|
|
|
@@ -495,8 +539,17 @@
|
|
|
495
539
|
await sendMessage()
|
|
496
540
|
}
|
|
497
541
|
|
|
498
|
-
const
|
|
499
|
-
|
|
542
|
+
const toggleExpanded = (id: string) => {
|
|
543
|
+
const area = chatAreaElement
|
|
544
|
+
const previousScrollTop = area?.scrollTop ?? 0
|
|
545
|
+
skipAutoScroll = true
|
|
546
|
+
expandedTools = { ...expandedTools, [id]: !expandedTools[id] }
|
|
547
|
+
tick().then(() => {
|
|
548
|
+
if (area) {
|
|
549
|
+
area.scrollTop = previousScrollTop
|
|
550
|
+
}
|
|
551
|
+
skipAutoScroll = false
|
|
552
|
+
})
|
|
500
553
|
}
|
|
501
554
|
|
|
502
555
|
const formatToolOutput = (output: unknown): string =>
|
|
@@ -530,7 +583,12 @@
|
|
|
530
583
|
$effect(() => {
|
|
531
584
|
if (!chatAreaElement) return
|
|
532
585
|
|
|
533
|
-
const obs = new MutationObserver(
|
|
586
|
+
const obs = new MutationObserver(() => {
|
|
587
|
+
if (skipAutoScroll) {
|
|
588
|
+
return
|
|
589
|
+
}
|
|
590
|
+
scrollToBottom()
|
|
591
|
+
})
|
|
534
592
|
obs.observe(chatAreaElement, {
|
|
535
593
|
childList: true,
|
|
536
594
|
subtree: true,
|
|
@@ -543,8 +601,8 @@
|
|
|
543
601
|
})
|
|
544
602
|
</script>
|
|
545
603
|
|
|
546
|
-
<div class="chat-area"
|
|
547
|
-
<div class="chatbox">
|
|
604
|
+
<div class="chat-area">
|
|
605
|
+
<div class="chatbox" bind:this={chatAreaElement}>
|
|
548
606
|
{#if !hasMessages && !isRequestPending}
|
|
549
607
|
<div class="empty-state">
|
|
550
608
|
<div class="empty-state-icon">
|
|
@@ -597,35 +655,13 @@
|
|
|
597
655
|
interactive={!!reasoningText}
|
|
598
656
|
expanded={Boolean(expandedTools[reasoningId])}
|
|
599
657
|
content={reasoningText}
|
|
600
|
-
ontoggle={() =>
|
|
601
|
-
(expandedTools = {
|
|
602
|
-
...expandedTools,
|
|
603
|
-
[reasoningId]: !expandedTools[reasoningId],
|
|
604
|
-
})}
|
|
658
|
+
ontoggle={() => toggleExpanded(reasoningId)}
|
|
605
659
|
/>
|
|
606
660
|
{/if}
|
|
607
661
|
{#each message.parts ?? [] as part, partIndex}
|
|
608
662
|
{#if isTextUIPart(part)}
|
|
609
663
|
<MarkdownViewer value={part.text} />
|
|
610
|
-
{:else if isToolUIPart(part) && isRaisedEscalation(part.output)}
|
|
611
|
-
{@const card = escalationCardProps(part)}
|
|
612
|
-
<EscalationCard
|
|
613
|
-
title={card.title}
|
|
614
|
-
summary={card.summary}
|
|
615
|
-
resolution={card.resolution}
|
|
616
|
-
statusMessage={card.escalationId
|
|
617
|
-
? resolveMessages[card.escalationId]
|
|
618
|
-
: undefined}
|
|
619
|
-
showApproval={showInlineApproval}
|
|
620
|
-
resolving={!!card.escalationId &&
|
|
621
|
-
!!resolvingEscalations[card.escalationId]}
|
|
622
|
-
onApprove={() =>
|
|
623
|
-
card.escalationId && handleResolve(card.escalationId, true)}
|
|
624
|
-
onReject={() =>
|
|
625
|
-
card.escalationId &&
|
|
626
|
-
handleResolve(card.escalationId, false)}
|
|
627
|
-
/>
|
|
628
|
-
{:else if isToolUIPart(part)}
|
|
664
|
+
{:else if isToolUIPart(part) && !isRaisedEscalation(part.output)}
|
|
629
665
|
{@const rawToolName = getToolName(part)}
|
|
630
666
|
{@const displayToolName = formatToolName(
|
|
631
667
|
rawToolName,
|
|
@@ -642,7 +678,7 @@
|
|
|
642
678
|
class="tool-header"
|
|
643
679
|
class:tool-header-expanded={expandedTools[toolId]}
|
|
644
680
|
type="button"
|
|
645
|
-
onclick={() =>
|
|
681
|
+
onclick={() => toggleExpanded(toolId)}
|
|
646
682
|
>
|
|
647
683
|
<span
|
|
648
684
|
class="tool-chevron"
|
|
@@ -753,6 +789,28 @@
|
|
|
753
789
|
</ul>
|
|
754
790
|
</div>
|
|
755
791
|
{/if}
|
|
792
|
+
{#each message.parts ?? [] as part}
|
|
793
|
+
{#if isToolUIPart(part) && isRaisedEscalation(part.output)}
|
|
794
|
+
{@const card = escalationCardProps(part)}
|
|
795
|
+
<EscalationCard
|
|
796
|
+
title={card.title}
|
|
797
|
+
summary={card.summary}
|
|
798
|
+
reviewContext={card.reviewContext}
|
|
799
|
+
resolution={card.resolution}
|
|
800
|
+
statusMessage={card.escalationId
|
|
801
|
+
? resolveMessages[card.escalationId]
|
|
802
|
+
: undefined}
|
|
803
|
+
showApproval={showInlineApproval}
|
|
804
|
+
resolving={!!card.escalationId &&
|
|
805
|
+
!!resolvingEscalations[card.escalationId]}
|
|
806
|
+
onApprove={() =>
|
|
807
|
+
card.escalationId && handleResolve(card.escalationId, true)}
|
|
808
|
+
onReject={() =>
|
|
809
|
+
card.escalationId &&
|
|
810
|
+
handleResolve(card.escalationId, false)}
|
|
811
|
+
/>
|
|
812
|
+
{/if}
|
|
813
|
+
{/each}
|
|
756
814
|
</div>
|
|
757
815
|
{/if}
|
|
758
816
|
{/if}
|
|
@@ -803,7 +861,7 @@
|
|
|
803
861
|
flex: 1 1 0;
|
|
804
862
|
display: flex;
|
|
805
863
|
flex-direction: column;
|
|
806
|
-
overflow
|
|
864
|
+
overflow: hidden;
|
|
807
865
|
min-height: 0;
|
|
808
866
|
font-family: var(--chat-font-sans, var(--font-sans));
|
|
809
867
|
--font-serif: var(--chat-font-sans, var(--font-sans));
|
|
@@ -819,7 +877,9 @@
|
|
|
819
877
|
flex-direction: column;
|
|
820
878
|
gap: 24px;
|
|
821
879
|
width: 100%;
|
|
822
|
-
flex: 1 1
|
|
880
|
+
flex: 1 1 0;
|
|
881
|
+
min-height: 0;
|
|
882
|
+
overflow-y: auto;
|
|
823
883
|
padding: 48px 0 24px 0;
|
|
824
884
|
}
|
|
825
885
|
|
|
@@ -868,14 +928,13 @@
|
|
|
868
928
|
}
|
|
869
929
|
|
|
870
930
|
.input-wrapper {
|
|
871
|
-
position: sticky;
|
|
872
|
-
bottom: 0;
|
|
873
931
|
width: 100%;
|
|
874
932
|
display: flex;
|
|
875
933
|
flex-direction: column;
|
|
876
934
|
flex-shrink: 0;
|
|
877
935
|
line-height: 1.4;
|
|
878
936
|
gap: 6px;
|
|
937
|
+
padding-top: 8px;
|
|
879
938
|
}
|
|
880
939
|
|
|
881
940
|
.input-footer {
|
|
@@ -983,7 +1042,9 @@
|
|
|
983
1042
|
}
|
|
984
1043
|
|
|
985
1044
|
.tool-part {
|
|
986
|
-
|
|
1045
|
+
display: flex;
|
|
1046
|
+
flex-direction: column;
|
|
1047
|
+
align-items: stretch;
|
|
987
1048
|
margin-top: var(--spacing-l);
|
|
988
1049
|
margin-bottom: 0;
|
|
989
1050
|
}
|
|
@@ -1076,22 +1137,17 @@
|
|
|
1076
1137
|
}
|
|
1077
1138
|
|
|
1078
1139
|
.tool-details {
|
|
1079
|
-
|
|
1080
|
-
top: 100%;
|
|
1081
|
-
left: 0;
|
|
1082
|
-
margin-top: var(--spacing-m);
|
|
1140
|
+
margin-top: var(--spacing-s);
|
|
1083
1141
|
width: 100%;
|
|
1084
1142
|
max-width: 100%;
|
|
1085
1143
|
box-sizing: border-box;
|
|
1086
1144
|
display: flex;
|
|
1087
1145
|
flex-direction: column;
|
|
1088
1146
|
gap: var(--spacing-s);
|
|
1089
|
-
background: var(--
|
|
1147
|
+
background: var(--spectrum-global-color-gray-75);
|
|
1090
1148
|
border: 1px solid var(--spectrum-global-color-gray-200);
|
|
1091
1149
|
border-radius: 6px;
|
|
1092
1150
|
padding: var(--spacing-m);
|
|
1093
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
1094
|
-
z-index: 1;
|
|
1095
1151
|
overflow-x: hidden;
|
|
1096
1152
|
min-width: 0;
|
|
1097
1153
|
}
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { Select, Multiselect } from "@budibase/bbui"
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
fetchData,
|
|
5
|
+
fetchUsersById,
|
|
6
|
+
loadTranslationsByGroup,
|
|
7
|
+
Utils,
|
|
8
|
+
} from "@budibase/frontend-core"
|
|
4
9
|
import { createAPIClient } from "../api"
|
|
5
10
|
|
|
6
11
|
export let API = createAPIClient()
|
|
7
12
|
|
|
13
|
+
/** @type {string | string[] | null} */
|
|
8
14
|
export let value = null
|
|
9
|
-
export let disabled
|
|
15
|
+
export let disabled = false
|
|
10
16
|
export let multiselect = false
|
|
11
17
|
|
|
18
|
+
let searchTerm = null
|
|
19
|
+
let appliedTerm = ""
|
|
20
|
+
let selectedCache = {}
|
|
21
|
+
let loadingIds = new Set()
|
|
22
|
+
|
|
12
23
|
$: fetch = fetchData({
|
|
13
24
|
API,
|
|
14
25
|
datasource: {
|
|
@@ -19,10 +30,57 @@
|
|
|
19
30
|
},
|
|
20
31
|
})
|
|
21
32
|
|
|
22
|
-
$:
|
|
33
|
+
$: search(searchTerm || "")
|
|
34
|
+
$: selectedIds = !value ? [] : Array.isArray(value) ? value : [value]
|
|
35
|
+
$: pageIds = new Set(($fetch.rows || []).map(user => user._id))
|
|
36
|
+
$: loadMissing(
|
|
37
|
+
selectedIds.filter(id => !pageIds.has(id) && !selectedCache[id])
|
|
38
|
+
)
|
|
39
|
+
$: options = sortSelectedFirst(
|
|
40
|
+
[
|
|
41
|
+
...($fetch.rows || []),
|
|
42
|
+
...Object.values(selectedCache).filter(
|
|
43
|
+
user => selectedIds.includes(user._id) && !pageIds.has(user._id)
|
|
44
|
+
),
|
|
45
|
+
],
|
|
46
|
+
selectedIds
|
|
47
|
+
)
|
|
23
48
|
|
|
24
49
|
$: component = multiselect ? Multiselect : Select
|
|
25
50
|
const pickerLabels = loadTranslationsByGroup("picker")
|
|
51
|
+
|
|
52
|
+
const search = Utils.debounce(term => {
|
|
53
|
+
if (term === appliedTerm) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
appliedTerm = term
|
|
57
|
+
fetch.update({ query: term ? { string: { email: term } } : {} })
|
|
58
|
+
}, 250)
|
|
59
|
+
|
|
60
|
+
const loadMissing = async ids => {
|
|
61
|
+
const wanted = ids.filter(id => !loadingIds.has(id))
|
|
62
|
+
if (!wanted.length) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
wanted.forEach(id => loadingIds.add(id))
|
|
66
|
+
const users = await fetchUsersById(API, wanted)
|
|
67
|
+
wanted.forEach(id => loadingIds.delete(id))
|
|
68
|
+
if (Object.keys(users).length) {
|
|
69
|
+
selectedCache = { ...selectedCache, ...users }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const sortSelectedFirst = (users, ids) => {
|
|
74
|
+
const selected = new Set(ids)
|
|
75
|
+
return [...users].sort((a, b) => {
|
|
76
|
+
const aSelected = selected.has(a._id)
|
|
77
|
+
const bSelected = selected.has(b._id)
|
|
78
|
+
if (aSelected !== bSelected) {
|
|
79
|
+
return aSelected ? -1 : 1
|
|
80
|
+
}
|
|
81
|
+
return (a.email || "").localeCompare(b.email || "")
|
|
82
|
+
})
|
|
83
|
+
}
|
|
26
84
|
</script>
|
|
27
85
|
|
|
28
86
|
<div class="user-control">
|
|
@@ -37,5 +95,6 @@
|
|
|
37
95
|
{disabled}
|
|
38
96
|
searchPlaceholder={pickerLabels.searchPlaceholder}
|
|
39
97
|
popoverAutoWidth
|
|
98
|
+
bind:searchTerm
|
|
40
99
|
/>
|
|
41
100
|
</div>
|
package/src/fetch/DataFetch.ts
CHANGED
|
@@ -84,6 +84,7 @@ export default abstract class BaseDataFetch<
|
|
|
84
84
|
}
|
|
85
85
|
store: Writable<DataFetchStore<TDefinition, TQuery, TRow>>
|
|
86
86
|
derivedStore: Readable<DataFetchDerivedStore<TDefinition, TQuery, TRow>>
|
|
87
|
+
lastLoadId: number | undefined
|
|
87
88
|
|
|
88
89
|
/**
|
|
89
90
|
* Constructs a new DataFetch instance.
|
|
@@ -203,11 +204,22 @@ export default abstract class BaseDataFetch<
|
|
|
203
204
|
async getInitialData() {
|
|
204
205
|
const { filter, paginate } = this.options
|
|
205
206
|
|
|
207
|
+
// Load using IDs to track invocations and ensure we're handling the
|
|
208
|
+
// latest update
|
|
209
|
+
this.lastLoadId = Math.random()
|
|
210
|
+
const thisLoadId = this.lastLoadId
|
|
211
|
+
|
|
206
212
|
// Fetch datasource definition and extract sort properties if configured
|
|
207
213
|
const definition = await this.getDefinition()
|
|
208
214
|
|
|
209
215
|
// Determine feature flags
|
|
210
216
|
const features = await this.determineFeatureFlags()
|
|
217
|
+
|
|
218
|
+
// In case a newer load started while we waited, abandon this one before
|
|
219
|
+
// it writes its query to the store
|
|
220
|
+
if (thisLoadId !== this.lastLoadId) {
|
|
221
|
+
return
|
|
222
|
+
}
|
|
211
223
|
this.features = {
|
|
212
224
|
supportsSearch: !!features?.supportsSearch,
|
|
213
225
|
supportsSort: !!features?.supportsSort,
|
|
@@ -259,6 +271,11 @@ export default abstract class BaseDataFetch<
|
|
|
259
271
|
|
|
260
272
|
// Actually fetch data
|
|
261
273
|
const page = await this.getPage()
|
|
274
|
+
|
|
275
|
+
// In case fetching takes longer than a newer load, abandon these results
|
|
276
|
+
if (thisLoadId !== this.lastLoadId) {
|
|
277
|
+
return
|
|
278
|
+
}
|
|
262
279
|
this.store.update($store => ({
|
|
263
280
|
...$store,
|
|
264
281
|
loading: false,
|
package/src/fetch/UserFetch.ts
CHANGED
|
@@ -11,7 +11,6 @@ import BaseDataFetch, { DataFetchParams } from "./DataFetch"
|
|
|
11
11
|
|
|
12
12
|
interface UserFetchQuery {
|
|
13
13
|
appId?: string
|
|
14
|
-
workspaceId?: string
|
|
15
14
|
paginated?: boolean
|
|
16
15
|
string?: {
|
|
17
16
|
email: string
|
|
@@ -36,6 +35,7 @@ export default class UserFetch extends BaseDataFetch<
|
|
|
36
35
|
super({
|
|
37
36
|
...opts,
|
|
38
37
|
datasource: {
|
|
38
|
+
...opts.datasource,
|
|
39
39
|
type: "user",
|
|
40
40
|
},
|
|
41
41
|
})
|
|
@@ -56,9 +56,14 @@ export default class UserFetch extends BaseDataFetch<
|
|
|
56
56
|
async getData() {
|
|
57
57
|
const { limit, paginate } = this.options
|
|
58
58
|
const { cursor, query } = get(this.store)
|
|
59
|
+
const {
|
|
60
|
+
groupIds,
|
|
61
|
+
workspaceId: datasourceWorkspaceId,
|
|
62
|
+
workspaceRoleId,
|
|
63
|
+
} = this.options.datasource
|
|
59
64
|
|
|
60
65
|
// Convert old format to new one - we now allow use of the lucene format
|
|
61
|
-
const { appId, paginated,
|
|
66
|
+
const { appId, paginated, ...rest } = query
|
|
62
67
|
|
|
63
68
|
const finalQuery: SearchFilters = utils.isSupportedUserSearch(rest)
|
|
64
69
|
? rest
|
|
@@ -69,7 +74,9 @@ export default class UserFetch extends BaseDataFetch<
|
|
|
69
74
|
bookmark: cursor ?? undefined,
|
|
70
75
|
query: finalQuery ?? undefined,
|
|
71
76
|
appId: appId,
|
|
72
|
-
workspaceId,
|
|
77
|
+
workspaceId: datasourceWorkspaceId,
|
|
78
|
+
groupIds,
|
|
79
|
+
workspaceRoleId,
|
|
73
80
|
paginate: paginated || paginate,
|
|
74
81
|
limit,
|
|
75
82
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { StrippedUser, User } from "@budibase/types"
|
|
2
|
+
import { APIClient } from "../api"
|
|
3
|
+
|
|
4
|
+
export type PickerUser = User | StrippedUser | { _id: string; email: string }
|
|
5
|
+
|
|
6
|
+
// Resolves user ids that fall outside a picker's first page. Ids that no
|
|
7
|
+
// longer match a user get a placeholder so callers stop re-requesting them
|
|
8
|
+
// and still have something to render.
|
|
9
|
+
export const fetchUsersById = async (
|
|
10
|
+
API: APIClient,
|
|
11
|
+
ids: string[]
|
|
12
|
+
): Promise<Record<string, PickerUser>> => {
|
|
13
|
+
const wanted = Array.from(new Set(ids.filter(Boolean)))
|
|
14
|
+
const users: Record<string, PickerUser> = {}
|
|
15
|
+
if (!wanted.length) {
|
|
16
|
+
return users
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const res = await API.searchUsers({
|
|
20
|
+
query: { oneOf: { _id: wanted } },
|
|
21
|
+
limit: wanted.length,
|
|
22
|
+
})
|
|
23
|
+
for (const user of res?.data || []) {
|
|
24
|
+
if (user?._id) {
|
|
25
|
+
users[user._id] = user
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (const id of wanted) {
|
|
29
|
+
if (!users[id]) {
|
|
30
|
+
users[id] = { _id: id, email: id }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error("Failed to load users by id", error)
|
|
35
|
+
}
|
|
36
|
+
return users
|
|
37
|
+
}
|