@adminforth/agent 1.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 (66) hide show
  1. package/.woodpecker/buildRelease.sh +13 -0
  2. package/.woodpecker/buildSlackNotify.sh +46 -0
  3. package/.woodpecker/release.yml +57 -0
  4. package/agent/middleware/apiBasedTools.ts +109 -0
  5. package/agent/middleware/sequenceDebug.ts +302 -0
  6. package/agent/simpleAgent.ts +291 -0
  7. package/agent/skills/registry.ts +135 -0
  8. package/agent/systemPrompt.ts +69 -0
  9. package/agent/toolCallEvents.ts +17 -0
  10. package/agent/tools/apiTool.ts +99 -0
  11. package/agent/tools/fetchSkill.ts +58 -0
  12. package/agent/tools/fetchToolSchema.ts +50 -0
  13. package/agent/tools/index.ts +26 -0
  14. package/apiBasedTools.ts +625 -0
  15. package/build.log +30 -0
  16. package/custom/ChatSurface.vue +184 -0
  17. package/custom/ConversationArea.vue +175 -0
  18. package/custom/Message.vue +206 -0
  19. package/custom/SessionsHistory.vue +93 -0
  20. package/custom/ToolRenderer.vue +131 -0
  21. package/custom/ToolsGroup.vue +67 -0
  22. package/custom/incremark_code_renderers/IncremarkShikiCodeBlock.vue +301 -0
  23. package/custom/incremark_code_renderers/incremarkCodeHighlight.ts +285 -0
  24. package/custom/incremark_code_renderers/incremarkRenderer.ts +653 -0
  25. package/custom/incremark_code_renderers/renderIncremarkMarkdown.ts +118 -0
  26. package/custom/package.json +26 -0
  27. package/custom/pnpm-lock.yaml +1467 -0
  28. package/custom/skills/fetch_data/SKILL.md +15 -0
  29. package/custom/skills/mutate_data/SKILL.md +108 -0
  30. package/custom/tsconfig.json +16 -0
  31. package/custom/types.ts +34 -0
  32. package/custom/useAgentStore.ts +349 -0
  33. package/dist/agent/middleware/apiBasedTools.js +91 -0
  34. package/dist/agent/middleware/sequenceDebug.js +210 -0
  35. package/dist/agent/simpleAgent.js +173 -0
  36. package/dist/agent/skills/registry.js +108 -0
  37. package/dist/agent/systemPrompt.js +64 -0
  38. package/dist/agent/toolCallEvents.js +1 -0
  39. package/dist/agent/tools/apiTool.js +93 -0
  40. package/dist/agent/tools/fetchSkill.js +51 -0
  41. package/dist/agent/tools/fetchToolSchema.js +36 -0
  42. package/dist/agent/tools/index.js +28 -0
  43. package/dist/apiBasedTools.js +412 -0
  44. package/dist/custom/ChatSurface.vue +184 -0
  45. package/dist/custom/ConversationArea.vue +175 -0
  46. package/dist/custom/Message.vue +206 -0
  47. package/dist/custom/SessionsHistory.vue +93 -0
  48. package/dist/custom/ToolRenderer.vue +131 -0
  49. package/dist/custom/ToolsGroup.vue +67 -0
  50. package/dist/custom/incremark_code_renderers/IncremarkShikiCodeBlock.vue +301 -0
  51. package/dist/custom/incremark_code_renderers/incremarkCodeHighlight.ts +285 -0
  52. package/dist/custom/incremark_code_renderers/incremarkRenderer.ts +653 -0
  53. package/dist/custom/incremark_code_renderers/renderIncremarkMarkdown.ts +118 -0
  54. package/dist/custom/package.json +26 -0
  55. package/dist/custom/pnpm-lock.yaml +1467 -0
  56. package/dist/custom/skills/fetch_data/SKILL.md +15 -0
  57. package/dist/custom/skills/mutate_data/SKILL.md +108 -0
  58. package/dist/custom/tsconfig.json +16 -0
  59. package/dist/custom/types.ts +34 -0
  60. package/dist/custom/useAgentStore.ts +349 -0
  61. package/dist/index.js +415 -0
  62. package/dist/types.js +1 -0
  63. package/index.ts +457 -0
  64. package/package.json +58 -0
  65. package/tsconfig.json +13 -0
  66. package/types.ts +45 -0
