@adminforth/agent 2.9.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/application/runTurnUseCase.d.ts +2 -1
- package/dist/application/runTurnUseCase.js +3 -3
- package/dist/custom/ChatFooter.vue +91 -104
- package/dist/custom/ChatHeader.vue +63 -62
- package/dist/custom/ChatSurface.vue +11 -18
- package/dist/custom/EditActionButtons.vue +12 -8
- package/dist/custom/SessionsHistory.vue +90 -28
- package/dist/custom/SteerQueue.vue +15 -9
- 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 +23 -2
- package/dist/custom/composables/useAgentStore.ts +8 -29
- package/dist/custom/conversation_area/ConversationArea.vue +48 -16
- package/dist/custom/conversation_area/MessageRenderer.vue +18 -13
- package/dist/custom/conversation_area/ProcessingTimeline.vue +83 -123
- package/dist/custom/conversation_area/ReasoningRenderer.vue +13 -16
- package/dist/custom/conversation_area/SystemMessageRenderer.vue +4 -4
- package/dist/custom/conversation_area/TextRenderer.vue +67 -28
- package/dist/custom/conversation_area/ToolApprovalRenderer.vue +89 -56
- package/dist/custom/conversation_area/ToolRenderer.vue +44 -82
- package/dist/custom/incremark_code_renderers/IncremarkShikiCodeBlock.vue +22 -61
- package/dist/custom/speech_recognition_frontend/AudioLines.vue +1 -2
- package/dist/custom/speech_recognition_frontend/MicrophoneButon.vue +36 -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/dist/transport/http/sessionEndpoints.js +34 -0
- package/package.json +3 -3
- package/dist/custom/SteerDebugPanel.vue +0 -45
- package/dist/custom/conversation_area/ToolsGroup.vue +0 -59
|
@@ -1,37 +1,73 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
"
|
|
2
|
+
<div
|
|
3
|
+
class="af-agent-history absolute top-0 left-0 z-20 flex h-full w-96 max-w-full flex-col overflow-y-auto overflow-x-hidden
|
|
4
|
+
border-r border-lightNavbarBorder bg-lightNavbar transition-transform duration-200 ease-in-out
|
|
5
|
+
dark:border-darkNavbarBorder dark:bg-darkNavbar"
|
|
7
6
|
>
|
|
8
|
-
<h3
|
|
9
|
-
<div
|
|
10
|
-
|
|
11
|
-
<div class="w-full border-b border-gray-200 dark:border-gray-700"/>
|
|
12
|
-
<div v-for="group in groupedSessions" :key="group.dayKey" class="w-full py-2">
|
|
13
|
-
<div class="px-4 pb-2 text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">
|
|
7
|
+
<h3 class="px-4 pt-4 pb-2 text-sm font-semibold text-lightNavbarText dark:text-darkNavbarText">{{ $t('Chat history') }}</h3>
|
|
8
|
+
<div v-for="group in groupedSessions" :key="group.dayKey" class="px-2 py-2">
|
|
9
|
+
<div class="px-2.5 pb-1 text-xs font-medium text-lightInputIconColor dark:text-darkInputIconColor">
|
|
14
10
|
{{ group.label }}
|
|
15
11
|
</div>
|
|
16
|
-
|
|
17
|
-
<
|
|
12
|
+
<!-- enter and esc are handled on the wrapper so they fire once: Input spreads attrs on both its root and the input -->
|
|
13
|
+
<div
|
|
18
14
|
v-for="session in group.sessions"
|
|
19
15
|
:key="session.sessionId"
|
|
20
|
-
class=" flex items-center
|
|
21
|
-
:class="
|
|
22
|
-
'
|
|
23
|
-
|
|
24
|
-
@
|
|
16
|
+
class="af-agent-history-row group flex items-center gap-0.5 rounded-default text-sm transition-colors"
|
|
17
|
+
:class="agentStore.activeSessionId === session.sessionId
|
|
18
|
+
? 'font-medium bg-lightVerticalTabsBackgroundActive text-lightVerticalTabsTextActive dark:bg-darkVerticalTabsBackgroundActive dark:text-darkVerticalTabsTextActive'
|
|
19
|
+
: 'text-lightVerticalTabsText hover:bg-lightVerticalTabsBackgroundHover hover:text-lightVerticalTabsTextHover dark:text-darkVerticalTabsText dark:hover:bg-darkVerticalTabsBackgroundHover dark:hover:text-darkVerticalTabsTextHover'"
|
|
20
|
+
@keydown.enter.prevent="renamingSessionId === session.sessionId && commitRename(session)"
|
|
21
|
+
@keydown.esc.prevent="renamingSessionId = null"
|
|
25
22
|
>
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
<Input
|
|
24
|
+
v-if="renamingSessionId === session.sessionId"
|
|
25
|
+
ref="renameInput"
|
|
26
|
+
v-model="renameTitle"
|
|
27
|
+
type="text"
|
|
28
|
+
class="min-w-0 flex-1"
|
|
29
|
+
fullWidth
|
|
30
|
+
maxlength="100"
|
|
31
|
+
:aria-label="$t('Chat name')"
|
|
32
|
+
@blur="commitRename(session)"
|
|
33
|
+
/>
|
|
34
|
+
<template v-else>
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
class="min-w-0 flex-1 truncate rounded-default px-2.5 py-1.5 text-left focus:outline-none focus-visible:ring-[3px] focus-visible:ring-lightFocusRing dark:focus-visible:ring-darkFocusRing"
|
|
38
|
+
@click="agentStore.setActiveSession(session.sessionId); agentStore.setSessionHistoryOpen(false);"
|
|
39
|
+
>
|
|
40
|
+
{{ session.title || session.sessionId }}
|
|
41
|
+
</button>
|
|
42
|
+
<Button
|
|
43
|
+
v-if="session.sessionId !== PRE_SESSION_ID"
|
|
44
|
+
variant="ghost"
|
|
45
|
+
size="sm"
|
|
46
|
+
iconOnly
|
|
47
|
+
:class="ROW_ACTION_CLASSES"
|
|
48
|
+
:aria-label="$t('Rename chat')"
|
|
49
|
+
:title="$t('Rename chat')"
|
|
50
|
+
@click="startRename(session)"
|
|
51
|
+
>
|
|
52
|
+
<IconPencil />
|
|
53
|
+
</Button>
|
|
54
|
+
<Button
|
|
55
|
+
variant="ghost"
|
|
56
|
+
size="sm"
|
|
57
|
+
iconOnly
|
|
58
|
+
:class="ROW_ACTION_CLASSES"
|
|
59
|
+
:aria-label="$t('Delete chat')"
|
|
60
|
+
:title="$t('Delete chat')"
|
|
61
|
+
@click="agentStore.deleteSession(session.sessionId)"
|
|
62
|
+
>
|
|
63
|
+
<IconTrash2 />
|
|
64
|
+
</Button>
|
|
65
|
+
</template>
|
|
66
|
+
</div>
|
|
31
67
|
</div>
|
|
32
68
|
<p
|
|
33
69
|
v-if="!groupedSessions || groupedSessions.length === 0"
|
|
34
|
-
class="
|
|
70
|
+
class="flex flex-1 items-center justify-center px-4 text-sm text-lightInputIconColor dark:text-darkInputIconColor"
|
|
35
71
|
>
|
|
36
72
|
{{ $t('There are no previous chat sessions') }}
|
|
37
73
|
</p>
|
|
@@ -40,15 +76,41 @@
|
|
|
40
76
|
|
|
41
77
|
|
|
42
78
|
<script setup lang="ts">
|
|
43
|
-
import { Button,
|
|
44
|
-
import { computed } from 'vue';
|
|
45
|
-
import {
|
|
79
|
+
import { Button, Input } from '@/afcl'
|
|
80
|
+
import { computed, nextTick, ref, useTemplateRef } from 'vue';
|
|
81
|
+
import { IconPencil, IconTrash2 } from '@iconify-prerendered/vue-lucide';
|
|
46
82
|
import type { ISessionsListItem } from './types';
|
|
47
83
|
import { useAgentStore } from './composables/useAgentStore';
|
|
84
|
+
import { PRE_SESSION_ID } from './composables/agentStore/constants';
|
|
85
|
+
|
|
86
|
+
// row actions show on hover or keyboard focus of the row, and always on touch screens which have no hover
|
|
87
|
+
const ROW_ACTION_CLASSES = 'hidden group-hover:flex group-focus-within:flex [@media(hover:none)]:flex';
|
|
48
88
|
|
|
49
89
|
const agentStore = useAgentStore();
|
|
90
|
+
const renamingSessionId = ref<string | null>(null);
|
|
91
|
+
const renameTitle = ref('');
|
|
92
|
+
// the input is rendered inside v-for, so the template ref is an array with the one open input
|
|
93
|
+
const renameInputs = useTemplateRef<Array<{ focus: () => void }>>('renameInput');
|
|
94
|
+
|
|
95
|
+
async function startRename(session: ISessionsListItem) {
|
|
96
|
+
renamingSessionId.value = session.sessionId;
|
|
97
|
+
renameTitle.value = session.title;
|
|
98
|
+
await nextTick();
|
|
99
|
+
renameInputs.value![0].focus();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// called by both enter and the blur that follows when the input goes away, only the first one saves
|
|
103
|
+
async function commitRename(session: ISessionsListItem) {
|
|
104
|
+
if (renamingSessionId.value !== session.sessionId) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
renamingSessionId.value = null;
|
|
108
|
+
const title = renameTitle.value.trim();
|
|
109
|
+
if (title && title !== session.title) {
|
|
110
|
+
await agentStore.renameSession(session.sessionId, title);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
50
113
|
|
|
51
|
-
const h3Style = "text-gray-800 dark:text-gray-200 font-medium text-xl tracking-widest my-2"
|
|
52
114
|
const dayLabelFormatter = new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric' });
|
|
53
115
|
const dayLabelWithYearFormatter = new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
|
|
54
116
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<div
|
|
16
16
|
v-for="item in agentStore.steerQueue"
|
|
17
17
|
:key="item.id"
|
|
18
|
-
class="flex items-center gap-2 rounded-lg border border-
|
|
18
|
+
class="flex items-center gap-2 rounded-lg border border-lightCardBorder bg-lightCardBackground py-1.5 pl-3 pr-1.5 shadow-popover dark:border-darkCardBorder dark:bg-darkCardBackground dark:shadow-darkPopover"
|
|
19
19
|
>
|
|
20
20
|
<span
|
|
21
21
|
class="flex-1 min-w-0 truncate text-sm text-lightInputText dark:text-darkInputText"
|
|
@@ -23,12 +23,13 @@
|
|
|
23
23
|
>
|
|
24
24
|
{{ item.text }}
|
|
25
25
|
</span>
|
|
26
|
-
<span class="shrink-0 text-xs text-
|
|
26
|
+
<span class="shrink-0 text-xs text-lightInputPlaceholderText dark:text-darkInputPlaceholderText">
|
|
27
27
|
{{ $t('queued') }}
|
|
28
28
|
</span>
|
|
29
|
-
<
|
|
29
|
+
<Button
|
|
30
|
+
variant="secondary"
|
|
31
|
+
size="sm"
|
|
30
32
|
type="button"
|
|
31
|
-
class="shrink-0 rounded-md px-2 py-1 text-xs font-medium text-lightPrimary transition-colors hover:bg-lightPrimary/10 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent dark:text-darkPrimary dark:hover:bg-darkPrimary/10 dark:disabled:hover:bg-transparent"
|
|
32
33
|
:disabled="agentStore.isFinalResponseStreaming"
|
|
33
34
|
:title="agentStore.isFinalResponseStreaming
|
|
34
35
|
? $t('The final response is streaming; this message will be sent after it finishes')
|
|
@@ -36,21 +37,26 @@
|
|
|
36
37
|
@click="agentStore.steerQueuedMessage(item.id)"
|
|
37
38
|
>
|
|
38
39
|
{{ $t('Steer') }}
|
|
39
|
-
</
|
|
40
|
-
<
|
|
40
|
+
</Button>
|
|
41
|
+
<Button
|
|
42
|
+
variant="ghost"
|
|
43
|
+
size="sm"
|
|
44
|
+
iconOnly
|
|
41
45
|
type="button"
|
|
42
|
-
class="shrink-0 flex h-6 w-6 items-center justify-center rounded-md text-lg leading-none text-gray-400 transition-colors hover:bg-gray-200 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300"
|
|
43
46
|
:title="$t('Remove from queue')"
|
|
47
|
+
:aria-label="$t('Remove from queue')"
|
|
44
48
|
@click="agentStore.removeQueuedMessage(item.id)"
|
|
45
49
|
>
|
|
46
|
-
|
|
47
|
-
</
|
|
50
|
+
<IconX />
|
|
51
|
+
</Button>
|
|
48
52
|
</div>
|
|
49
53
|
</TransitionGroup>
|
|
50
54
|
</div>
|
|
51
55
|
</template>
|
|
52
56
|
|
|
53
57
|
<script setup lang="ts">
|
|
58
|
+
import { IconX } from '@iconify-prerendered/vue-lucide';
|
|
59
|
+
import { Button } from '@/afcl';
|
|
54
60
|
import { useAgentStore } from './composables/useAgentStore';
|
|
55
61
|
|
|
56
62
|
const agentStore = useAgentStore();
|
|
@@ -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({
|
|
@@ -203,6 +203,26 @@ export function createAgentSessionManager({
|
|
|
203
203
|
setCurrentChat(PRE_SESSION_ID);
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
async function renameSession(sessionId: string, title: string) {
|
|
207
|
+
const res = await callAdminForthApi({
|
|
208
|
+
method: 'POST',
|
|
209
|
+
path: '/agent/rename-session',
|
|
210
|
+
body: { sessionId, title },
|
|
211
|
+
});
|
|
212
|
+
if (res.error) {
|
|
213
|
+
console.error('Error renaming session:', res.error);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
sessionList.value.find((s: ISessionsListItem) => s.sessionId === sessionId)!.title = res.title;
|
|
217
|
+
// a loaded session is cached apart from the list, and the open one is shown in the header
|
|
218
|
+
if (sessions.value[sessionId]) {
|
|
219
|
+
sessions.value[sessionId].title = res.title;
|
|
220
|
+
}
|
|
221
|
+
if (currentSession.value?.sessionId === sessionId) {
|
|
222
|
+
currentSession.value.title = res.title;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
206
226
|
async function deleteSession(sessionId: string) {
|
|
207
227
|
if (sessionId === PRE_SESSION_ID) {
|
|
208
228
|
deletePreSession();
|
|
@@ -370,6 +390,7 @@ export function createAgentSessionManager({
|
|
|
370
390
|
createPreSession,
|
|
371
391
|
setActiveSession,
|
|
372
392
|
fetchSessionsList,
|
|
393
|
+
renameSession,
|
|
373
394
|
deleteSession,
|
|
374
395
|
addDebugMessage,
|
|
375
396
|
addSystemMessage,
|
|
@@ -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) => {
|
|
@@ -222,6 +198,7 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
222
198
|
createPreSession,
|
|
223
199
|
setActiveSession,
|
|
224
200
|
fetchSessionsList,
|
|
201
|
+
renameSession,
|
|
225
202
|
deleteSession,
|
|
226
203
|
addDebugMessage,
|
|
227
204
|
addSystemMessage,
|
|
@@ -614,6 +591,7 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
614
591
|
sessionList,
|
|
615
592
|
setActiveSession,
|
|
616
593
|
fetchSessionsList,
|
|
594
|
+
renameSession,
|
|
617
595
|
deleteSession,
|
|
618
596
|
createPreSession,
|
|
619
597
|
//____________________________________________
|
|
@@ -628,6 +606,7 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
628
606
|
submitToolApproval,
|
|
629
607
|
userMessageInput,
|
|
630
608
|
userMessagePlaceholder,
|
|
609
|
+
placeholderMessages,
|
|
631
610
|
chatMessages: computed(() => currentChat.value?.messages || []),
|
|
632
611
|
trimmedUserMessage,
|
|
633
612
|
isResponseInProgress,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<div
|
|
6
6
|
v-if="agentStore.isSessionHistoryOpen"
|
|
7
7
|
@click="agentStore.setSessionHistoryOpen(false)"
|
|
8
|
-
class="absolute
|
|
8
|
+
class="absolute z-10 h-full w-full bg-lightScrim dark:bg-darkScrim"
|
|
9
9
|
>
|
|
10
10
|
</div>
|
|
11
11
|
<div class="relative flex-1 min-h-0 overflow-hidden [container-type:size]" @click="recalculateScroll()">
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
<div
|
|
38
38
|
v-for="(message, index) in props.messages" :key="index"
|
|
39
|
-
class="flex flex-col w-full
|
|
39
|
+
class="flex flex-col w-full pt-5 first:pt-4"
|
|
40
40
|
:class="message.role === 'user' ? 'self-end' : 'self-start'"
|
|
41
41
|
ref="messagesRefs"
|
|
42
42
|
>
|
|
@@ -45,10 +45,34 @@
|
|
|
45
45
|
<!-- absolute, so it doesn't resize the scroll content when the container resizes (see the spacer comment) -->
|
|
46
46
|
<div
|
|
47
47
|
v-if="props.messages.length === 0"
|
|
48
|
-
class="absolute inset-0 flex flex-col items-center justify-center
|
|
48
|
+
class="absolute inset-0 flex flex-col items-center justify-center px-7 pb-10 text-center"
|
|
49
49
|
>
|
|
50
|
-
<
|
|
51
|
-
|
|
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-lg font-semibold tracking-tight 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-lightInputIconColor dark:text-darkInputIconColor">
|
|
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-lightInputIconColor dark:text-darkInputIconColor">{{ $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-lg border text-sm text-left transition-colors duration-150
|
|
66
|
+
border-lightCardBorder bg-lightCardBackground text-lightNavbarText hover:bg-lightCardBackgroundHover
|
|
67
|
+
focus:outline-none focus-visible:ring-[3px] focus-visible:ring-lightFocusRing
|
|
68
|
+
dark:border-darkCardBorder dark:bg-darkCardBackground dark:text-darkNavbarText dark:hover:bg-darkCardBackgroundHover
|
|
69
|
+
dark:focus-visible:ring-darkFocusRing"
|
|
70
|
+
@click="applySuggestion(suggestion)"
|
|
71
|
+
>
|
|
72
|
+
<span class="flex-1 min-w-0">{{ suggestion }}</span>
|
|
73
|
+
<IconArrowRight class="w-4 h-4 shrink-0 text-lightInputIconColor transition-transform duration-150 group-hover:translate-x-0.5 dark:text-darkInputIconColor" />
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
52
76
|
</div>
|
|
53
77
|
</CustomAutoScrollContainer>
|
|
54
78
|
<Transition
|
|
@@ -62,18 +86,17 @@
|
|
|
62
86
|
<button
|
|
63
87
|
v-if="showScrollToBottomButton"
|
|
64
88
|
@click="scrollContainer.scrollToBottom();"
|
|
65
|
-
class="absolute z-10 bottom-8 left-1/2 -translate-x-1/2
|
|
66
|
-
bg-
|
|
67
|
-
|
|
68
|
-
text-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
:
|
|
89
|
+
class="absolute z-10 bottom-8 left-1/2 -translate-x-1/2 flex items-center justify-center gap-0.5 w-[2.125rem] h-[2.125rem] rounded-full border
|
|
90
|
+
border-lightInputBorder bg-lightInputBackground text-lightInputIconColor shadow-popover hover:text-lightInputText
|
|
91
|
+
focus:outline-none focus-visible:ring-[3px] focus-visible:ring-lightFocusRing
|
|
92
|
+
dark:border-darkInputBorder dark:bg-darkInputBackground dark:text-darkInputIconColor dark:shadow-darkPopover dark:hover:text-darkInputText
|
|
93
|
+
dark:focus-visible:ring-darkFocusRing"
|
|
94
|
+
:class="{ 'w-12': showAnimationInScrollToBottomButton }"
|
|
95
|
+
:aria-label="$t('Scroll to the latest message')"
|
|
72
96
|
>
|
|
73
|
-
<
|
|
97
|
+
<IconArrowDown
|
|
74
98
|
v-if="!showAnimationInScrollToBottomButton"
|
|
75
|
-
class="
|
|
76
|
-
:disabled="!showScrollToBottomButton"
|
|
99
|
+
class="w-4 h-4"
|
|
77
100
|
/>
|
|
78
101
|
<ThreeDotsAnimation
|
|
79
102
|
v-else
|
|
@@ -88,7 +111,7 @@
|
|
|
88
111
|
<script setup lang="ts">
|
|
89
112
|
import type { IMessage } from '../types';
|
|
90
113
|
import { useTemplateRef, ref, onMounted, onUnmounted, watch, nextTick, defineAsyncComponent, computed } from 'vue';
|
|
91
|
-
import {
|
|
114
|
+
import { IconArrowDown, IconArrowRight, IconSparkles } from '@iconify-prerendered/vue-lucide';
|
|
92
115
|
import SessionsHistory from '../SessionsHistory.vue';
|
|
93
116
|
import { useAgentStore } from '../composables/useAgentStore';
|
|
94
117
|
import { useAgentTransitions } from '../composables/useAgentTransitions';
|
|
@@ -132,6 +155,7 @@ const lastTurnHeight = ref(0);
|
|
|
132
155
|
const MASK_HEIGHT = 20;
|
|
133
156
|
const EMPTY_MESSAGE_HEIGHT = 18;
|
|
134
157
|
const THRESHOLD_TO_SHOW_BUTTON = 10;
|
|
158
|
+
const MAX_SUGGESTIONS = 3;
|
|
135
159
|
let messageResizeObserver: ResizeObserver | null = null;
|
|
136
160
|
let observedLastUserMessageElement: HTMLElement | null = null;
|
|
137
161
|
let observedLastAgentMessageElement: HTMLElement | null = null;
|
|
@@ -143,6 +167,14 @@ const showAnimationInScrollToBottomButton = computed(() => {
|
|
|
143
167
|
return agentStore.isResponseInProgress && showScrollToBottomButton.value;
|
|
144
168
|
});
|
|
145
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
|
+
|
|
146
178
|
onMounted(async () => {
|
|
147
179
|
messageResizeObserver = new ResizeObserver(() => {
|
|
148
180
|
scheduleSpacerHeightUpdate();
|
|
@@ -18,34 +18,38 @@
|
|
|
18
18
|
>
|
|
19
19
|
<div
|
|
20
20
|
v-if="showActionsBar && showEditButton"
|
|
21
|
-
class="absolute flex -
|
|
21
|
+
class="absolute flex top-full -mt-1 p-1 z-20"
|
|
22
22
|
:class="isUserMessage ? 'right-0 self-end mr-4' : 'left-0 self-start ml-4'"
|
|
23
23
|
@mouseenter="showEditButtonOnHover"
|
|
24
24
|
@mouseleave="hideEditButtonOnHover"
|
|
25
25
|
>
|
|
26
|
-
<
|
|
26
|
+
<Button
|
|
27
27
|
v-if="canCopy"
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
variant="ghost"
|
|
29
|
+
size="sm"
|
|
30
|
+
iconOnly
|
|
30
31
|
:title="$t('Copy message')"
|
|
32
|
+
:aria-label="$t('Copy message')"
|
|
31
33
|
@click="copyMessageToClipboard"
|
|
32
34
|
>
|
|
33
|
-
<
|
|
34
|
-
</
|
|
35
|
-
<
|
|
35
|
+
<IconCopy />
|
|
36
|
+
</Button>
|
|
37
|
+
<Button
|
|
36
38
|
v-if="canEdit"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
variant="ghost"
|
|
40
|
+
size="sm"
|
|
41
|
+
iconOnly
|
|
39
42
|
:title="$t('Edit message')"
|
|
43
|
+
:aria-label="$t('Edit message')"
|
|
40
44
|
@click="agentStore.startEditMessage(message)"
|
|
41
45
|
>
|
|
42
|
-
<
|
|
43
|
-
</
|
|
46
|
+
<IconPencil />
|
|
47
|
+
</Button>
|
|
44
48
|
</div>
|
|
45
49
|
</Transition>
|
|
46
50
|
<div
|
|
47
51
|
v-if="message.role === 'user' && message.metadata?.steer && !isBeingEditedTurn"
|
|
48
|
-
class="self-end mr-4 mb-0.5 text-xs italic text-
|
|
52
|
+
class="self-end mr-4 mb-0.5 text-xs italic text-lightInputPlaceholderText dark:text-darkInputPlaceholderText"
|
|
49
53
|
>
|
|
50
54
|
{{ $t('Steering instruction') }}
|
|
51
55
|
</div>
|
|
@@ -83,7 +87,8 @@
|
|
|
83
87
|
|
|
84
88
|
<script setup lang="ts">
|
|
85
89
|
import { computed, ref } from 'vue';
|
|
86
|
-
import {
|
|
90
|
+
import { IconCopy, IconPencil } from '@iconify-prerendered/vue-lucide';
|
|
91
|
+
import { Button } from '@/afcl';
|
|
87
92
|
import TextRenderer from './TextRenderer.vue';
|
|
88
93
|
import type { IMessage } from '../types';
|
|
89
94
|
import { getMessageParts } from '../utils';
|