@adminforth/agent 2.9.0 → 2.10.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/README.md +4 -0
- package/dist/application/runTurnUseCase.d.ts +2 -1
- package/dist/application/runTurnUseCase.js +3 -3
- package/dist/custom/ChatFooter.vue +103 -81
- package/dist/custom/ChatHeader.vue +10 -3
- package/dist/custom/composables/agentStore/constants.ts +1 -1
- package/dist/custom/composables/agentStore/useAgentChat.ts +4 -3
- package/dist/custom/composables/agentStore/useAgentPlaceholder.ts +1 -0
- package/dist/custom/composables/agentStore/useAgentSessions.ts +2 -2
- package/dist/custom/composables/useAgentStore.ts +6 -29
- package/dist/custom/conversation_area/ConversationArea.vue +52 -24
- package/dist/custom/conversation_area/MessageRenderer.vue +9 -5
- package/dist/custom/conversation_area/ProcessingTimeline.vue +83 -123
- package/dist/custom/conversation_area/ReasoningRenderer.vue +14 -16
- package/dist/custom/conversation_area/TextRenderer.vue +4 -3
- package/dist/custom/conversation_area/ToolApprovalRenderer.vue +95 -54
- package/dist/custom/conversation_area/ToolRenderer.vue +44 -82
- package/dist/custom/speech_recognition_frontend/AudioLines.vue +1 -2
- package/dist/custom/speech_recognition_frontend/MicrophoneButon.vue +34 -32
- package/dist/custom/types.ts +16 -8
- package/dist/domain/agentEvents.d.ts +3 -2
- package/dist/tools/approvalFormatter.d.ts +19 -5
- package/dist/tools/approvalFormatter.js +88 -40
- package/dist/transport/http/context.d.ts +2 -1
- package/package.json +1 -1
- package/dist/custom/conversation_area/ToolsGroup.vue +0 -59
package/README.md
CHANGED
|
@@ -424,6 +424,10 @@ approval interrupt: generation pauses and the UI shows an approve/reject prompt.
|
|
|
424
424
|
resolves it via `POST /agent/approval`, and the run resumes (or, on reject, the model is
|
|
425
425
|
told the action was declined).
|
|
426
426
|
|
|
427
|
+
The prompt names the resource and columns by their labels and lists every value the tool
|
|
428
|
+
would write; for `update_record` each field shows its current value next to the new one.
|
|
429
|
+
Values of `masked` and `backendOnly` columns are never shown.
|
|
430
|
+
|
|
427
431
|
Where the pending approval lives depends on your setup: with `checkpointResource` configured
|
|
428
432
|
the LangGraph checkpoint is authoritative, so a resume survives a restart and works across
|
|
429
433
|
instances; with the in-memory fallback the state is held per process instance.
|
|
@@ -3,6 +3,7 @@ import type { AgentSessionStore } from "../persistence/sessionStore.js";
|
|
|
3
3
|
import type { SteerBuffer } from "../domain/steerBuffer.js";
|
|
4
4
|
import type { PluginOptions } from "../types.js";
|
|
5
5
|
import type { LlmPort } from "./ports.js";
|
|
6
|
+
import { type ApprovalDescription } from "../tools/approvalFormatter.js";
|
|
6
7
|
import type { BaseAgentTurnInput, DebugSink, HandleTurnInput, HandleEditTurnInput, PendingInterrupt, RunAndPersistAgentResponseInput, RunAndPersistAgentResponseResult } from "../domain/turnTypes.js";
|
|
7
8
|
/**
|
|
8
9
|
* Build the provider-agnostic resume payload for a human-in-the-loop turn.
|
|
@@ -78,7 +79,7 @@ export declare class RunTurnUseCase {
|
|
|
78
79
|
* page reload would otherwise leave the user with no way to answer it. Empty when
|
|
79
80
|
* nothing is pending.
|
|
80
81
|
*/
|
|
81
|
-
getPendingApprovals(sessionId: string, modeName?: string | null): Promise<
|
|
82
|
+
getPendingApprovals(sessionId: string, modeName?: string | null): Promise<ApprovalDescription[]>;
|
|
82
83
|
private requiresSessionOwnership;
|
|
83
84
|
/** Verify the session exists and belongs to the requesting admin user. */
|
|
84
85
|
assertSessionOwnership(sessionId: string, adminUser: BaseAgentTurnInput["adminUser"]): Promise<void>;
|
|
@@ -125,9 +125,9 @@ export class RunTurnUseCase {
|
|
|
125
125
|
/** Describe pending tool calls in plain language an admin (not a developer) can act on. */
|
|
126
126
|
describeApprovals(interrupts) {
|
|
127
127
|
const adminforth = this.deps.getAdminforth();
|
|
128
|
-
return interrupts
|
|
128
|
+
return Promise.all(interrupts
|
|
129
129
|
.flatMap((interrupt) => interrupt.requests)
|
|
130
|
-
.map((request) => formatApprovalRequest(adminforth, request));
|
|
130
|
+
.map((request) => formatApprovalRequest(adminforth, request)));
|
|
131
131
|
}
|
|
132
132
|
/**
|
|
133
133
|
* Describe what a session is still waiting for approval on, so a client that (re)loads
|
|
@@ -322,7 +322,7 @@ export class RunTurnUseCase {
|
|
|
322
322
|
type: "interrupt",
|
|
323
323
|
sessionId: prepared.sessionId,
|
|
324
324
|
interrupt,
|
|
325
|
-
approvals: this.describeApprovals(descriptors),
|
|
325
|
+
approvals: yield this.describeApprovals(descriptors),
|
|
326
326
|
}));
|
|
327
327
|
});
|
|
328
328
|
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
-
ref="promptInput"
|
|
4
|
-
class="w-full mb-
|
|
5
|
-
:style="{
|
|
3
|
+
ref="promptInput"
|
|
4
|
+
class="w-full mb-3 px-3 relative translate-x-[-50%] left-1/2"
|
|
5
|
+
:style="{
|
|
6
6
|
maxWidth: agentStore.isFullScreen ? remToPx(agentStore.MAX_WIDTH)+'px' : '100%',
|
|
7
7
|
transition: `transform ${agentTransitions.TRANSITION_DURATION}ms ease-in-out`
|
|
8
|
-
}"
|
|
8
|
+
}"
|
|
9
9
|
>
|
|
10
10
|
<SteerQueue />
|
|
11
11
|
<!-- <SteerDebugPanel /> -->
|
|
12
12
|
<div
|
|
13
|
-
class="w-full
|
|
14
|
-
|
|
13
|
+
class="w-full flex flex-col rounded-2xl border shadow-sm transition-colors duration-150
|
|
14
|
+
border-gray-300 bg-white focus-within:border-gray-400
|
|
15
|
+
dark:border-gray-600 dark:bg-gray-800 dark:focus-within:border-gray-500"
|
|
15
16
|
>
|
|
16
17
|
<textarea
|
|
17
18
|
v-if="!agentStore.isAudioChatMode"
|
|
@@ -20,36 +21,42 @@
|
|
|
20
21
|
rows="1"
|
|
21
22
|
@input="autoResize"
|
|
22
23
|
:class="[
|
|
23
|
-
|
|
24
|
+
`block w-full px-3.5 pt-3 pb-1 resize-none overflow-hidden bg-transparent border-none text-sm leading-6
|
|
25
|
+
focus:outline-none focus:ring-0
|
|
26
|
+
text-lightInputText placeholder:text-gray-500 dark:text-darkInputText dark:placeholder:text-gray-400`,
|
|
24
27
|
{ '!text-base': coreStore.isIos }
|
|
25
28
|
]"
|
|
26
29
|
:placeholder="placeholderText"
|
|
27
30
|
@keydown.enter.exact.prevent="sendMessage"
|
|
28
31
|
@keydown.esc="onEscape"
|
|
29
32
|
/>
|
|
30
|
-
|
|
33
|
+
<!-- in audio chat mode the voice button is centered between the mode picker and an empty column -->
|
|
31
34
|
<div
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class="relative"
|
|
35
|
+
class="items-center gap-1 px-2 pb-2"
|
|
36
|
+
:class="agentStore.isAudioChatMode ? 'grid grid-cols-[1fr_auto_1fr] pt-2' : 'flex pt-1'"
|
|
35
37
|
>
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
transition-colors duration-200 hover:bg-gray-100
|
|
41
|
-
dark:text-darkListTableHeadingText dark:bg-gray-700 dark:hover:bg-gray-800"
|
|
42
|
-
:class="isModeMenuOpen ? 'bg-gray-100 dark:bg-gray-700' : ''"
|
|
43
|
-
title="Select mode"
|
|
44
|
-
type="button"
|
|
45
|
-
@click="toggleModeMenu"
|
|
38
|
+
<div
|
|
39
|
+
v-if="agentStore.availableModes.length > 1"
|
|
40
|
+
ref="modeMenu"
|
|
41
|
+
class="relative justify-self-start"
|
|
46
42
|
>
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
class="
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
<button
|
|
44
|
+
aria-label="Select mode"
|
|
45
|
+
class="flex items-center gap-1.5 h-7 pl-2 pr-1.5 rounded-lg text-xs font-medium whitespace-nowrap transition-colors duration-150
|
|
46
|
+
text-gray-700 hover:bg-gray-100
|
|
47
|
+
dark:text-gray-200 dark:hover:bg-gray-700"
|
|
48
|
+
:class="isModeMenuOpen ? 'bg-gray-100 dark:bg-gray-700' : ''"
|
|
49
|
+
title="Select mode"
|
|
50
|
+
type="button"
|
|
51
|
+
@click="toggleModeMenu"
|
|
52
|
+
>
|
|
53
|
+
<IconSparklesSolid class="w-3.5 h-3.5 text-lightPrimary dark:text-darkPrimary" />
|
|
54
|
+
{{ agentStore.activeModeName }}
|
|
55
|
+
<IconChevronDown
|
|
56
|
+
class="w-3.5 h-3.5 transition-transform duration-200"
|
|
57
|
+
:class="isModeMenuOpen ? 'rotate-180' : 'rotate-0'"
|
|
58
|
+
/>
|
|
59
|
+
</button>
|
|
53
60
|
<Transition
|
|
54
61
|
enter-active-class="transition ease-out duration-200"
|
|
55
62
|
enter-from-class="opacity-0 scale-95"
|
|
@@ -58,69 +65,75 @@
|
|
|
58
65
|
leave-from-class="opacity-100 scale-100"
|
|
59
66
|
leave-to-class="opacity-0 scale-95"
|
|
60
67
|
>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
<div
|
|
69
|
+
v-if="isModeMenuOpen"
|
|
70
|
+
class="absolute z-20 bottom-full left-0 mb-2 min-w-44 p-1 rounded-xl border border-gray-200 bg-white shadow-lg dark:border-gray-600 dark:bg-gray-800"
|
|
71
|
+
>
|
|
72
|
+
<button
|
|
73
|
+
v-for="mode in agentStore.availableModes"
|
|
74
|
+
:key="mode.name"
|
|
75
|
+
class="flex items-center justify-between gap-3 w-full px-2.5 py-1.5 rounded-lg text-left text-sm transition-colors duration-150
|
|
76
|
+
text-lightInputText hover:bg-gray-100 dark:text-darkInputText dark:hover:bg-gray-700"
|
|
77
|
+
type="button"
|
|
78
|
+
@click="selectMode(mode.name)"
|
|
79
|
+
>
|
|
80
|
+
{{ mode.name }}
|
|
81
|
+
<IconCheck
|
|
82
|
+
v-if="mode.name === agentStore.activeModeName"
|
|
83
|
+
class="w-4 h-4 text-lightPrimary dark:text-darkPrimary"
|
|
84
|
+
/>
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
</Transition>
|
|
88
|
+
</div>
|
|
89
|
+
<div
|
|
90
|
+
class="ml-auto flex items-center gap-1"
|
|
91
|
+
:class="agentStore.isAudioChatMode ? 'col-start-2' : ''"
|
|
92
|
+
>
|
|
93
|
+
<div v-if="props.meta.hasAudioAdapter" v-show="!agentStore.isEditingMessage" class="contents">
|
|
94
|
+
<MicrophoneButton />
|
|
95
|
+
</div>
|
|
96
|
+
<template v-if="!agentStore.isAudioChatMode">
|
|
97
|
+
<EditActionButtons
|
|
98
|
+
v-if="agentStore.isEditingMessage"
|
|
99
|
+
:confirm-disabled="!agentStore.trimmedUserMessage"
|
|
100
|
+
@cancel="cancelEdit"
|
|
101
|
+
@confirm="sendMessage"
|
|
102
|
+
/>
|
|
65
103
|
<button
|
|
66
|
-
v-
|
|
67
|
-
:key="mode.name"
|
|
68
|
-
class="block w-full px-3 py-2 text-left text-sm text-lightInputText transition-colors duration-150 hover:bg-gray-100 dark:text-darkInputText dark:hover:bg-gray-700"
|
|
69
|
-
:class="mode.name === agentStore.activeModeName ? 'bg-gray-100 dark:bg-gray-700' : ''"
|
|
104
|
+
v-else-if="!agentStore.isResponseInProgress"
|
|
70
105
|
type="button"
|
|
71
|
-
|
|
106
|
+
:class="ROUND_PRIMARY_BUTTON_CLASSES"
|
|
107
|
+
:disabled="!agentStore.trimmedUserMessage"
|
|
108
|
+
:title="$t('Send message')"
|
|
109
|
+
:aria-label="$t('Send message')"
|
|
110
|
+
@click="sendMessage"
|
|
72
111
|
>
|
|
73
|
-
|
|
112
|
+
<IconArrowUp class="w-4 h-4" />
|
|
74
113
|
</button>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
/>
|
|
88
|
-
</template>
|
|
89
|
-
<template v-else>
|
|
90
|
-
<Button
|
|
91
|
-
v-if="!agentStore.isResponseInProgress"
|
|
92
|
-
class=" !p-0 h-7 w-7 transition-opacity duration-200"
|
|
93
|
-
@click="sendMessage"
|
|
94
|
-
:disabled="!agentStore.trimmedUserMessage"
|
|
95
|
-
>
|
|
96
|
-
<IconArrowUpOutline
|
|
97
|
-
class="w-6 h-6
|
|
98
|
-
text-white"
|
|
99
|
-
/>
|
|
100
|
-
</Button>
|
|
101
|
-
<Button
|
|
102
|
-
v-else
|
|
103
|
-
class="right-4 bottom-2 !p-0 h-7 w-7"
|
|
104
|
-
@click="stopCurrentRequest"
|
|
105
|
-
>
|
|
106
|
-
<div
|
|
107
|
-
class="w-2.5 h-2.5 bg-white rounded-sm"
|
|
108
|
-
/>
|
|
109
|
-
</Button>
|
|
110
|
-
</template>
|
|
111
|
-
</template>
|
|
114
|
+
<button
|
|
115
|
+
v-else
|
|
116
|
+
type="button"
|
|
117
|
+
:class="ROUND_PRIMARY_BUTTON_CLASSES"
|
|
118
|
+
:title="$t('Stop')"
|
|
119
|
+
:aria-label="$t('Stop')"
|
|
120
|
+
@click="stopCurrentRequest"
|
|
121
|
+
>
|
|
122
|
+
<span class="w-2.5 h-2.5 rounded-sm bg-current" />
|
|
123
|
+
</button>
|
|
124
|
+
</template>
|
|
125
|
+
</div>
|
|
112
126
|
</div>
|
|
113
127
|
</div>
|
|
114
128
|
</div>
|
|
115
129
|
</template>
|
|
116
130
|
|
|
117
131
|
<script setup lang="ts">
|
|
118
|
-
import {
|
|
119
|
-
import { useTemplateRef, onMounted, ref,
|
|
132
|
+
import { IconArrowUp, IconCheck, IconChevronDown, IconSparklesSolid } from '@iconify-prerendered/vue-heroicons';
|
|
133
|
+
import { useTemplateRef, onMounted, ref, watch, computed } from 'vue';
|
|
120
134
|
import { onClickOutside } from '@vueuse/core'
|
|
121
135
|
import { useAgentStore } from './composables/useAgentStore';
|
|
122
136
|
import { useAgentTransitions } from './composables/useAgentTransitions';
|
|
123
|
-
import { Button } from '@/afcl';
|
|
124
137
|
import { useCoreStore } from '@/stores/core';
|
|
125
138
|
import { remToPx } from './utils';
|
|
126
139
|
import MicrophoneButton from './speech_recognition_frontend/MicrophoneButon.vue';
|
|
@@ -143,6 +156,12 @@ const props = defineProps<{
|
|
|
143
156
|
conversationAreaRef: any
|
|
144
157
|
}>();
|
|
145
158
|
|
|
159
|
+
const ROUND_PRIMARY_BUTTON_CLASSES = `flex items-center justify-center w-8 h-8 shrink-0 rounded-full transition-colors duration-150
|
|
160
|
+
bg-lightPrimary text-lightPrimaryContrast hover:opacity-90
|
|
161
|
+
dark:bg-darkPrimary dark:text-darkPrimaryContrast
|
|
162
|
+
disabled:bg-gray-200 disabled:text-gray-400 disabled:hover:opacity-100 disabled:cursor-default
|
|
163
|
+
dark:disabled:bg-gray-700 dark:disabled:text-gray-500`;
|
|
164
|
+
|
|
146
165
|
const textInput = useTemplateRef('textInput');
|
|
147
166
|
const modeMenu = useTemplateRef('modeMenu');
|
|
148
167
|
const agentStore = useAgentStore();
|
|
@@ -163,14 +182,17 @@ onMounted(async () => {
|
|
|
163
182
|
agentStore.setAvailableModes(props.meta.modes, props.meta.defaultModeName);
|
|
164
183
|
agentStore.setCurrentGenerationModeFromLocalStorage();
|
|
165
184
|
agentStore.setEditingEnabled(props.meta.editingEnabled);
|
|
166
|
-
agentStore.regisrerTextInput(textInput.value);
|
|
167
185
|
textInput.value?.focus();
|
|
168
186
|
autoResize();
|
|
169
187
|
});
|
|
170
188
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
});
|
|
189
|
+
// The textarea is re-created after leaving audio chat mode; keep the store pointing at the
|
|
190
|
+
// live element, otherwise focusTextInput() targets a detached one and does nothing.
|
|
191
|
+
watch(textInput, (el) => agentStore.regisrerTextInput(el), { immediate: true, flush: 'post' });
|
|
192
|
+
|
|
193
|
+
// Re-measure whenever the text changes programmatically (suggestion picked, edit restored)
|
|
194
|
+
// or the textarea is re-mounted after leaving audio chat mode.
|
|
195
|
+
watch([() => agentStore.userMessageInput, textInput], autoResize, { flush: 'post' });
|
|
174
196
|
|
|
175
197
|
function autoResize() {
|
|
176
198
|
const el = textInput.value
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
ref="headerRef"
|
|
4
4
|
class="flex items-center justify-between h-14 border-b border-gray-200 dark:border-gray-700"
|
|
5
5
|
>
|
|
6
|
-
<div
|
|
7
|
-
class="flex items-center"
|
|
6
|
+
<div
|
|
7
|
+
class="flex items-center shrink-0"
|
|
8
8
|
>
|
|
9
9
|
<IconBarsOutline
|
|
10
10
|
class="m-2 w-8 h-8 p-1 cursor-pointer hover:scale-110 rounded transition-colors duration-200
|
|
@@ -47,7 +47,10 @@
|
|
|
47
47
|
@click="agentStore.setFullScreen(false)"
|
|
48
48
|
/>
|
|
49
49
|
</div>
|
|
50
|
-
<
|
|
50
|
+
<p class="flex-1 min-w-0 truncate px-1 text-sm font-semibold text-lightNavbarText dark:text-darkNavbarText">
|
|
51
|
+
{{ isNewChat ? $t('New chat') : agentStore.currentSession.title }}
|
|
52
|
+
</p>
|
|
53
|
+
<div class="flex items-center justify-center shrink-0">
|
|
51
54
|
<Button
|
|
52
55
|
@click="agentStore.createPreSession(); agentStore.setSessionHistoryOpen(false); agentStore.focusTextInput();"
|
|
53
56
|
class="!py-1 !px-2 max-w-64 mr-2"
|
|
@@ -67,12 +70,16 @@
|
|
|
67
70
|
|
|
68
71
|
|
|
69
72
|
<script setup>
|
|
73
|
+
import { computed } from 'vue';
|
|
70
74
|
import { IconArrowsPointingOut, IconArrowsPointingIn } from '@iconify-prerendered/vue-heroicons';
|
|
71
75
|
import { IconCloseOutline, IconBarsOutline, IconCloseSidebarSolid, IconOpenSidebarSolid, IconPlusOutline } from '@iconify-prerendered/vue-flowbite';
|
|
72
76
|
import { useAgentStore } from './composables/useAgentStore';
|
|
73
77
|
import { useCoreStore } from '@/stores/core';
|
|
74
78
|
import { Button } from '@/afcl';
|
|
79
|
+
import { PRE_SESSION_ID } from './composables/agentStore/constants';
|
|
75
80
|
|
|
76
81
|
const agentStore = useAgentStore();
|
|
77
82
|
const coreStore = useCoreStore();
|
|
83
|
+
|
|
84
|
+
const isNewChat = computed(() => !agentStore.currentSession || agentStore.currentSession.sessionId === PRE_SESSION_ID);
|
|
78
85
|
</script>
|
|
@@ -6,7 +6,7 @@ export const DEFAULT_CHAT_WIDTH = 30;
|
|
|
6
6
|
export const MAX_WIDTH = 60;
|
|
7
7
|
export const MIN_WIDTH = 25;
|
|
8
8
|
|
|
9
|
-
export const DEFAULT_TEXTAREA_PLACEHOLDER = '
|
|
9
|
+
export const DEFAULT_TEXTAREA_PLACEHOLDER = 'Ask anything about your data…';
|
|
10
10
|
export const PLACEHOLDER_TYPING_DELAY_MS = 60;
|
|
11
11
|
export const PLACEHOLDER_DELETING_DELAY_MS = 35;
|
|
12
12
|
export const PLACEHOLDER_HOLD_DELAY_MS = 3000;
|
|
@@ -2,6 +2,7 @@ import { DefaultChatTransport } from 'ai';
|
|
|
2
2
|
import { ref, shallowRef, watch, type Ref } from 'vue';
|
|
3
3
|
import { Chat } from '../../chat';
|
|
4
4
|
import { getCurrentPageContext } from './pageContext';
|
|
5
|
+
import type { IApprovalDescription } from '../../types';
|
|
5
6
|
// const { DefaultChatTransport } = await import('ai');
|
|
6
7
|
|
|
7
8
|
type AgentImportMeta = ImportMeta & {
|
|
@@ -14,7 +15,7 @@ type CreateAgentChatManagerOptions = {
|
|
|
14
15
|
lastMessage: Ref<string>;
|
|
15
16
|
activeModeName: Ref<string | null>;
|
|
16
17
|
onOpenPage: (targetPath: string) => void;
|
|
17
|
-
onToolApprovalRequest: (sessionId: string,
|
|
18
|
+
onToolApprovalRequest: (sessionId: string, approvals: IApprovalDescription[]) => void;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export function createAgentChatManager({
|
|
@@ -119,7 +120,7 @@ export function createAgentChatManager({
|
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
if (dataPart?.type === 'data-interrupt' && typeof dataPart.data?.sessionId === 'string') {
|
|
122
|
-
onToolApprovalRequest(dataPart.data.sessionId, dataPart.data.
|
|
123
|
+
onToolApprovalRequest(dataPart.data.sessionId, dataPart.data.approvals);
|
|
123
124
|
return;
|
|
124
125
|
}
|
|
125
126
|
|
|
@@ -165,7 +166,7 @@ export function createAgentChatManager({
|
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
if (dataPart?.type === 'data-interrupt' && typeof dataPart.data?.sessionId === 'string') {
|
|
168
|
-
onToolApprovalRequest(dataPart.data.sessionId, dataPart.data.
|
|
169
|
+
onToolApprovalRequest(dataPart.data.sessionId, dataPart.data.approvals);
|
|
169
170
|
return;
|
|
170
171
|
}
|
|
171
172
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ComputedRef, Ref, ShallowRef } from 'vue';
|
|
2
2
|
import { callAdminForthApi } from '@/utils';
|
|
3
3
|
import type { Chat } from '../../chat';
|
|
4
|
-
import type { IAgentSession, ISessionsListItem, IPart } from '../../types';
|
|
4
|
+
import type { IAgentSession, IApprovalDescription, ISessionsListItem, IPart } from '../../types';
|
|
5
5
|
import { PRE_SESSION_ID, STEER_PERSIST_PREFIX } from './constants';
|
|
6
6
|
import { i18nInstance } from '@/i18n';
|
|
7
7
|
|
|
@@ -23,7 +23,7 @@ type CreateAgentSessionManagerOptions = {
|
|
|
23
23
|
blockCloseOfChat: Ref<boolean>;
|
|
24
24
|
adminforth: AdminforthLike;
|
|
25
25
|
setCurrentChat: (sessionId: string) => void;
|
|
26
|
-
restorePendingToolApproval: (sessionId: string,
|
|
26
|
+
restorePendingToolApproval: (sessionId: string, approvals: IApprovalDescription[]) => void;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
export function createAgentSessionManager({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineStore } from 'pinia';
|
|
2
|
-
import { IAgentSession, ISessionsListItem } from '../types';
|
|
2
|
+
import { IAgentSession, IApprovalDescription, ISessionsListItem } from '../types';
|
|
3
3
|
import { ref, nextTick, computed, watch, onMounted } from 'vue';
|
|
4
4
|
import { useRouter } from 'vue-router';
|
|
5
5
|
import { useAdminforth } from '@/adminforth';
|
|
@@ -69,10 +69,11 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
69
69
|
lastMessage,
|
|
70
70
|
activeModeName,
|
|
71
71
|
onOpenPage: openAgentPage,
|
|
72
|
-
onToolApprovalRequest:
|
|
72
|
+
onToolApprovalRequest: addToolApprovalPart,
|
|
73
73
|
});
|
|
74
74
|
const {
|
|
75
75
|
userMessagePlaceholder,
|
|
76
|
+
placeholderMessages,
|
|
76
77
|
hasTypedMessageInPageSession,
|
|
77
78
|
fetchPlaceholderMessages,
|
|
78
79
|
stopPlaceholderAnimation,
|
|
@@ -108,28 +109,13 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
108
109
|
isAudioChatMode.value = isAudioChat;
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
function
|
|
112
|
-
const interrupts = Array.isArray(interrupt) ? interrupt : [interrupt];
|
|
113
|
-
|
|
114
|
-
return interrupts.flatMap((item: any) => {
|
|
115
|
-
const value = item?.value ?? item;
|
|
116
|
-
const actionRequests = Array.isArray(value?.actionRequests) ? value.actionRequests : [];
|
|
117
|
-
|
|
118
|
-
return actionRequests.map((actionRequest: any) => (
|
|
119
|
-
typeof actionRequest?.description === 'string'
|
|
120
|
-
? actionRequest.description
|
|
121
|
-
: String(actionRequest?.name ?? 'Tool execution pending approval')
|
|
122
|
-
));
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function addToolApprovalPart(sessionId: string, messages: string[]) {
|
|
112
|
+
function addToolApprovalPart(sessionId: string, approvals: IApprovalDescription[]) {
|
|
127
113
|
const approvalPart = {
|
|
128
114
|
type: 'data-tool-approval' as const,
|
|
129
115
|
data: {
|
|
130
116
|
sessionId,
|
|
131
117
|
status: 'pending' as const,
|
|
132
|
-
|
|
118
|
+
approvals,
|
|
133
119
|
},
|
|
134
120
|
};
|
|
135
121
|
const lastChatMessage = currentChat.value?.lastMessage;
|
|
@@ -146,16 +132,6 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
146
132
|
});
|
|
147
133
|
}
|
|
148
134
|
|
|
149
|
-
function addToolApprovalMessage(sessionId: string, interrupt: unknown, approvals?: unknown) {
|
|
150
|
-
// The backend describes each pending tool call in plain language; its raw
|
|
151
|
-
// descriptions are only the fallback when it could not.
|
|
152
|
-
const formatted = Array.isArray(approvals)
|
|
153
|
-
? approvals.filter((line): line is string => typeof line === 'string' && Boolean(line))
|
|
154
|
-
: [];
|
|
155
|
-
|
|
156
|
-
addToolApprovalPart(sessionId, formatted.length ? formatted : getToolApprovalMessages(interrupt));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
135
|
async function submitToolApproval(sessionId: string, decision: 'approve' | 'reject') {
|
|
160
136
|
const message = (currentChat.value?.messages ?? [])
|
|
161
137
|
.findLast(candidate => candidate.role === 'assistant' && (candidate.parts ?? []).some((part: any) => {
|
|
@@ -628,6 +604,7 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
628
604
|
submitToolApproval,
|
|
629
605
|
userMessageInput,
|
|
630
606
|
userMessagePlaceholder,
|
|
607
|
+
placeholderMessages,
|
|
631
608
|
chatMessages: computed(() => currentChat.value?.messages || []),
|
|
632
609
|
trimmedUserMessage,
|
|
633
610
|
isResponseInProgress,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
class="absolute bg-black/10 backdrop-blur-md z-10 h-full w-full"
|
|
9
9
|
>
|
|
10
10
|
</div>
|
|
11
|
-
<div
|
|
11
|
+
<div class="relative flex-1 min-h-0 overflow-hidden [container-type:size]" @click="recalculateScroll()">
|
|
12
12
|
<CustomAutoScrollContainer
|
|
13
13
|
v-if="showScrollContainer"
|
|
14
14
|
class="relative h-full flex flex-col overflow-y-auto translate-x-[-50%] left-1/2"
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
}"
|
|
27
27
|
:style="{
|
|
28
28
|
maxWidth: agentStore.isFullScreen ? agentStore.MAX_WIDTH+'rem' : '100%',
|
|
29
|
+
paddingBottom: showBottomSpacer ? `max(0px, 100cqh - ${lastTurnHeight}px)` : undefined,
|
|
29
30
|
transition: `
|
|
30
31
|
max-width ${agentTransitions.TRANSITION_DURATION}ms ease-in-out,
|
|
31
32
|
transform ${agentTransitions.TRANSITION_DURATION}ms ease-in-out
|
|
@@ -41,17 +42,36 @@
|
|
|
41
42
|
>
|
|
42
43
|
<MessageRenderer :message="message" :isLastMessageInChat="index === props.messages.length - 1"/>
|
|
43
44
|
</div>
|
|
44
|
-
|
|
45
|
+
<!-- absolute, so it doesn't resize the scroll content when the container resizes (see the spacer comment) -->
|
|
46
|
+
<div
|
|
45
47
|
v-if="props.messages.length === 0"
|
|
46
|
-
class="
|
|
47
|
-
:style="{
|
|
48
|
-
height: chatContainerRef ? chatContainerRef.clientHeight + 'px' : '100%',
|
|
49
|
-
}"
|
|
48
|
+
class="absolute inset-0 flex flex-col items-center justify-center px-7 pb-10 text-center"
|
|
50
49
|
>
|
|
51
|
-
<
|
|
52
|
-
|
|
50
|
+
<div class="flex items-center justify-center w-11 h-11 rounded-xl bg-lightPrimary/10 text-lightPrimary dark:bg-darkPrimary/15 dark:text-darkPrimary">
|
|
51
|
+
<IconSparkles class="w-6 h-6" />
|
|
52
|
+
</div>
|
|
53
|
+
<h2 class="mt-4 text-xl font-semibold text-lightNavbarText dark:text-darkNavbarText">
|
|
54
|
+
{{ $t('How can I help?') }}
|
|
55
|
+
</h2>
|
|
56
|
+
<p class="mt-1.5 max-w-xs text-sm leading-relaxed text-gray-600 dark:text-gray-400">
|
|
57
|
+
{{ $t('Ask about your data, build charts or update records.') }}
|
|
58
|
+
</p>
|
|
59
|
+
<div v-if="suggestions.length" class="mt-7 w-full max-w-sm flex flex-col gap-2 text-left">
|
|
60
|
+
<p class="pl-0.5 text-xs font-medium text-gray-500 dark:text-gray-400">{{ $t('Try asking') }}</p>
|
|
61
|
+
<button
|
|
62
|
+
v-for="suggestion in suggestions"
|
|
63
|
+
:key="suggestion"
|
|
64
|
+
type="button"
|
|
65
|
+
class="group flex items-center gap-2.5 w-full px-3 py-2.5 rounded-xl border text-sm text-left transition-colors duration-150
|
|
66
|
+
border-gray-200 text-lightNavbarText hover:border-gray-300 hover:bg-gray-50
|
|
67
|
+
dark:border-gray-700 dark:text-darkNavbarText dark:hover:border-gray-600 dark:hover:bg-gray-800"
|
|
68
|
+
@click="applySuggestion(suggestion)"
|
|
69
|
+
>
|
|
70
|
+
<span class="flex-1 min-w-0">{{ suggestion }}</span>
|
|
71
|
+
<IconArrowRight class="w-4 h-4 shrink-0 text-gray-400 transition-transform duration-150 group-hover:translate-x-0.5" />
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
53
74
|
</div>
|
|
54
|
-
<div v-if="showBottomSpacer" class="w-full" :style="{ height: spacerHeight + 'px' }"></div>
|
|
55
75
|
</CustomAutoScrollContainer>
|
|
56
76
|
<Transition
|
|
57
77
|
enter-active-class="transition-all duration-250 ease-out"
|
|
@@ -91,6 +111,7 @@
|
|
|
91
111
|
import type { IMessage } from '../types';
|
|
92
112
|
import { useTemplateRef, ref, onMounted, onUnmounted, watch, nextTick, defineAsyncComponent, computed } from 'vue';
|
|
93
113
|
import { IconArrowDownOutline } from '@iconify-prerendered/vue-flowbite';
|
|
114
|
+
import { IconArrowRight, IconSparkles } from '@iconify-prerendered/vue-heroicons';
|
|
94
115
|
import SessionsHistory from '../SessionsHistory.vue';
|
|
95
116
|
import { useAgentStore } from '../composables/useAgentStore';
|
|
96
117
|
import { useAgentTransitions } from '../composables/useAgentTransitions';
|
|
@@ -113,7 +134,6 @@ const innerScrollContainerRef = ref<HTMLElement | null>(null);
|
|
|
113
134
|
const agentStore = useAgentStore();
|
|
114
135
|
const agentTransitions = useAgentTransitions();
|
|
115
136
|
const showScrollContainer = ref(true);
|
|
116
|
-
const chatContainerRef = ref<HTMLElement | null>(null);
|
|
117
137
|
const messagesRefs = ref<Array<HTMLElement | null>>([]);
|
|
118
138
|
/*
|
|
119
139
|
* Whenever user sends a message, it adds a bottom spacer, that takes the remaining height
|
|
@@ -121,14 +141,21 @@ const messagesRefs = ref<Array<HTMLElement | null>>([]);
|
|
|
121
141
|
*
|
|
122
142
|
* On send message, happens the following logic:
|
|
123
143
|
* 1) showBottomSpacer is set to true
|
|
124
|
-
* 2) updateSpacerHeight is called, which
|
|
125
|
-
* 3) Spacer moves text up
|
|
144
|
+
* 2) updateSpacerHeight is called, which measures the last turn (last user + last agent message)
|
|
145
|
+
* 3) Spacer moves text up
|
|
146
|
+
*
|
|
147
|
+
* The spacer is the scroll element's padding-bottom, resolved in CSS (100cqh minus the last turn), so
|
|
148
|
+
* it follows the container height in the same layout pass, e.g. when the prompt textarea grows.
|
|
149
|
+
* It is padding rather than a child element on purpose: custom-vue-scrollbar measures the viewport and
|
|
150
|
+
* the content with separate debounced observers, and when both resize together it briefly computes a
|
|
151
|
+
* wrong scroll range and flashes the scrollbar thumb. Padding keeps the content size unchanged.
|
|
126
152
|
*/
|
|
127
153
|
const showBottomSpacer = ref(false);
|
|
128
|
-
const
|
|
154
|
+
const lastTurnHeight = ref(0);
|
|
129
155
|
const MASK_HEIGHT = 20;
|
|
130
156
|
const EMPTY_MESSAGE_HEIGHT = 18;
|
|
131
157
|
const THRESHOLD_TO_SHOW_BUTTON = 10;
|
|
158
|
+
const MAX_SUGGESTIONS = 3;
|
|
132
159
|
let messageResizeObserver: ResizeObserver | null = null;
|
|
133
160
|
let observedLastUserMessageElement: HTMLElement | null = null;
|
|
134
161
|
let observedLastAgentMessageElement: HTMLElement | null = null;
|
|
@@ -140,6 +167,14 @@ const showAnimationInScrollToBottomButton = computed(() => {
|
|
|
140
167
|
return agentStore.isResponseInProgress && showScrollToBottomButton.value;
|
|
141
168
|
});
|
|
142
169
|
|
|
170
|
+
// The configured placeholder examples double as clickable starters on an empty chat.
|
|
171
|
+
const suggestions = computed(() => agentStore.placeholderMessages.slice(0, MAX_SUGGESTIONS));
|
|
172
|
+
|
|
173
|
+
function applySuggestion(suggestion: string) {
|
|
174
|
+
agentStore.userMessageInput = suggestion;
|
|
175
|
+
agentStore.focusTextInput();
|
|
176
|
+
}
|
|
177
|
+
|
|
143
178
|
onMounted(async () => {
|
|
144
179
|
messageResizeObserver = new ResizeObserver(() => {
|
|
145
180
|
scheduleSpacerHeightUpdate();
|
|
@@ -195,7 +230,7 @@ watch(scrollContainer, async () => {
|
|
|
195
230
|
|
|
196
231
|
function resetSpacer() {
|
|
197
232
|
showBottomSpacer.value = false;
|
|
198
|
-
|
|
233
|
+
lastTurnHeight.value = 0;
|
|
199
234
|
}
|
|
200
235
|
|
|
201
236
|
function getLastMessageElement(role: 'user' | 'assistant') {
|
|
@@ -230,15 +265,7 @@ async function updateSpacerHeight() {
|
|
|
230
265
|
return;
|
|
231
266
|
}
|
|
232
267
|
|
|
233
|
-
|
|
234
|
-
if (!clientHeight) {
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const lastUserMessageHeight = getHeightOfLastUserMessage();
|
|
239
|
-
const lastAgentMessageHeight = getHeightOfLastAgentMessage();
|
|
240
|
-
const newSpacerHeight = Math.max(0, clientHeight - (lastUserMessageHeight + MASK_HEIGHT + lastAgentMessageHeight));
|
|
241
|
-
spacerHeight.value = newSpacerHeight;
|
|
268
|
+
lastTurnHeight.value = getHeightOfLastUserMessage() + MASK_HEIGHT + getHeightOfLastAgentMessage();
|
|
242
269
|
}
|
|
243
270
|
|
|
244
271
|
function scheduleSpacerHeightUpdate() {
|
|
@@ -328,7 +355,8 @@ async function handleSendMessage() {
|
|
|
328
355
|
|
|
329
356
|
function recalculateScroll() {
|
|
330
357
|
if (scrollContainer.value) {
|
|
331
|
-
scrollContainer.value.
|
|
358
|
+
const spacerHeight = parseFloat(getComputedStyle(scrollContainer.value.container.scrollEl).paddingBottom);
|
|
359
|
+
scrollContainer.value.handleScroll(false, spacerHeight + THRESHOLD_TO_SHOW_BUTTON);
|
|
332
360
|
const isScrolledUp = scrollContainer.value.isUserScrolledUp();
|
|
333
361
|
showScrollToBottomButton.value = !!isScrolledUp;
|
|
334
362
|
}
|