@@ -0,0 +1,184 @@
1
+ <template>
2
+ <div
3
+ class="relative w-6 h-6 cursor-pointer mr-6 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"
7
+ @click="agentStore.setIsChatOpen(!agentStore.isChatOpen)"
8
+ >
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>
18
+
19
+ <div
20
+ ref="chatSurface"
21
+ class="fixed bg-lightNavbar dark:bg-darkNavbar h-screen top-0 right-0 border-x border-b border-gray-200 dark:border-gray-700
22
+ transition-transform duration-200 ease-in-out
23
+ flex flex z-10"
24
+ :class="[agentStore.isChatOpen ? 'translate-x-0' : 'translate-x-full', !agentStore.isTeleportedToBody ? 'shadow-2xl' : '']"
25
+ :style="{ width: agentStore.chatWidth + 'px' }"
26
+ >
27
+ <div
28
+ v-if="!coreStore.isMobile"
29
+ class="w-2 cursor-ew-resize absolute left-0 top-0 h-full z-30"
30
+ @mousedown="startResize"
31
+ ></div>
32
+
33
+ <div class="w-full h-full flex flex-col">
34
+ <div class="flex items-center justify-between">
35
+ <div class="flex items-center h-[55px]">
36
+ <IconBarsOutline
37
+ class="m-2 w-8 h-8 p-1 cursor-pointer hover:scale-110 rounded transition-colors duration-200
38
+ text-lightNavbarIcons hover:text-lightNavbarIcons/80 hover:bg-lightNavbarIcons/20
39
+ dark:text-darkNavbarIcons hover:text-darkNavbarIcons/80 hover:bg-darkNavbarIcons/20 "
40
+ :class="agentStore.isSessionHistoryOpen ?
41
+ 'bg-lightNavbarIcons/20 text-lightNavbarIcons/80 dark:bg-darkNavbarIcons/20 dark:text-darkNavbarIcons/80' :
42
+ ''"
43
+ @click="agentStore.setSessionHistoryOpen(!agentStore.isSessionHistoryOpen)"
44
+ />
45
+ <IconOpenSidebarSolid
46
+ v-if="!agentStore.isTeleportedToBody && !coreStore.isMobile"
47
+ class="m-2 w-8 h-8 p-1 cursor-pointer hover:scale-110 rounded transition-colors duration-200
48
+ text-lightNavbarIcons hover:text-lightNavbarIcons/80 hover:bg-lightNavbarIcons/20
49
+ dark:text-darkNavbarIcons hover:text-darkNavbarIcons/80 hover:bg-darkNavbarIcons/20 "
50
+ @click="agentStore.setIsTeleportedToBody(true)"
51
+ />
52
+ <IconCloseSidebarSolid
53
+ v-else-if="!coreStore.isMobile"
54
+ class="m-2 w-8 h-8 p-1 cursor-pointer hover:scale-110 rounded transition-colors duration-200
55
+ text-lightNavbarIcons hover:text-lightNavbarIcons/80 bg-lightNavbarIcons/20
56
+ dark:bg-darkNavbarIcons/20 dark:text-darkNavbarIcons/80 hover:text-darkNavbarIcons/80 hover:bg-darkNavbarIcons/20 "
57
+ @click="agentStore.setIsTeleportedToBody(false)"
58
+ />
59
+ </div>
60
+
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
+
68
+ </div>
69
+ <div class="relative flex-1 flex flex-col overflow-hidden">
70
+ <ConversationArea
71
+ v-if="agentStore.isChatOpen"
72
+ class="flex-1 overflow-auto"
73
+ :messages="agentStore.chatMessages"
74
+ />
75
+
76
+ <div class="w-full mb-2 flex items-center justify-center px-2 bg-transparent relative">
77
+ <textarea
78
+ v-model="agentStore.userMessageInput"
79
+ ref="textInput"
80
+ @input="autoResize"
81
+ class="min-h-12 p-4 pr-12 w-full resize-none overflow-hidden border text-lightInputText dark:text-darkInputText rounded-md bg-transparent text-sm bg-gray-50 dark:bg-gray-700 dark:border-gray-600 focus:outline-none"
82
+ placeholder="Type a message..."
83
+ @keydown.enter.exact.prevent="async () => {await agentStore.sendMessage(); autoResize();}"
84
+ />
85
+ <Button
86
+ class="absolute right-4 bottom-2 !p-0 h-[34px] w-[34px]"
87
+ @click="async () => {await agentStore.sendMessage(); autoResize();}"
88
+ :disabled="!agentStore.trimmedUserMessage || agentStore.isResponseInProgress"
89
+ >
90
+ <IconArrowUpOutline
91
+ class="w-8 h-8 p-1
92
+ text-white"
93
+ />
94
+ </Button>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ </template>
101
+
102
+ <script setup lang="ts">
103
+ import { IconChatBubbleLeft20Solid, IconSparklesSolid } from '@iconify-prerendered/vue-heroicons';
104
+ import { IconCloseOutline, IconBarsOutline, IconArrowUpOutline, IconCloseSidebarSolid, IconOpenSidebarSolid } from '@iconify-prerendered/vue-flowbite';
105
+ import { useTemplateRef, onMounted, ref, computed } from 'vue';
106
+ import { onClickOutside } from '@vueuse/core'
107
+ import ConversationArea from './ConversationArea.vue';
108
+ import { useAgentStore } from './useAgentStore';
109
+ import { Button } from '@/afcl';
110
+ import { useCoreStore } from '@/stores/core';
111
+
112
+ const props = defineProps<{
113
+ meta: {
114
+ pluginInstanceId: string;
115
+ }
116
+ }>();
117
+
118
+ const chatSurface = useTemplateRef('chatSurface');
119
+ const textInput = useTemplateRef('textInput');
120
+ const agentStore = useAgentStore();
121
+ const coreStore = useCoreStore();
122
+
123
+ const MAX_WIDTH = 800;
124
+ const MIN_WIDTH = 382; //w-96
125
+
126
+ let startX = 0
127
+ let startWidth = 0
128
+
129
+ const startResize = (e: MouseEvent) => {
130
+ startX = e.clientX
131
+ startWidth = agentStore.chatWidth
132
+
133
+ document.body.style.userSelect = 'none'
134
+ document.body.style.cursor = 'ew-resize'
135
+
136
+ document.addEventListener('mousemove', onResize)
137
+ document.addEventListener('mouseup', stopResize)
138
+ }
139
+
140
+ const onResize = (e: MouseEvent) => {
141
+ const dx = startX - e.clientX
142
+ agentStore.setChatWidth(Math.min(Math.max(startWidth + dx, MIN_WIDTH), MAX_WIDTH))
143
+ }
144
+
145
+ const stopResize = () => {
146
+ document.body.style.userSelect = ''
147
+ document.body.style.cursor = ''
148
+
149
+ document.removeEventListener('mousemove', onResize)
150
+ document.removeEventListener('mouseup', stopResize)
151
+
152
+ const appRoot = document.getElementById('app');
153
+ const header = document.getElementById('af-header-nav');
154
+ if (appRoot && header) {
155
+ appRoot.style.transition = 'padding-right 200ms ease-in-out';
156
+ header.style.transition = 'padding-right 200ms ease-in-out';
157
+ }
158
+ }
159
+
160
+ onClickOutside(chatSurface, () => {if (!agentStore.isTeleportedToBody) agentStore.setIsChatOpen(false);});
161
+
162
+ onMounted(async () => {
163
+ agentStore.regisrerTextInput(textInput.value);
164
+ textInput.value?.focus();
165
+ await agentStore.fetchSessionsList();
166
+ });
167
+
168
+ function autoResize() {
169
+ const el = textInput.value
170
+ if (!el) return
171
+
172
+ el.style.height = 'auto'
173
+ //max-w-48
174
+ const maxHeight = 192
175
+ if (el.scrollHeight > maxHeight) {
176
+ el.style.height = maxHeight + 'px'
177
+ el.style.overflowY = 'auto'
178
+ } else {
179
+ el.style.height = el.scrollHeight + 'px'
180
+ el.style.overflowY = 'hidden'
181
+ }
182
+ }
183
+
184
+ </script>
@@ -0,0 +1,175 @@
1
+ <template>
2
+ <button @click="scrollContainer.scrollToBottom()">
3
+ <IconArrowDownOutline
4
+ class="absolute bottom-32 right-4 bg-lightPrimary dark:bg-darkPrimary text-white p-2 w-10 h-10 rounded-full transition-opacity duration-100 ease-in"
5
+ :class="showScrollToBottomButton ? 'opacity-100' : 'opacity-0 pointer-events-none'"
6
+ :disabled="!showScrollToBottomButton"
7
+ />
8
+ </button>
9
+ <SessionsHistory
10
+ :class="agentStore.isSessionHistoryOpen ? 'translate-x-0' : '-translate-x-full'"
11
+ />
12
+ <div
13
+ v-if="agentStore.isSessionHistoryOpen"
14
+ @click="agentStore.setSessionHistoryOpen(false)"
15
+ class="absolute bg-black/10 backdrop-blur-md z-10 h-full w-full"
16
+ >
17
+
18
+ </div>
19
+ <AutoScrollContainer
20
+ enabled
21
+ class="flex flex-col overflow-y-auto border-t border-gray-200 dark:border-gray-700"
22
+ ref="scrollContainer"
23
+ behavior="smooth"
24
+ >
25
+
26
+ <div
27
+ v-for="message in props.messages" :key="message.id"
28
+ class="flex flex-col w-full"
29
+ :class="message.role === 'user' ? 'self-end' : 'self-start'"
30
+ >
31
+ <ToolsGroup :toolGroup="groupToolCallParts(message)" />
32
+ <template
33
+ v-for="part in getParts(message)"
34
+ :key="part.type"
35
+ >
36
+ <Message
37
+ v-if="part.type !== 'data-tool-call'"
38
+ :message="part.text"
39
+ :role="message.role"
40
+ :type="part.type"
41
+ :state="part.state"
42
+ :data="part.data"
43
+ @toggle-thoughts="() => clicks++"
44
+ >
45
+ </Message>
46
+ </template>
47
+ </div>
48
+ <!-- Show a placeholder message if the last message is not of type 'text' or 'reasoning' -->
49
+ <Message
50
+ v-if="props.messages.length > 0 && showFakeThinkingMessage"
51
+ :message="''"
52
+ :role="props.messages[props.messages.length - 1].role"
53
+ type="reasoning"
54
+ state="streaming"
55
+ />
56
+ <div
57
+ v-if="props.messages.length === 0"
58
+ class="flex-1 flex flex-col items-center justify-center text-gray-400 tracking-widest text-xl font-medium"
59
+ >
60
+ <p>Start the conversation</p>
61
+ <p class="tracking-normal text-base text">Give any input to begin</p>
62
+ </div>
63
+ </AutoScrollContainer>
64
+ </template>
65
+
66
+
67
+ <script setup lang="ts">
68
+ import Message from './Message.vue';
69
+ import type { IMessage, IPart } from './types';
70
+ import { useTemplateRef, ref, defineAsyncComponent, onMounted, watch, computed } from 'vue';
71
+ import { IconArrowDownOutline } from '@iconify-prerendered/vue-flowbite';
72
+ import SessionsHistory from './SessionsHistory.vue';
73
+ import { useAgentStore } from './useAgentStore';
74
+ import ToolRenderer from './ToolRenderer.vue';
75
+ import ToolsGroup from './ToolsGroup.vue';
76
+
77
+ const scrollContainer = useTemplateRef('scrollContainer');
78
+ const showScrollToBottomButton = ref(false);
79
+ const innerScrollContainerRef = ref(null);
80
+ const AutoScrollContainer = defineAsyncComponent(() => import('@incremark/vue').then(module => module.AutoScrollContainer))
81
+ const agentStore = useAgentStore();
82
+ const clicks = ref(0);
83
+
84
+ function recalculateScroll() {
85
+ if (scrollContainer.value) {
86
+ const isScrolledUp = scrollContainer.value.isUserScrolledUp();
87
+ showScrollToBottomButton.value = !!isScrolledUp;
88
+ }
89
+ }
90
+
91
+ onMounted(async () => {
92
+ await import('@incremark/theme/styles.css')
93
+ });
94
+
95
+ watch(scrollContainer, () => {
96
+ if (scrollContainer.value) {
97
+ innerScrollContainerRef.value = scrollContainer.value.container;
98
+
99
+ innerScrollContainerRef.value.addEventListener('scroll', () => {
100
+ recalculateScroll();
101
+ });
102
+ }
103
+ })
104
+
105
+ watch(clicks, () => {
106
+ recalculateScroll();
107
+ })
108
+
109
+ const showFakeThinkingMessage = computed(() => {
110
+ const lastMessage = props.messages[props.messages.length - 1];
111
+ if (!lastMessage) return false;
112
+ const lastPart = getParts(lastMessage)[getParts(lastMessage).length - 1];
113
+ return lastPart?.type !== 'text' && lastPart?.type !== 'reasoning';
114
+ })
115
+
116
+ const getParts = (message: IMessage) => {
117
+ return message.parts?.length
118
+ ? message.parts
119
+ : [{ text: '', type: 'reasoning', state: 'streaming' }];
120
+ };
121
+
122
+ const formatToolCallTextPart = ((part: IPart, currentMessage: IMessage) => {
123
+ if (part.type === 'data-tool-call') {
124
+ if (part.data?.phase === 'start') {
125
+ const finishedPart = currentMessage.parts?.find(p => p.type === 'data-tool-call' && p.data?.toolCallId === part.data?.toolCallId && p.data?.phase === 'end');
126
+ return {
127
+ type: 'text',
128
+ toolInfo: {
129
+ toolCallId: part.data!.toolCallId,
130
+ toolName: part.data!.toolName,
131
+ phase: finishedPart ? 'end' : 'start',
132
+ durationMs: finishedPart ? finishedPart.data?.durationMs : undefined,
133
+ input: part.data!.input,
134
+ output: finishedPart ? finishedPart.data!.output : undefined,
135
+ }
136
+ }
137
+ }
138
+ }
139
+ return null;
140
+ });
141
+
142
+ const groupToolCallParts = (message: IMessage) => {
143
+ const groupedParts = [];
144
+ let currentToolName = null;
145
+ const parts = getParts(message);
146
+ if (!parts) return [];
147
+ const formatedToolParts = parts.map(part => {
148
+ return formatToolCallTextPart(part as IPart, message)
149
+ });
150
+ for( const[index, part] of formatedToolParts.entries()){
151
+ if(!part?.toolInfo) {
152
+ continue;
153
+ }
154
+ console.log('part', part);
155
+ if (part.toolInfo.toolName === currentToolName) {
156
+ console.log('grouping part with tool name', currentToolName);
157
+ groupedParts[groupedParts.length - 1].groupedTools.push(part);
158
+ continue;
159
+ }
160
+ currentToolName = part.toolInfo.toolName;
161
+ console.log('starting new group with tool name', currentToolName);
162
+ groupedParts.push({
163
+ title: currentToolName,
164
+ groupedTools: [part]
165
+ });
166
+ }
167
+ console.log('groupedParts', groupedParts);
168
+ return groupedParts;
169
+ }
170
+
171
+ const props = defineProps<{
172
+ messages: IMessage[]
173
+ }>();
174
+
175
+ </script>
@@ -0,0 +1,206 @@
1
+ <template>
2
+ <div
3
+ class="max-w-[80%] flex px-4 py-2 m-2 rounded-xl border border-gray-200 dark:border-gray-700"
4
+ @click="handleMarkdownLinkClick"
5
+ :class="props.role === 'user' ? 'bg-lightListTableHeading dark:bg-darkListTableHeading self-end'
6
+ : isTypeReasoning || isTypeToolCall ? 'bg-transparent border-none self-start'
7
+ : 'bg-blue-100 dark:bg-blue-700/10 self-start'"
8
+ >
9
+ <IncremarkContent
10
+ class="text-wrap break-words max-w-full"
11
+ v-if="content && props.type === 'text'"
12
+ :content="content"
13
+ :is-finished="isFinished"
14
+ :components="incremarkComponents"
15
+ :incremark-options="incremarkOptions"
16
+ />
17
+ <!-- reasoning/thinking -->
18
+ <div
19
+ v-else-if="isTypeReasoning || isStateStreaming"
20
+ class="flex flex-col items-start gap-1 text-gray-500 py-2 "
21
+ >
22
+ <div class="flex items-center gap-1 hover:underline cursor-pointer text-lightListTableHeadingText hover:text-lightListTableHeadingText dark:text-darkListTableHeadingText dark:hover:text-darkListTableHeadingText" @click="isThoughtsExpanded = !isThoughtsExpanded">
23
+ <IconAngleDownOutline
24
+ v-if="content"
25
+ :class="isThoughtsExpanded ? 'rotate-180' : 'rotate-0'"
26
+ class="transition-transform duration-200"
27
+ />
28
+ {{ isStateStreaming ? 'Thinking' : 'Thoughts' }}
29
+ <template v-if="isStateStreaming">
30
+ <span class="bounce-dot1 rounded-full w-2 h-2 bg-lightPrimary"></span>
31
+ <span class="bounce-dot2 rounded-full w-2 h-2 bg-lightPrimary"></span>
32
+ <span class="bounce-dot3 rounded-full w-2 h-2 bg-lightPrimary"></span>
33
+ </template>
34
+ </div>
35
+ <transition name="expand" class="max-h-36 overflow-y-auto">
36
+ <p v-show="isThoughtsExpanded" class="overflow-hidden">
37
+ {{ content }}
38
+ </p>
39
+ </transition>
40
+ </div>
41
+ <div v-else-if="isTypeToolCall && isToolCallStart">
42
+ {{ props.data?.toolName }} start
43
+ </div>
44
+ <div v-else-if="isTypeToolCall && isToolCallEnd">
45
+ {{ props.data?.toolName }} end
46
+ </div>
47
+ <p v-else class="text-red-500 py-2">
48
+ Error occured
49
+ </p>
50
+ </div>
51
+ </template>
52
+
53
+ <script setup lang="ts">
54
+ import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue';
55
+ import { useRouter } from 'vue-router';
56
+ import { IconAngleDownOutline } from '@iconify-prerendered/vue-flowbite';
57
+
58
+ const IncremarkContent = defineAsyncComponent(() => import('@incremark/vue').then(module => module.IncremarkContent))
59
+ const ShikiCodeBlock = defineAsyncComponent(() => import('./incremark_code_renderers/IncremarkShikiCodeBlock.vue'))
60
+
61
+ const incremarkComponents = {
62
+ code: ShikiCodeBlock,
63
+ };
64
+
65
+ const incremarkOptions = {
66
+ gfm: true,
67
+ math: { tex: true },
68
+ containers: true,
69
+ htmlTree: true,
70
+ };
71
+
72
+ const router = useRouter();
73
+
74
+ onMounted(async () => {
75
+ void import('katex/dist/katex.min.css')
76
+ })
77
+
78
+ const props = defineProps<{
79
+ type: string,
80
+ message: string
81
+ state: string
82
+ data?: any
83
+ role: 'user' | 'assistant'
84
+ }>();
85
+
86
+ const emit = defineEmits(['toggle-thoughts']);
87
+
88
+ const content = computed(() => props.message)
89
+ const isFinished = computed(() => props.state === 'done')
90
+ const isThoughtsExpanded = ref(false)
91
+
92
+ const isTypeReasoning = computed(() => props.type === 'reasoning')
93
+ const isTypeToolCall = computed(() => props.type === 'data-tool-call')
94
+ const isToolCallStart = computed(() => {
95
+ if (props.type !== 'data-tool-call') return false;
96
+ return props.data?.phase === 'start';
97
+ })
98
+ const isToolCallEnd = computed(() => {
99
+ if (props.type !== 'data-tool-call') return false;
100
+ return props.data?.phase === 'end';
101
+ })
102
+ const isStateStreaming = computed(() => props.state === 'streaming')
103
+
104
+ watch(isThoughtsExpanded, (newValue: boolean) => {
105
+ emit('toggle-thoughts', newValue);
106
+ })
107
+
108
+ function handleMarkdownLinkClick(event: MouseEvent) {
109
+ if (props.type !== 'text' || event.defaultPrevented || event.button !== 0) {
110
+ return;
111
+ }
112
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
113
+ return;
114
+ }
115
+ const target = event.target;
116
+ if (!(target instanceof Element)) {
117
+ return;
118
+ }
119
+ const anchor = target.closest('a');
120
+ if (!(anchor instanceof HTMLAnchorElement)) {
121
+ return;
122
+ }
123
+ const href = anchor.getAttribute('href');
124
+ if (!href || href.startsWith('mailto:') || href.startsWith('tel:')) {
125
+ return;
126
+ }
127
+ event.preventDefault();
128
+
129
+ const internalRoute = resolveInternalRoute(href);
130
+ if (internalRoute !== null) {
131
+ void router.push(internalRoute);
132
+ return;
133
+ }
134
+
135
+ window.location.assign(new URL(href, window.location.href).toString());
136
+ }
137
+
138
+ function resolveInternalRoute(href: string): string | null {
139
+ if (href.startsWith('#')) {
140
+ return `${window.location.pathname}${window.location.search}${href}`;
141
+ }
142
+
143
+ const resolvedUrl = new URL(href, window.location.href);
144
+ if (resolvedUrl.origin !== window.location.origin) {
145
+ return null;
146
+ }
147
+
148
+ return `${resolvedUrl.pathname}${resolvedUrl.search}${resolvedUrl.hash}`;
149
+ }
150
+
151
+ </script>
152
+
153
+ <style lang="scss">
154
+ .incremark-paragraph {
155
+ margin: 8px 0;
156
+ }
157
+ </style>
158
+
159
+ <style scoped>
160
+
161
+ .bounce-dot1 {
162
+ animation: bounce 1.5s infinite;
163
+ animation-delay: 0s;
164
+ }
165
+
166
+ .bounce-dot2 {
167
+ animation: bounce 1.5s infinite;
168
+ animation-delay: 0.1s;
169
+ }
170
+
171
+ .bounce-dot3 {
172
+ animation: bounce 1.5s infinite;
173
+ animation-delay: 0.2s;
174
+ }
175
+
176
+ @keyframes bounce {
177
+ 0%, 100% {
178
+ transform: translateY(20%);
179
+ opacity: 0.3;
180
+ animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
181
+ }
182
+ 50% {
183
+ transform: none;
184
+ opacity: 1;
185
+ animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
186
+ }
187
+ }
188
+
189
+ .expand-enter-active,
190
+ .expand-leave-active {
191
+ transition: all 0.3s ease;
192
+ }
193
+
194
+ .expand-enter-from,
195
+ .expand-leave-to {
196
+ opacity: 0;
197
+ max-height: 0;
198
+ }
199
+
200
+ .expand-enter-to,
201
+ .expand-leave-from {
202
+ opacity: 1;
203
+ max-height: 144px;
204
+ }
205
+
206
+ </style>
@@ -0,0 +1,93 @@
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
+ "
7
+ >
8
+ <h3 :class="h3Style">{{ $t('Chat history') }}</h3>
9
+ <Button @click="agentStore.createPreSession()" :disabled="agentStore.isResponseInProgress" class="w-[360px] mx-4 my-2 mb-4 rounded-3xl text-gray-800 dark:text-gray-200">
10
+ <IconPlusOutline class="w-5 h-5" />
11
+ {{ $t('New chat') }}
12
+ </Button>
13
+ <div class="w-full border-b border-gray-200 dark:border-gray-700"/>
14
+ <div class="absolute w-full h-full flex flex-col items-center justify-center bg-gray-100/50 dark:bg-gray-700/50 z-10" v-if="agentStore.isResponseInProgress">
15
+ <Spinner class="w-8 h-8" v-if="agentStore.isResponseInProgress" />
16
+ <p class="mt-2 text-gray-800 dark:text-gray-200">generation in progress...</p>
17
+ </div>
18
+ <div v-for="group in groupedSessions" :key="group.dayKey" class="w-full py-2">
19
+ <div class="px-4 pb-2 text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">
20
+ {{ group.label }}
21
+ </div>
22
+
23
+ <button
24
+ v-for="session in group.sessions"
25
+ :key="session.sessionId"
26
+ 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 overflow-hidden text-nowrap"
27
+ :class="{'bg-lightPrimary/20 hover:bg-lightPrimary/20 dark:bg-darkPrimary/20 dark:hover:bg-darkPrimary/20': agentStore.activeSessionId === session.sessionId, 'cursor-default opacity-50 pointer-events-none': agentStore.isResponseInProgress}"
28
+ @click="agentStore.setActiveSession(session.sessionId)"
29
+ :disabled="agentStore.isResponseInProgress"
30
+ >
31
+ {{ session.title || session.sessionId }}
32
+ <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">
33
+ <IconPlusOutline class="rotate-45 w-6 h-6"/>
34
+ </div>
35
+ </button>
36
+ </div>
37
+ <p
38
+ v-if="!groupedSessions || groupedSessions.length === 0"
39
+ class="w-full h-full flex items-center justify-center text-gray-800 dark:text-gray-200"
40
+ >
41
+ There is no previous chat sessions
42
+ </p>
43
+ </div>
44
+ </template>
45
+
46
+
47
+ <script setup lang="ts">
48
+ import { Button, Spinner } from '@/afcl'
49
+ import { computed } from 'vue';
50
+ import { IconPlusOutline } from '@iconify-prerendered/vue-flowbite';
51
+ import type { ISessionsListItem } from './types';
52
+ import { useAgentStore } from './useAgentStore';
53
+
54
+ const agentStore = useAgentStore();
55
+
56
+ const h3Style = "text-gray-800 dark:text-gray-200 font-medium text-xl tracking-widest mt-4"
57
+
58
+ const dayLabelFormatter = new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric' });
59
+ const dayLabelWithYearFormatter = new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
60
+
61
+ function getLocalDayKey(date: Date) {
62
+ const year = date.getFullYear();
63
+ const month = `${date.getMonth() + 1}`.padStart(2, '0');
64
+ const day = `${date.getDate()}`.padStart(2, '0');
65
+
66
+ return `${year}-${month}-${day}`;
67
+ }
68
+
69
+ const groupedSessions = computed(() => {
70
+ const groups = new Map<string, { dayKey: string; label: string; sessions: ISessionsListItem[] }>();
71
+
72
+ for (const session of agentStore.sessionList) {
73
+ const date = new Date(session.timestamp);
74
+ const dayKey = getLocalDayKey(date);
75
+ const label = date.getFullYear() === new Date().getFullYear()
76
+ ? dayLabelFormatter.format(date)
77
+ : dayLabelWithYearFormatter.format(date);
78
+
79
+ if (!groups.has(dayKey)) {
80
+ groups.set(dayKey, {
81
+ dayKey,
82
+ label,
83
+ sessions: [],
84
+ });
85
+ }
86
+
87
+ groups.get(dayKey)!.sessions.push(session);
88
+ }
89
+
90
+ return Array.from(groups.values());
91
+ });
92
+
93
+ </script>