@adminforth/agent 2.10.0 → 3.0.1

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.
@@ -8,11 +8,12 @@
8
8
  }"
9
9
  >
10
10
  <SteerQueue />
11
- <!-- <SteerDebugPanel /> -->
12
11
  <div
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"
12
+ class="af-agent-composer w-full flex flex-col rounded-xl border transition-colors duration-150
13
+ border-lightInputBorder bg-lightInputBackground hover:border-lightInputBorderHover
14
+ focus-within:border-lightInputFocusBorder focus-within:ring-[3px] focus-within:ring-lightInputFocusRing
15
+ dark:border-darkInputBorder dark:bg-darkInputBackground dark:hover:border-darkInputBorderHover
16
+ dark:focus-within:border-darkInputFocusBorder dark:focus-within:ring-darkInputFocusRing"
16
17
  >
17
18
  <textarea
18
19
  v-if="!agentStore.isAudioChatMode"
@@ -21,9 +22,9 @@
21
22
  rows="1"
22
23
  @input="autoResize"
23
24
  :class="[
24
- `block w-full px-3.5 pt-3 pb-1 resize-none overflow-hidden bg-transparent border-none text-sm leading-6
25
+ `block w-full px-3 pt-2.5 pb-1 resize-none overflow-hidden bg-transparent border-none text-sm leading-[22px]
25
26
  focus:outline-none focus:ring-0
26
- text-lightInputText placeholder:text-gray-500 dark:text-darkInputText dark:placeholder:text-gray-400`,
27
+ text-lightInputText placeholder:text-lightInputPlaceholderText dark:text-darkInputText dark:placeholder:text-darkInputPlaceholderText`,
27
28
  { '!text-base': coreStore.isIos }
28
29
  ]"
29
30
  :placeholder="placeholderText"
@@ -35,57 +36,38 @@
35
36
  class="items-center gap-1 px-2 pb-2"
36
37
  :class="agentStore.isAudioChatMode ? 'grid grid-cols-[1fr_auto_1fr] pt-2' : 'flex pt-1'"
37
38
  >
38
- <div
39
+ <DropdownMenu
39
40
  v-if="agentStore.availableModes.length > 1"
40
- ref="modeMenu"
41
- class="relative justify-self-start"
41
+ v-model:open="isModeMenuOpen"
42
+ class="af-agent-mode-menu justify-self-start"
43
+ teleportToBody
42
44
  >
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>
60
- <Transition
61
- enter-active-class="transition ease-out duration-200"
62
- enter-from-class="opacity-0 scale-95"
63
- enter-to-class="opacity-100 scale-100"
64
- leave-active-class="transition ease-in duration-150"
65
- leave-from-class="opacity-100 scale-100"
66
- leave-to-class="opacity-0 scale-95"
67
- >
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"
45
+ <template #trigger="{ open }">
46
+ <Button
47
+ variant="ghost"
48
+ size="sm"
49
+ type="button"
50
+ :class="open && 'bg-lightSecondaryDarken text-lightInputText dark:bg-darkSecondaryLighten dark:text-darkInputText'"
51
+ :aria-label="$t('Select mode')"
52
+ :aria-expanded="open"
53
+ :title="$t('Select mode')"
71
54
  >
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>
55
+ <IconSparkles class="w-4 h-4" />
56
+ {{ agentStore.activeModeName }}
57
+ <IconChevronDown class="w-3.5 h-3.5 transition-transform duration-200" :class="open && 'rotate-180'" />
58
+ </Button>
59
+ </template>
60
+ <DropdownMenuItem
61
+ v-for="mode in agentStore.availableModes"
62
+ :key="mode.name"
63
+ @click="agentStore.setActiveMode(mode.name)"
64
+ >
65
+ {{ mode.name }}
66
+ <template v-if="mode.name === agentStore.activeModeName" #suffix>
67
+ <IconCheck class="w-4 h-4 text-lightPrimary dark:text-darkPrimary" />
68
+ </template>
69
+ </DropdownMenuItem>
70
+ </DropdownMenu>
89
71
  <div
90
72
  class="ml-auto flex items-center gap-1"
91
73
  :class="agentStore.isAudioChatMode ? 'col-start-2' : ''"
@@ -100,27 +82,29 @@
100
82
  @cancel="cancelEdit"
101
83
  @confirm="sendMessage"
102
84
  />
103
- <button
85
+ <Button
104
86
  v-else-if="!agentStore.isResponseInProgress"
