@budibase/frontend-core 3.39.23 → 3.39.25
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/api/chatLinks.ts +44 -0
- package/src/api/escalations.ts +41 -0
- package/src/api/index.ts +6 -0
- package/src/api/projects.ts +78 -0
- package/src/api/types.ts +6 -0
- package/src/components/Chatbox/EscalationCard.svelte +113 -0
- package/src/components/Chatbox/index.svelte +113 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/frontend-core",
|
|
3
|
-
"version": "3.39.
|
|
3
|
+
"version": "3.39.25",
|
|
4
4
|
"description": "Budibase frontend core libraries used in builder and client",
|
|
5
5
|
"author": "Budibase",
|
|
6
6
|
"license": "MPL-2.0",
|
|
@@ -23,5 +23,5 @@
|
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"vitest": "^4.1.0"
|
|
25
25
|
},
|
|
26
|
-
"gitHead": "
|
|
26
|
+
"gitHead": "02e3837724b2102305a6aeb8590004680d403c32"
|
|
27
27
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ChatIdentityLink,
|
|
3
|
+
type ChatIdentityLinkProvider,
|
|
4
|
+
} from "@budibase/types"
|
|
5
|
+
import { BaseAPIClient } from "./types"
|
|
6
|
+
|
|
7
|
+
export interface SlackChannel {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MSTeamsChannel {
|
|
13
|
+
id: string
|
|
14
|
+
name: string
|
|
15
|
+
teamId: string
|
|
16
|
+
teamName: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ChatLinksEndpoints {
|
|
20
|
+
fetchChatIdentityLinks: (
|
|
21
|
+
provider?: ChatIdentityLinkProvider
|
|
22
|
+
) => Promise<ChatIdentityLink[]>
|
|
23
|
+
fetchSlackChannels: (agentId: string) => Promise<SlackChannel[]>
|
|
24
|
+
fetchMSTeamsChannels: (agentId: string) => Promise<MSTeamsChannel[]>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const buildChatLinksEndpoints = (
|
|
28
|
+
API: BaseAPIClient
|
|
29
|
+
): ChatLinksEndpoints => ({
|
|
30
|
+
fetchChatIdentityLinks: async provider => {
|
|
31
|
+
const query = provider
|
|
32
|
+
? `?${new URLSearchParams({ provider }).toString()}`
|
|
33
|
+
: ""
|
|
34
|
+
return await API.get({ url: `/api/chat-links${query}` })
|
|
35
|
+
},
|
|
36
|
+
fetchSlackChannels: async agentId => {
|
|
37
|
+
const query = new URLSearchParams({ agentId }).toString()
|
|
38
|
+
return await API.get({ url: `/api/slack-channels?${query}` })
|
|
39
|
+
},
|
|
40
|
+
fetchMSTeamsChannels: async agentId => {
|
|
41
|
+
const query = new URLSearchParams({ agentId }).toString()
|
|
42
|
+
return await API.get({ url: `/api/teams-channels?${query}` })
|
|
43
|
+
},
|
|
44
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EscalationContextDoc,
|
|
3
|
+
EscalationRespondResult,
|
|
4
|
+
EscalationResponse,
|
|
5
|
+
EscalationResult,
|
|
6
|
+
} from "@budibase/types"
|
|
7
|
+
import type { BaseAPIClient } from "./types"
|
|
8
|
+
|
|
9
|
+
export interface EscalationEndpoints {
|
|
10
|
+
fetchEscalationContext: (
|
|
11
|
+
escalationId: string
|
|
12
|
+
) => Promise<EscalationContextDoc>
|
|
13
|
+
fetchEscalationResult: (escalationId: string) => Promise<EscalationResult>
|
|
14
|
+
resolveEscalation: (
|
|
15
|
+
escalationId: string,
|
|
16
|
+
response: EscalationResponse
|
|
17
|
+
) => Promise<EscalationRespondResult>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const buildEscalationEndpoints = (
|
|
21
|
+
API: BaseAPIClient
|
|
22
|
+
): EscalationEndpoints => ({
|
|
23
|
+
fetchEscalationContext: async escalationId => {
|
|
24
|
+
return await API.get({
|
|
25
|
+
url: `/api/escalations/context/${escalationId}`,
|
|
26
|
+
})
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
fetchEscalationResult: async escalationId => {
|
|
30
|
+
return await API.get({
|
|
31
|
+
url: `/api/escalations/${escalationId}/result`,
|
|
32
|
+
})
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
resolveEscalation: async (escalationId, response) => {
|
|
36
|
+
return await API.post({
|
|
37
|
+
url: `/api/escalations/${escalationId}/resolve`,
|
|
38
|
+
body: { response },
|
|
39
|
+
})
|
|
40
|
+
},
|
|
41
|
+
})
|
package/src/api/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { buildFlagEndpoints } from "./flags"
|
|
|
23
23
|
import { buildLayoutEndpoints } from "./layouts"
|
|
24
24
|
import { buildOtherEndpoints } from "./other"
|
|
25
25
|
import { buildPermissionsEndpoints } from "./permissions"
|
|
26
|
+
import { buildProjectEndpoints } from "./projects"
|
|
26
27
|
import { buildQueryEndpoints } from "./queries"
|
|
27
28
|
import { buildRelationshipEndpoints } from "./relationships"
|
|
28
29
|
import { buildRoleEndpoints } from "./roles"
|
|
@@ -51,6 +52,8 @@ import { buildAgentTestEndpoints } from "./agentTests"
|
|
|
51
52
|
import { buildAgentLogEndpoints } from "./agentLogs"
|
|
52
53
|
import { buildAgentRequestEndpoints } from "./agentRequests"
|
|
53
54
|
import { buildChatAppEndpoints } from "./chatApps"
|
|
55
|
+
import { buildEscalationEndpoints } from "./escalations"
|
|
56
|
+
import { buildChatLinksEndpoints } from "./chatLinks"
|
|
54
57
|
import { buildFeatureFlagEndpoints } from "./features"
|
|
55
58
|
import { buildNavigationEndpoints } from "./navigation"
|
|
56
59
|
import { buildWorkspaceAppEndpoints } from "./workspaceApps"
|
|
@@ -323,12 +326,15 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
|
|
|
323
326
|
...buildAgentLogEndpoints(API),
|
|
324
327
|
...buildAgentRequestEndpoints(API),
|
|
325
328
|
...buildChatAppEndpoints(API),
|
|
329
|
+
...buildEscalationEndpoints(API),
|
|
330
|
+
...buildChatLinksEndpoints(API),
|
|
326
331
|
...buildFeatureFlagEndpoints(API),
|
|
327
332
|
deployment: buildDeploymentEndpoints(API),
|
|
328
333
|
viewV2: buildViewV2Endpoints(API),
|
|
329
334
|
rowActions: buildRowActionEndpoints(API),
|
|
330
335
|
oauth2: buildOAuth2Endpoints(API),
|
|
331
336
|
navigation: buildNavigationEndpoints(API),
|
|
337
|
+
projects: buildProjectEndpoints(API),
|
|
332
338
|
workspaceApp: buildWorkspaceAppEndpoints(API),
|
|
333
339
|
workspace: buildWorkspaceFavouriteEndpoints(API),
|
|
334
340
|
workspaceHome: buildWorkspaceHomeEndpoints(API),
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CreateProjectRequest,
|
|
3
|
+
CreateProjectResponse,
|
|
4
|
+
ExportProjectRequest,
|
|
5
|
+
FetchProjectsResponse,
|
|
6
|
+
ImportProjectRequest,
|
|
7
|
+
ImportProjectResponse,
|
|
8
|
+
UpdateProjectRequest,
|
|
9
|
+
UpdateProjectResponse,
|
|
10
|
+
} from "@budibase/types"
|
|
11
|
+
import { BaseAPIClient } from "./types"
|
|
12
|
+
|
|
13
|
+
export interface ProjectEndpoints {
|
|
14
|
+
fetch: () => Promise<FetchProjectsResponse>
|
|
15
|
+
create: (project: CreateProjectRequest) => Promise<CreateProjectResponse>
|
|
16
|
+
exportBundle: (id: string, body?: ExportProjectRequest) => Promise<Response>
|
|
17
|
+
importBundle: (
|
|
18
|
+
file: File,
|
|
19
|
+
body?: ImportProjectRequest
|
|
20
|
+
) => Promise<ImportProjectResponse>
|
|
21
|
+
update: (project: UpdateProjectRequest) => Promise<UpdateProjectResponse>
|
|
22
|
+
delete: (id: string, rev: string) => Promise<void>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const buildProjectEndpoints = (
|
|
26
|
+
API: BaseAPIClient
|
|
27
|
+
): ProjectEndpoints => ({
|
|
28
|
+
fetch: async () => {
|
|
29
|
+
return await API.get({
|
|
30
|
+
url: "/api/projects",
|
|
31
|
+
})
|
|
32
|
+
},
|
|
33
|
+
create: async project => {
|
|
34
|
+
return await API.post({
|
|
35
|
+
url: "/api/projects",
|
|
36
|
+
body: project,
|
|
37
|
+
})
|
|
38
|
+
},
|
|
39
|
+
exportBundle: async (id, body) => {
|
|
40
|
+
return await API.post<ExportProjectRequest | undefined, Response>({
|
|
41
|
+
url: `/api/projects/${id}/export`,
|
|
42
|
+
body,
|
|
43
|
+
parseResponse: response => response,
|
|
44
|
+
})
|
|
45
|
+
},
|
|
46
|
+
importBundle: async (file, body) => {
|
|
47
|
+
const formData = new FormData()
|
|
48
|
+
formData.append("file", file)
|
|
49
|
+
for (const [key, value] of Object.entries(body || {})) {
|
|
50
|
+
if (value !== undefined) {
|
|
51
|
+
formData.append(key, value)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return await API.post<FormData, ImportProjectResponse>({
|
|
55
|
+
url: "/api/projects/import",
|
|
56
|
+
body: formData,
|
|
57
|
+
json: false,
|
|
58
|
+
})
|
|
59
|
+
},
|
|
60
|
+
update: async project => {
|
|
61
|
+
const { _id, _rev, name, description, color } = project
|
|
62
|
+
return await API.put({
|
|
63
|
+
url: `/api/projects/${_id}`,
|
|
64
|
+
body: {
|
|
65
|
+
_id,
|
|
66
|
+
_rev,
|
|
67
|
+
name,
|
|
68
|
+
description,
|
|
69
|
+
color,
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
},
|
|
73
|
+
delete: async (id, rev) => {
|
|
74
|
+
return await API.delete({
|
|
75
|
+
url: `/api/projects/${id}/${rev}`,
|
|
76
|
+
})
|
|
77
|
+
},
|
|
78
|
+
})
|
package/src/api/types.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { MigrationEndpoints } from "./migrations"
|
|
|
20
20
|
import { OAuth2Endpoints } from "./oauth2"
|
|
21
21
|
import { OtherEndpoints } from "./other"
|
|
22
22
|
import { PermissionEndpoints } from "./permissions"
|
|
23
|
+
import { ProjectEndpoints } from "./projects"
|
|
23
24
|
import { PluginEndpoins } from "./plugins"
|
|
24
25
|
import { QueryEndpoints } from "./queries"
|
|
25
26
|
import { RelationshipEndpoints } from "./relationships"
|
|
@@ -39,6 +40,8 @@ import { AgentTestEndpoints } from "./agentTests"
|
|
|
39
40
|
import { AgentLogEndpoints } from "./agentLogs"
|
|
40
41
|
import { AgentRequestEndpoints } from "./agentRequests"
|
|
41
42
|
import { ChatAppEndpoints } from "./chatApps"
|
|
43
|
+
import { ChatLinksEndpoints } from "./chatLinks"
|
|
44
|
+
import { EscalationEndpoints } from "./escalations"
|
|
42
45
|
import { NavigationEndpoints } from "./navigation"
|
|
43
46
|
import { WorkspaceAppEndpoints } from "./workspaceApps"
|
|
44
47
|
import { ResourceEndpoints } from "./resource"
|
|
@@ -125,6 +128,8 @@ export type APIClient = BaseAPIClient &
|
|
|
125
128
|
AgentLogEndpoints &
|
|
126
129
|
AgentRequestEndpoints &
|
|
127
130
|
ChatAppEndpoints &
|
|
131
|
+
ChatLinksEndpoints &
|
|
132
|
+
EscalationEndpoints &
|
|
128
133
|
AnalyticsEndpoints &
|
|
129
134
|
AppEndpoints &
|
|
130
135
|
AttachmentEndpoints &
|
|
@@ -162,6 +167,7 @@ export type APIClient = BaseAPIClient &
|
|
|
162
167
|
viewV2: ViewV2Endpoints
|
|
163
168
|
oauth2: OAuth2Endpoints
|
|
164
169
|
navigation: NavigationEndpoints
|
|
170
|
+
projects: ProjectEndpoints
|
|
165
171
|
workspaceApp: WorkspaceAppEndpoints
|
|
166
172
|
workspace: WorkspaceFavouriteEndpoints
|
|
167
173
|
workspaceHome: WorkspaceHomeEndpoints
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Body, Button, Icon } from "@budibase/bbui"
|
|
3
|
+
import type { EscalationContextDoc } from "@budibase/types"
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
title?: string
|
|
7
|
+
summary?: string
|
|
8
|
+
// Live resolution from the poll (not the frozen tool output).
|
|
9
|
+
resolution: EscalationContextDoc["resolution"]
|
|
10
|
+
// Message relayed from resolve ("Response recorded." etc.) - set as soon as
|
|
11
|
+
// the action is taken here, before the poll flips resolution.
|
|
12
|
+
statusMessage?: string
|
|
13
|
+
// Dev-only: whether to show the inline Approve/Reject buttons.
|
|
14
|
+
showApproval?: boolean
|
|
15
|
+
resolving?: boolean
|
|
16
|
+
onApprove?: () => void
|
|
17
|
+
onReject?: () => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
title,
|
|
22
|
+
summary,
|
|
23
|
+
resolution,
|
|
24
|
+
statusMessage,
|
|
25
|
+
showApproval = false,
|
|
26
|
+
resolving = false,
|
|
27
|
+
onApprove,
|
|
28
|
+
onReject,
|
|
29
|
+
}: Props = $props()
|
|
30
|
+
|
|
31
|
+
// Resolved either by this card's action (statusMessage) or elsewhere (poll).
|
|
32
|
+
let isResolved = $derived(!!statusMessage || resolution !== "pending")
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<div class="escalation-card" aria-live="polite">
|
|
36
|
+
<div class="escalation-card-header">
|
|
37
|
+
<Icon name={isResolved ? "check-circle" : "clock"} size="M" />
|
|
38
|
+
<span class="escalation-card-title">{title || "Approval required"}</span>
|
|
39
|
+
{#if showApproval && !isResolved}
|
|
40
|
+
<span class="escalation-card-badge">Test mode</span>
|
|
41
|
+
{/if}
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{#if summary}
|
|
45
|
+
<Body size="S" color="var(--spectrum-global-color-gray-700)">
|
|
46
|
+
{summary}
|
|
47
|
+
</Body>
|
|
48
|
+
{/if}
|
|
49
|
+
|
|
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>
|
|
58
|
+
<div class="escalation-card-actions">
|
|
59
|
+
<Button cta disabled={resolving} on:click={() => onApprove?.()}>
|
|
60
|
+
Approve
|
|
61
|
+
</Button>
|
|
62
|
+
<Button secondary disabled={resolving} on:click={() => onReject?.()}>
|
|
63
|
+
Reject
|
|
64
|
+
</Button>
|
|
65
|
+
</div>
|
|
66
|
+
{:else}
|
|
67
|
+
<Body size="XS" color="var(--spectrum-global-color-gray-600)">
|
|
68
|
+
Awaiting a human response.
|
|
69
|
+
</Body>
|
|
70
|
+
{/if}
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<style>
|
|
74
|
+
.escalation-card {
|
|
75
|
+
align-self: flex-start;
|
|
76
|
+
width: 100%;
|
|
77
|
+
max-width: 100%;
|
|
78
|
+
box-sizing: border-box;
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
gap: var(--spacing-s);
|
|
82
|
+
padding: var(--spacing-m);
|
|
83
|
+
border: 1px solid var(--spectrum-global-color-gray-300);
|
|
84
|
+
border-radius: 12px;
|
|
85
|
+
background: var(--spectrum-global-color-gray-75);
|
|
86
|
+
}
|
|
87
|
+
.escalation-card-header {
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
gap: var(--spacing-s);
|
|
91
|
+
}
|
|
92
|
+
.escalation-card-title {
|
|
93
|
+
font-weight: 600;
|
|
94
|
+
font-size: 14px;
|
|
95
|
+
color: var(--spectrum-global-color-gray-900);
|
|
96
|
+
}
|
|
97
|
+
.escalation-card-badge {
|
|
98
|
+
margin-left: auto;
|
|
99
|
+
border-radius: 999px;
|
|
100
|
+
border: 1px solid var(--spectrum-global-color-blue-400);
|
|
101
|
+
color: var(--spectrum-global-color-blue-700);
|
|
102
|
+
background: var(--spectrum-global-color-blue-100);
|
|
103
|
+
font-size: 11px;
|
|
104
|
+
font-weight: 600;
|
|
105
|
+
line-height: 1;
|
|
106
|
+
padding: 4px 8px;
|
|
107
|
+
}
|
|
108
|
+
.escalation-card-actions {
|
|
109
|
+
display: flex;
|
|
110
|
+
gap: var(--spacing-s);
|
|
111
|
+
margin-top: var(--spacing-xs);
|
|
112
|
+
}
|
|
113
|
+
</style>
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
ChatConversation,
|
|
12
12
|
DraftChatConversation,
|
|
13
13
|
AgentMessageMetadata,
|
|
14
|
+
EscalationContextDoc,
|
|
15
|
+
EscalationRespondResult,
|
|
14
16
|
} from "@budibase/types"
|
|
15
17
|
import { Header } from "@budibase/shared-core"
|
|
16
18
|
import { tick, untrack } from "svelte"
|
|
@@ -19,6 +21,7 @@
|
|
|
19
21
|
import { formatToolName } from "../../utils/aiTools"
|
|
20
22
|
import ReasoningStatus from "./ReasoningStatus.svelte"
|
|
21
23
|
import ContextUsage from "./ContextUsage.svelte"
|
|
24
|
+
import EscalationCard from "./EscalationCard.svelte"
|
|
22
25
|
import {
|
|
23
26
|
DefaultChatTransport,
|
|
24
27
|
isTextUIPart,
|
|
@@ -38,6 +41,20 @@
|
|
|
38
41
|
onchatsaved?: (_event: {
|
|
39
42
|
detail: { chatId?: string; chat: ChatConversationLike }
|
|
40
43
|
}) => void
|
|
44
|
+
// Fired when an escalation parks; the consumer polls the outcome and
|
|
45
|
+
// injects it via appendAssistantMessage.
|
|
46
|
+
onEscalationPending?: (_detail: { escalationId: string }) => void
|
|
47
|
+
// Live resolution per escalationId (from the poll) - drives the card state.
|
|
48
|
+
escalationState?: Record<
|
|
49
|
+
string,
|
|
50
|
+
{ resolution: EscalationContextDoc["resolution"] }
|
|
51
|
+
>
|
|
52
|
+
// Dev-only: show the inline Approve/Reject buttons on the escalation card.
|
|
53
|
+
showInlineApproval?: boolean
|
|
54
|
+
onResolve?: (
|
|
55
|
+
_escalationId: string,
|
|
56
|
+
_accepted: boolean
|
|
57
|
+
) => Promise<EscalationRespondResult | undefined>
|
|
41
58
|
isAgentPreviewChat?: boolean
|
|
42
59
|
readOnly?: boolean
|
|
43
60
|
readOnlyReason?: "disabled" | "deleted" | "offline"
|
|
@@ -50,11 +67,47 @@
|
|
|
50
67
|
conversationStarters = [],
|
|
51
68
|
initialPrompt = "",
|
|
52
69
|
onchatsaved,
|
|
70
|
+
onEscalationPending,
|
|
71
|
+
escalationState,
|
|
72
|
+
showInlineApproval = false,
|
|
73
|
+
onResolve,
|
|
53
74
|
isAgentPreviewChat = false,
|
|
54
75
|
readOnly = false,
|
|
55
76
|
readOnlyReason,
|
|
56
77
|
}: Props = $props()
|
|
57
78
|
|
|
79
|
+
// Per-escalation in-flight flag + the message relayed from resolve, so the
|
|
80
|
+
// card shows the real outcome on click rather than a static label.
|
|
81
|
+
let resolvingEscalations = $state<Record<string, boolean>>({})
|
|
82
|
+
let resolveMessages = $state<Record<string, string>>({})
|
|
83
|
+
const handleResolve = async (escalationId: string, accepted: boolean) => {
|
|
84
|
+
resolvingEscalations = { ...resolvingEscalations, [escalationId]: true }
|
|
85
|
+
try {
|
|
86
|
+
const result = await onResolve?.(escalationId, accepted)
|
|
87
|
+
if (result?.message) {
|
|
88
|
+
resolveMessages = { ...resolveMessages, [escalationId]: result.message }
|
|
89
|
+
}
|
|
90
|
+
} finally {
|
|
91
|
+
resolvingEscalations = { ...resolvingEscalations, [escalationId]: false }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// The escalate part's input/output are loosely typed by the AI SDK, so the
|
|
96
|
+
// casts live here rather than cluttering the template.
|
|
97
|
+
const escalationCardProps = (part: { input?: unknown; output?: unknown }) => {
|
|
98
|
+
const output = part.output as { escalationId?: string } | undefined
|
|
99
|
+
const input = part.input as { title?: string; summary?: string } | undefined
|
|
100
|
+
const escalationId = output?.escalationId
|
|
101
|
+
return {
|
|
102
|
+
escalationId,
|
|
103
|
+
title: input?.title,
|
|
104
|
+
summary: input?.summary,
|
|
105
|
+
resolution:
|
|
106
|
+
(escalationId && escalationState?.[escalationId]?.resolution) ||
|
|
107
|
+
"pending",
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
58
111
|
let API = $state(
|
|
59
112
|
createAPIClient({
|
|
60
113
|
attachHeaders: headers => {
|
|
@@ -322,6 +375,48 @@
|
|
|
322
375
|
let messages = $derived(chatInstance.messages)
|
|
323
376
|
let lastMessage = $derived(messages[messages.length - 1])
|
|
324
377
|
|
|
378
|
+
// Notify the consumer of every unseen parked escalation. The escalate part's
|
|
379
|
+
// output stays frozen at pending_approval, so notifying only the first would
|
|
380
|
+
// shadow later escalations.
|
|
381
|
+
const notifiedEscalations = new Set<string>()
|
|
382
|
+
let pendingEscalationIds = $derived.by(() => {
|
|
383
|
+
const ids: string[] = []
|
|
384
|
+
for (const message of messages) {
|
|
385
|
+
for (const part of message.parts ?? []) {
|
|
386
|
+
if (
|
|
387
|
+
!isToolUIPart(part) ||
|
|
388
|
+
getToolName(part) !== "escalate" ||
|
|
389
|
+
part.state !== "output-available"
|
|
390
|
+
) {
|
|
391
|
+
continue
|
|
392
|
+
}
|
|
393
|
+
const output = part.output as
|
|
394
|
+
| { status?: string; escalationId?: string }
|
|
395
|
+
| undefined
|
|
396
|
+
if (output?.status === "pending_approval" && output.escalationId) {
|
|
397
|
+
ids.push(output.escalationId)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return ids
|
|
402
|
+
})
|
|
403
|
+
$effect(() => {
|
|
404
|
+
for (const escalationId of pendingEscalationIds) {
|
|
405
|
+
if (!notifiedEscalations.has(escalationId)) {
|
|
406
|
+
notifiedEscalations.add(escalationId)
|
|
407
|
+
onEscalationPending?.({ escalationId })
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
// Injects the polled resume outcome as a plain assistant turn - the agent's
|
|
413
|
+
// own reply self-frames the approval.
|
|
414
|
+
export function appendAssistantMessage(
|
|
415
|
+
message: UIMessage<AgentMessageMetadata>
|
|
416
|
+
) {
|
|
417
|
+
chatInstance.messages = [...chatInstance.messages, message]
|
|
418
|
+
}
|
|
419
|
+
|
|
325
420
|
let lastAssistantUsage = $derived(
|
|
326
421
|
messages.findLast(m => m.role === "assistant" && m.metadata?.usage)
|
|
327
422
|
?.metadata?.usage
|
|
@@ -678,6 +773,24 @@
|
|
|
678
773
|
{#each message.parts ?? [] as part, partIndex}
|
|
679
774
|
{#if isTextUIPart(part)}
|
|
680
775
|
<MarkdownViewer value={part.text} />
|
|
776
|
+
{:else if isToolUIPart(part) && getToolName(part) === "escalate"}
|
|
777
|
+
{@const card = escalationCardProps(part)}
|
|
778
|
+
<EscalationCard
|
|
779
|
+
title={card.title}
|
|
780
|
+
summary={card.summary}
|
|
781
|
+
resolution={card.resolution}
|
|
782
|
+
statusMessage={card.escalationId
|
|
783
|
+
? resolveMessages[card.escalationId]
|
|
784
|
+
: undefined}
|
|
785
|
+
showApproval={showInlineApproval}
|
|
786
|
+
resolving={!!card.escalationId &&
|
|
787
|
+
!!resolvingEscalations[card.escalationId]}
|
|
788
|
+
onApprove={() =>
|
|
789
|
+
card.escalationId && handleResolve(card.escalationId, true)}
|
|
790
|
+
onReject={() =>
|
|
791
|
+
card.escalationId &&
|
|
792
|
+
handleResolve(card.escalationId, false)}
|
|
793
|
+
/>
|
|
681
794
|
{:else if isToolUIPart(part)}
|
|
682
795
|
{@const rawToolName = getToolName(part)}
|
|
683
796
|
{@const displayToolName = formatToolName(
|