@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.
Files changed (34) hide show
  1. package/README.md +4 -0
  2. package/dist/application/runTurnUseCase.d.ts +2 -1
  3. package/dist/application/runTurnUseCase.js +3 -3
  4. package/dist/custom/ChatFooter.vue +91 -104
  5. package/dist/custom/ChatHeader.vue +63 -62
  6. package/dist/custom/ChatSurface.vue +11 -18
  7. package/dist/custom/EditActionButtons.vue +12 -8
  8. package/dist/custom/SessionsHistory.vue +90 -28
  9. package/dist/custom/SteerQueue.vue +15 -9
  10. package/dist/custom/composables/agentStore/constants.ts +1 -1
  11. package/dist/custom/composables/agentStore/useAgentChat.ts +4 -3
  12. package/dist/custom/composables/agentStore/useAgentPlaceholder.ts +1 -0
  13. package/dist/custom/composables/agentStore/useAgentSessions.ts +23 -2
  14. package/dist/custom/composables/useAgentStore.ts +8 -29
  15. package/dist/custom/conversation_area/ConversationArea.vue +48 -16
  16. package/dist/custom/conversation_area/MessageRenderer.vue +18 -13
  17. package/dist/custom/conversation_area/ProcessingTimeline.vue +83 -123
  18. package/dist/custom/conversation_area/ReasoningRenderer.vue +13 -16
  19. package/dist/custom/conversation_area/SystemMessageRenderer.vue +4 -4
  20. package/dist/custom/conversation_area/TextRenderer.vue +67 -28
  21. package/dist/custom/conversation_area/ToolApprovalRenderer.vue +89 -56
  22. package/dist/custom/conversation_area/ToolRenderer.vue +44 -82
  23. package/dist/custom/incremark_code_renderers/IncremarkShikiCodeBlock.vue +22 -61
  24. package/dist/custom/speech_recognition_frontend/AudioLines.vue +1 -2
  25. package/dist/custom/speech_recognition_frontend/MicrophoneButon.vue +36 -32
  26. package/dist/custom/types.ts +16 -8
  27. package/dist/domain/agentEvents.d.ts +3 -2
  28. package/dist/tools/approvalFormatter.d.ts +19 -5
  29. package/dist/tools/approvalFormatter.js +88 -40
  30. package/dist/transport/http/context.d.ts +2 -1
  31. package/dist/transport/http/sessionEndpoints.js +34 -0
  32. package/package.json +3 -3
  33. package/dist/custom/SteerDebugPanel.vue +0 -45
  34. package/dist/custom/conversation_area/ToolsGroup.vue +0 -59
@@ -1,37 +1,73 @@
1
1
  <template>
2
- <div class="absolute top-0 left-0 transition-transform
3
- duration-200 ease-in-out border-y border-r dark:border-gray-600 z-20
4
- bg-lightNavbar dark:bg-darkNavbar w-96 h-full flex flex-col items-center
5
- overflow-y-auto overflow-x-hidden
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 :class="h3Style">{{ $t('Chat history') }}</h3>
9
- <div class="w-full flex items-center justify-center">
10
- </div>
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
- <button
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 justify-between w-full px-4 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors duration-200 ease-in-out text-gray-800 dark:text-gray-200"
21
- :class="{
22
- 'bg-lightPrimary/20 hover:bg-lightPrimary/20 dark:bg-darkPrimary/20 dark:hover:bg-darkPrimary/20': agentStore.activeSessionId === session.sessionId,
23
- }"
24
- @click="agentStore.setActiveSession(session.sessionId); agentStore.setSessionHistoryOpen(false);"
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
- <p class="truncate">{{ session.title || session.sessionId }}</p>
27
- <div @click.stop="agentStore.deleteSession(session.sessionId)" class="w-7 h-7 p-1 hover:scale-110 hover:bg-gray-200 dark:hover:bg-gray-500 flex items-center justify-center rounded">
28
- <IconPlusOutline class="rotate-45 w-6 h-6"/>
29
- </div>
30
- </button>
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="w-full h-full flex items-center justify-center text-gray-800 dark:text-gray-200"
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, Spinner } from '@/afcl'
44
- import { computed } from 'vue';
45
- import { IconPlusOutline } from '@iconify-prerendered/vue-flowbite';
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-gray-200 bg-gray-50 px-3 py-1.5 shadow-sm dark:border-gray-600 dark:bg-gray-800"
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-gray-400 dark:text-gray-500">
26
+ <span class="shrink-0 text-xs text-lightInputPlaceholderText dark:text-darkInputPlaceholderText">
27
27
  {{ $t('queued') }}
28
28
  </span>
29
- <button
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
- </button>
40
- <button
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
- &times;
47
- </button>
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 = 'Type a message...';
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, interrupt: unknown, approvals?: unknown) => void;
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.interrupt, dataPart.data.approvals);
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.interrupt, dataPart.data.approvals);
169
+ onToolApprovalRequest(dataPart.data.sessionId, dataPart.data.approvals);
169
170
  return;
170
171
  }
171
172
 
@@ -135,6 +135,7 @@ export function createAgentPlaceholderController({
135
135
 
136
136
  return {
137
137
  userMessagePlaceholder,
138
+ placeholderMessages,
138
139
  hasTypedMessageInPageSession,
139
140
  fetchPlaceholderMessages,
140
141
  stopPlaceholderAnimation,
@@ -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, messages: string[]) => void;
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: addToolApprovalMessage,
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 getToolApprovalMessages(interrupt: unknown): string[] {
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
- messages,
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 bg-black/10 backdrop-blur-md z-10 h-full w-full"
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 mt-2 pb-2"
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 text-gray-400 tracking-widest text-xl font-medium"
48
+ class="absolute inset-0 flex flex-col items-center justify-center px-7 pb-10 text-center"
49
49
  >
50
- <p>{{ $t('Start the conversation') }}</p>
51
- <p class="tracking-normal text-base text">{{ $t('Give any input to begin') }}</p>
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-lightPrimary/20 dark:bg-darkPrimary/10
67
- border border-lightPrimary dark:border-darkPrimary
68
- text-lightPrimary dark:text-darkPrimary
69
- backdrop-blur-sm rounded-full
70
- w-10 h-10 flex items-center justify-center gap-0.5"
71
- :class="{ 'w-12 h-8': showAnimationInScrollToBottomButton }"
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
- <IconArrowDownOutline
97
+ <IconArrowDown
74
98
  v-if="!showAnimationInScrollToBottomButton"
75
- class="p-2 w-10 h-10 transition-opacity duration-100 ease-in "
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 { IconArrowDownOutline } from '@iconify-prerendered/vue-flowbite';
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 -bottom-6 p-1 mt-0.5 z-20"
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
- <button
26
+ <Button
27
27
  v-if="canCopy"
28
- type="button"
29
- class="flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-lightPrimary dark:hover:text-darkPrimary opacity-60 group-hover/msg:opacity-100 transition-opacity duration-150 hover:scale-110"
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
- <IconFileCopySolid class="w-5 h-5" />
34
- </button>
35
- <button
35
+ <IconCopy />
36
+ </Button>
37
+ <Button
36
38
  v-if="canEdit"
37
- type="button"
38
- class="flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-lightPrimary dark:hover:text-darkPrimary opacity-60 group-hover/msg:opacity-100 transition-opacity duration-150 hover:scale-110"
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
- <IconPenSolid class="w-5 h-5" />
43
- </button>
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-gray-400 dark:text-gray-500"
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 { IconPenSolid, IconFileCopySolid } from '@iconify-prerendered/vue-flowbite';
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';