105
87
  type="button"
106
- :class="ROUND_PRIMARY_BUTTON_CLASSES"
88
+ iconOnly
89
+ class="rounded-full"
107
90
  :disabled="!agentStore.trimmedUserMessage"
108
91
  :title="$t('Send message')"
109
92
  :aria-label="$t('Send message')"
110
93
  @click="sendMessage"
111
94
  >
112
- <IconArrowUp class="w-4 h-4" />
113
- </button>
114
- <button
95
+ <IconArrowUp />
96
+ </Button>
97
+ <Button
115
98
  v-else
116
99
  type="button"
117
- :class="ROUND_PRIMARY_BUTTON_CLASSES"
100
+ iconOnly
101
+ class="rounded-full"
118
102
  :title="$t('Stop')"
119
103
  :aria-label="$t('Stop')"
120
104
  @click="stopCurrentRequest"
121
105
  >
122
106
  <span class="w-2.5 h-2.5 rounded-sm bg-current" />
123
- </button>
107
+ </Button>
124
108
  </template>
125
109
  </div>
126
110
  </div>
@@ -129,16 +113,15 @@
129
113
  </template>
130
114
 
131
115
  <script setup lang="ts">
132
- import { IconArrowUp, IconCheck, IconChevronDown, IconSparklesSolid } from '@iconify-prerendered/vue-heroicons';
116
+ import { IconArrowUp, IconCheck, IconChevronDown, IconSparkles } from '@iconify-prerendered/vue-lucide';
133
117
  import { useTemplateRef, onMounted, ref, watch, computed } from 'vue';
134
- import { onClickOutside } from '@vueuse/core'
118
+ import { Button, DropdownMenu, DropdownMenuItem } from '@/afcl';
135
119
  import { useAgentStore } from './composables/useAgentStore';
136
120
  import { useAgentTransitions } from './composables/useAgentTransitions';
137
121
  import { useCoreStore } from '@/stores/core';
138
122
  import { remToPx } from './utils';
139
123
  import MicrophoneButton from './speech_recognition_frontend/MicrophoneButon.vue';
140
124
  import SteerQueue from './SteerQueue.vue';
141
- import SteerDebugPanel from './SteerDebugPanel.vue';
142
125
  import EditActionButtons from './EditActionButtons.vue';
143
126
 
144
127
  const props = defineProps<{
@@ -156,14 +139,7 @@ const props = defineProps<{
156
139
  conversationAreaRef: any
157
140
  }>();
158
141
 
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
-
165
142
  const textInput = useTemplateRef('textInput');
166
- const modeMenu = useTemplateRef('modeMenu');
167
143
  const agentStore = useAgentStore();
168
144
  const agentTransitions = useAgentTransitions();
169
145
  const coreStore = useCoreStore();
@@ -176,8 +152,6 @@ const placeholderText = computed(() => {
176
152
  return agentStore.userMessagePlaceholder;
177
153
  });
178
154
 
179
- onClickOutside(modeMenu as any, () => { isModeMenuOpen.value = false; });
180
-
181
155
  onMounted(async () => {
182
156
  agentStore.setAvailableModes(props.meta.modes, props.meta.defaultModeName);
183
157
  agentStore.setCurrentGenerationModeFromLocalStorage();
@@ -210,15 +184,6 @@ function autoResize() {
210
184
  }
211
185
  }
212
186
 
213
- function toggleModeMenu() {
214
- isModeMenuOpen.value = !isModeMenuOpen.value;
215
- }
216
-
217
- function selectMode(modeName: string) {
218
- agentStore.setActiveMode(modeName);
219
- isModeMenuOpen.value = false;
220
- }
221
-
222
187
  async function sendMessage() {
223
188
  if (agentStore.isAudioChatMode) return;
224
189
  isModeMenuOpen.value = false;
@@ -1,83 +1,79 @@
1
1
  <template>
2
- <div
2
+ <!-- in full screen the controls line up with the centered column of the conversation -->
3
+ <div
3
4
  ref="headerRef"
4
- class="flex items-center justify-between h-14 border-b border-gray-200 dark:border-gray-700"
5
+ class="af-agent-header flex items-center gap-0.5 h-12 px-2 border-b border-lightNavbarBorder dark:border-darkNavbarBorder"
6
+ :style="agentStore.isFullScreen ? { paddingInline: `max(0.5rem, calc((100% - ${agentStore.MAX_WIDTH}rem) / 2 + 0.5rem))` } : undefined"
5
7
  >
6
- <div
7
- class="flex items-center shrink-0"
8
+ <Button
9
+ variant="ghost"
10
+ iconOnly
11
+ :class="agentStore.isSessionHistoryOpen && PRESSED_CLASSES"
12
+ :aria-pressed="agentStore.isSessionHistoryOpen"
13
+ :aria-label="$t('Chat history')"
14
+ :title="$t('Chat history')"
15
+ @click="agentStore.setSessionHistoryOpen(!agentStore.isSessionHistoryOpen)"
8
16
  >
9
- <IconBarsOutline
10
- class="m-2 w-8 h-8 p-1 cursor-pointer hover:scale-110 rounded transition-colors duration-200
11
- text-lightNavbarIcons hover:text-lightNavbarIcons/80 hover:bg-lightNavbarIcons/20
12
- dark:text-darkNavbarIcons hover:text-darkNavbarIcons/80 hover:bg-darkNavbarIcons/20 "
13
- :class="agentStore.isSessionHistoryOpen ?
14
- 'bg-lightNavbarIcons/20 text-lightNavbarIcons/80 dark:bg-darkNavbarIcons/20 dark:text-darkNavbarIcons/80' :
15
- ''"
16
- @click="agentStore.setSessionHistoryOpen(!agentStore.isSessionHistoryOpen)"
17
- />
18
- <IconOpenSidebarSolid
19
- v-if="!agentStore.isTeleportedToBody && !coreStore.isMobile && !agentStore.isFullScreen"
20
- class="m-2 w-8 h-8 p-1 cursor-pointer hover:scale-110 rounded transition-colors duration-200
21
- text-lightNavbarIcons hover:text-lightNavbarIcons/80 hover:bg-lightNavbarIcons/20
22
- dark:text-darkNavbarIcons hover:text-darkNavbarIcons/80 hover:bg-darkNavbarIcons/20 "
23
- @click="agentStore.setIsTeleportedToBody(true)"
24
- />
25
- <IconCloseSidebarSolid
26
- v-else-if="!coreStore.isMobile && !agentStore.isFullScreen"
27
- class="m-2 w-8 h-8 p-1 cursor-pointer hover:scale-110 rounded transition-colors duration-200
28
- text-lightNavbarIcons hover:text-lightNavbarIcons/80 bg-lightNavbarIcons/20
29
- dark:bg-darkNavbarIcons/20 dark:text-darkNavbarIcons/80 hover:text-darkNavbarIcons/80 hover:bg-darkNavbarIcons/20 "
30
- @click="agentStore.setIsTeleportedToBody(false)"
31
- />
32
- <IconArrowsPointingOut
33
- v-if="!agentStore.isFullScreen && !coreStore.isMobile"
34
- class="m-2 w-8 h-8 p-1 cursor-pointer hover:scale-110 rounded transition-colors duration-200
35
- text-lightNavbarIcons hover:text-lightNavbarIcons/80 hover:bg-lightNavbarIcons/20
36
- dark:text-darkNavbarIcons hover:text-darkNavbarIcons/80 hover:bg-darkNavbarIcons/20 "
37
- @click="agentStore.setFullScreen(true)"
38
- />
39
- <IconArrowsPointingIn
40
- v-else-if="!coreStore.isMobile"
41
- class="m-2 w-8 h-8 p-1 cursor-pointer hover:scale-110 rounded transition-colors duration-200
42
- text-lightNavbarIcons hover:text-lightNavbarIcons/80 hover:bg-lightNavbarIcons/20
43
- dark:text-darkNavbarIcons hover:text-darkNavbarIcons/80 hover:bg-darkNavbarIcons/20 "
44
- :class="agentStore.isFullScreen ?
45
- 'bg-lightNavbarIcons/20 text-lightNavbarIcons/80 dark:bg-darkNavbarIcons/20 dark:text-darkNavbarIcons/80' :
46
- ''"
47
- @click="agentStore.setFullScreen(false)"
48
- />
49
- </div>
50
- <p class="flex-1 min-w-0 truncate px-1 text-sm font-semibold text-lightNavbarText dark:text-darkNavbarText">
17
+ <IconHistory />
18
+ </Button>
19
+ <Button
20
+ v-if="!coreStore.isMobile && !agentStore.isFullScreen"
21
+ variant="ghost"
22
+ iconOnly
23
+ :class="agentStore.isTeleportedToBody && PRESSED_CLASSES"
24
+ :aria-pressed="agentStore.isTeleportedToBody"
25
+ :aria-label="$t('Dock to the side')"
26
+ :title="$t('Dock to the side')"
27
+ @click="agentStore.setIsTeleportedToBody(!agentStore.isTeleportedToBody)"
28
+ >
29
+ <IconPanelRight />
30
+ </Button>
31
+ <Button
32
+ v-if="!coreStore.isMobile"
33
+ variant="ghost"
34
+ iconOnly
35
+ :aria-label="agentStore.isFullScreen ? $t('Exit full screen') : $t('Full screen')"
36
+ :title="agentStore.isFullScreen ? $t('Exit full screen') : $t('Full screen')"
37
+ @click="agentStore.setFullScreen(!agentStore.isFullScreen)"
38
+ >
39
+ <IconMinimize2 v-if="agentStore.isFullScreen" />
40
+ <IconMaximize2 v-else />
41
+ </Button>
42
+ <p class="flex-1 min-w-0 truncate px-1 text-[13px] font-semibold text-lightNavbarText dark:text-darkNavbarText">
51
43
  {{ isNewChat ? $t('New chat') : agentStore.currentSession.title }}
52
44
  </p>
53
- <div class="flex items-center justify-center shrink-0">
54
- <Button
55
- @click="agentStore.createPreSession(); agentStore.setSessionHistoryOpen(false); agentStore.focusTextInput();"
56
- class="!py-1 !px-2 max-w-64 mr-2"
57
- >
58
- <IconPlusOutline class="w-5 h-5" />
59
- {{ $t('New chat') }}
60
- </Button>
61
- <IconCloseOutline
62
- class="m-2 w-8 h-8 p-1 cursor-pointer hover:scale-110 rounded transition-colors duration-200
63
- text-lightNavbarIcons hover:text-lightNavbarIcons/80 hover:bg-lightNavbarIcons/20
64
- dark:text-darkNavbarIcons hover:text-darkNavbarIcons/80 hover:bg-darkNavbarIcons/20 "
65
- @click="agentStore.setIsChatOpen(false)"
66
- />
67
- </div>
45
+ <Button
46
+ variant="secondary"
47
+ class="shrink-0"
48
+ @click="agentStore.createPreSession(); agentStore.setSessionHistoryOpen(false); agentStore.focusTextInput();"
49
+ >
50
+ <IconPlus class="w-4 h-4" />
51
+ {{ $t('New chat') }}
52
+ </Button>
53
+ <Button
54
+ variant="ghost"
55
+ iconOnly
56
+ :aria-label="$t('Close')"
57
+ :title="$t('Close')"
58
+ @click="agentStore.setIsChatOpen(false)"
59
+ >
60
+ <IconX />
61
+ </Button>
68
62
  </div>
69
63
  </template>
70
64
 
71
65
 
72
66
  <script setup>
73
67
  import { computed } from 'vue';
74
- import { IconArrowsPointingOut, IconArrowsPointingIn } from '@iconify-prerendered/vue-heroicons';
75
- import { IconCloseOutline, IconBarsOutline, IconCloseSidebarSolid, IconOpenSidebarSolid, IconPlusOutline } from '@iconify-prerendered/vue-flowbite';
68
+ import { IconHistory, IconMaximize2, IconMinimize2, IconPanelRight, IconPlus, IconX } from '@iconify-prerendered/vue-lucide';
76
69
  import { useAgentStore } from './composables/useAgentStore';
77
70
  import { useCoreStore } from '@/stores/core';
78
71
  import { Button } from '@/afcl';
79
72
  import { PRE_SESSION_ID } from './composables/agentStore/constants';
80
73
 
74
+ // a toggle in the on state, same fill as the hover of a ghost button
75
+ const PRESSED_CLASSES = 'bg-lightSecondaryDarken text-lightInputText dark:bg-darkSecondaryLighten dark:text-darkInputText';
76
+
81
77
  const agentStore = useAgentStore();
82
78
  const coreStore = useCoreStore();
83
79
 
@@ -1,29 +1,21 @@
1
1
  <template>
2
- <div
3
- class="relative w-6 h-6 cursor-pointer mr-1 mt-1
4
- text-lightNavbarIcons hover:text-lightNavbarIcons/80
5
- dark:text-darkNavbarIcons hover:text-darkNavbarIcons/80
6
- hover:scale-110 transition-colors duration-200"
2
+ <Button
3
+ variant="secondary"
4
+ :class="{ 'bg-lightButtonGroupActiveBackground hover:bg-lightButtonGroupActiveBackground dark:bg-darkButtonGroupActiveBackground dark:hover:bg-darkButtonGroupActiveBackground': agentStore.isChatOpen }"
5
+ :aria-expanded="agentStore.isChatOpen"
7
6
  @click="agentStore.setIsChatOpen(!agentStore.isChatOpen)"
8
7
  >
9
- <IconChatBubbleLeft20Solid
10
- class="w-6 h-6"
11
- />
12
- <div class="absolute w-4 h-4 bg-lightNavbar dark:bg-darkNavbar rounded-full -top-1 -right-2">
13
- <IconSparklesSolid
14
- class="w-4 h-4"
15
- />
16
- </div>
17
- </div>
8
+ <IconBot class="w-4 h-4" />
9
+ {{ $t('Agent') }}
10
+ </Button>
18
11
 
19
12
  <Teleport to="body">
20
13
  <div
21
14
  ref="chatSurface"
22
15
  id="adminforth-agent-chat-surface"
23
- class="fixed bg-lightNavbar dark:bg-darkNavbar top-0 right-0 border-x border-b border-gray-200 dark:border-gray-700
24
- flex flex z-40 h-screen"
25
- :class="[agentStore.isChatOpen ? 'translate-x-0' : 'translate-x-full', !agentStore.isTeleportedToBody ? 'shadow-2xl' : '']"
26
- :style="{ width: agentStore.chatWidth + 'rem' }"
16
+ class="fixed top-0 right-0 z-40 flex h-screen border-l border-lightNavbarBorder bg-lightNavbar dark:border-darkNavbarBorder dark:bg-darkNavbar"
17
+ :class="[agentStore.isChatOpen ? 'translate-x-0' : 'translate-x-full', !agentStore.isTeleportedToBody && !agentStore.isFullScreen ? 'shadow-popover dark:shadow-darkPopover' : '']"
18
+ :style="{ width: agentStore.isFullScreen ? '100%' : agentStore.chatWidth + 'rem' }"
27
19
  >
28
20
  <div
29
21
  v-if="!(coreStore.isMobile || agentStore.isFullScreen)"
@@ -58,7 +50,8 @@
58
50
  </template>
59
51
 
60
52
  <script setup lang="ts">
61
- import { IconChatBubbleLeft20Solid, IconSparklesSolid } from '@iconify-prerendered/vue-heroicons';
53
+ import { IconBot } from '@iconify-prerendered/vue-lucide';
54
+ import { Button } from '@/afcl';
62
55
  import { useTemplateRef, onMounted, ref, onUnmounted } from 'vue';
63
56
  import { onClickOutside } from '@vueuse/core'
64
57
  import ConversationArea from './conversation_area/ConversationArea.vue';
@@ -1,26 +1,30 @@
1
1
  <template>
2
- <div class="flex items-center gap-3">
2
+ <div class="flex items-center gap-2">
3
3
  <Button
4
4
  variant="secondary"
5
- class="!p-0 h-7 w-7 mr-1 bg-red-600 text-white hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800"
6
- title="Cancel edit"
5
+ size="sm"
6
+ iconOnly
7
+ :title="$t('Cancel edit')"
8
+ :aria-label="$t('Cancel edit')"
7
9
  @click="emit('cancel')"
8
10
  >
9
- <IconCloseOutline class="w-5 h-5" />
11
+ <IconX />
10
12
  </Button>
11
13
  <Button
12
- class="!p-0 h-7 w-7"
13
- title="Send edited message"
14
+ size="sm"
15
+ iconOnly
16
+ :title="$t('Send edited message')"
17
+ :aria-label="$t('Send edited message')"
14
18
  :disabled="confirmDisabled"
15
19
  @click="emit('confirm')"
16
20
  >
17
- <IconCheckOutline class="w-6 h-6 text-white" />
21
+ <IconCheck />
18
22
  </Button>
19
23
  </div>
20
24
  </template>
21
25
 
22
26
  <script setup lang="ts">
23
- import { IconCloseOutline, IconCheckOutline } from '@iconify-prerendered/vue-flowbite';
27
+ import { IconCheck, IconX } from '@iconify-prerendered/vue-lucide';
24
28
  import { Button } from '@/afcl';
25
29
 
26
30
  defineProps<{
@@ -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