@adminforth/agent 1.20.0 → 1.22.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 (40) hide show
  1. package/apiBasedTools.ts +7 -0
  2. package/build.log +12 -6
  3. package/custom/ChatSurface.vue +2 -2
  4. package/custom/CustomAutoScrollContainer.vue +127 -0
  5. package/custom/composables/useAgentStore.ts +8 -4
  6. package/custom/conversation_area/ConversationArea.vue +109 -0
  7. package/custom/conversation_area/MessageRenderer.vue +33 -0
  8. package/custom/conversation_area/ProcessingTimeline.vue +190 -0
  9. package/custom/conversation_area/ReasoningRenderer.vue +87 -0
  10. package/{dist/custom/Message.vue → custom/conversation_area/TextRenderer.vue} +14 -102
  11. package/custom/conversation_area/ThreeDotsAnimation.vue +35 -0
  12. package/custom/{ToolRenderer.vue → conversation_area/ToolRenderer.vue} +65 -13
  13. package/custom/conversation_area/ToolsGroup.vue +63 -0
  14. package/custom/package.json +2 -1
  15. package/custom/pnpm-lock.yaml +18 -0
  16. package/custom/skills/data-analytics/SKILL.md +3 -3
  17. package/custom/types.ts +11 -1
  18. package/custom/utils.ts +29 -0
  19. package/dist/apiBasedTools.js +6 -0
  20. package/dist/custom/ChatSurface.vue +2 -2
  21. package/dist/custom/CustomAutoScrollContainer.vue +127 -0
  22. package/dist/custom/composables/useAgentStore.ts +8 -4
  23. package/dist/custom/conversation_area/ConversationArea.vue +109 -0
  24. package/dist/custom/conversation_area/MessageRenderer.vue +33 -0
  25. package/dist/custom/conversation_area/ProcessingTimeline.vue +190 -0
  26. package/dist/custom/conversation_area/ReasoningRenderer.vue +87 -0
  27. package/{custom/Message.vue → dist/custom/conversation_area/TextRenderer.vue} +14 -102
  28. package/dist/custom/conversation_area/ThreeDotsAnimation.vue +35 -0
  29. package/dist/custom/{ToolRenderer.vue → conversation_area/ToolRenderer.vue} +65 -13
  30. package/dist/custom/conversation_area/ToolsGroup.vue +63 -0
  31. package/dist/custom/package.json +2 -1
  32. package/dist/custom/pnpm-lock.yaml +18 -0
  33. package/dist/custom/skills/data-analytics/SKILL.md +3 -3
  34. package/dist/custom/types.ts +11 -1
  35. package/dist/custom/utils.ts +29 -0
  36. package/package.json +1 -1
  37. package/custom/ConversationArea.vue +0 -198
  38. package/custom/ToolsGroup.vue +0 -67
  39. package/dist/custom/ConversationArea.vue +0 -198
  40. package/dist/custom/ToolsGroup.vue +0 -67
package/apiBasedTools.ts CHANGED
@@ -75,6 +75,7 @@ type ToolOverrideContext = {
75
75
 
76
76
  type ToolOverride = {
77
77
  wipe_frontend_specific_data?: readonly string[];
78
+ format_tool?: (params: ToolOverrideContext) => Promise<string> | string;
78
79
  post_process_response?: (params: ToolOverrideContext) => Promise<unknown> | unknown;
79
80
  };
80
81
 
@@ -103,6 +104,9 @@ const TOOL_OVERRIDES: Record<string, ToolOverride> = {
103
104
  'resource.options.actions[].customComponent',
104
105
  'resource.options.pageInjections',
105
106
  ],
107
+ format_tool: async ({ }) => {
108
+ return "get resource Apartments"
109
+ }
106
110
  },
107
111
  get_resource_data: {
108
112
  post_process_response: async ({ output, inputs, invokeTool, userTimeZone }) => {
@@ -126,6 +130,9 @@ const TOOL_OVERRIDES: Record<string, ToolOverride> = {
126
130
 
127
131
  return response;
128
132
  },
133
+ format_tool: async ({ }) => {
134
+ return "get 1-20 Apartment filtered listed=yes"
135
+ }
129
136
  },
130
137
  };
131
138
 
package/build.log CHANGED
@@ -5,11 +5,8 @@
5
5
  sending incremental file list
6
6
  custom/
7
7
  custom/ChatSurface.vue
8
- custom/ConversationArea.vue
9
- custom/Message.vue
8
+ custom/CustomAutoScrollContainer.vue
10
9
  custom/SessionsHistory.vue
11
- custom/ToolRenderer.vue
12
- custom/ToolsGroup.vue
13
10
  custom/chat.ts
14
11
  custom/package.json
15
12
  custom/pnpm-lock.yaml
@@ -19,6 +16,15 @@ custom/utils.ts
19
16
  custom/composables/
20
17
  custom/composables/useAgentStore.ts
21
18
  custom/composables/useAgentTransitions.ts
19
+ custom/conversation_area/
20
+ custom/conversation_area/ConversationArea.vue
21
+ custom/conversation_area/MessageRenderer.vue
22
+ custom/conversation_area/ProcessingTimeline.vue
23
+ custom/conversation_area/ReasoningRenderer.vue
24
+ custom/conversation_area/TextRenderer.vue
25
+ custom/conversation_area/ThreeDotsAnimation.vue
26
+ custom/conversation_area/ToolRenderer.vue
27
+ custom/conversation_area/ToolsGroup.vue
22
28
  custom/incremark_code_renderers/
23
29
  custom/incremark_code_renderers/IncremarkShikiCodeBlock.vue
24
30
  custom/incremark_code_renderers/incremarkCodeHighlight.ts
@@ -32,5 +38,5 @@ custom/skills/fetch_data/SKILL.md
32
38
  custom/skills/mutate_data/
33
39
  custom/skills/mutate_data/SKILL.md
34
40
 
35
- sent 188,483 bytes received 451 bytes 377,868.00 bytes/sec
36
- total size is 186,631 speedup is 0.99
41
+ sent 199,171 bytes received 562 bytes 399,466.00 bytes/sec
42
+ total size is 196,867 speedup is 0.99
@@ -179,7 +179,7 @@ import { IconChatBubbleLeft20Solid, IconSparklesSolid, IconArrowsPointingOut, Ic
179
179
  import { IconCloseOutline, IconBarsOutline, IconArrowUpOutline, IconCloseSidebarSolid, IconOpenSidebarSolid, IconAngleDownOutline } from '@iconify-prerendered/vue-flowbite';
180
180
  import { useTemplateRef, onMounted, ref,computed } from 'vue';
181
181
  import { onClickOutside } from '@vueuse/core'
182
- import ConversationArea from './ConversationArea.vue';
182
+ import ConversationArea from './conversation_area/ConversationArea.vue';
183
183
  import { useAgentStore } from './composables/useAgentStore';
184
184
  import { useAgentTransitions } from './composables/useAgentTransitions';
185
185
  import { Button } from '@/afcl';
@@ -246,7 +246,7 @@ onMounted(async () => {
246
246
  agentStore.setAvailableModes(props.meta.modes, props.meta.defaultModeName);
247
247
  agentStore.regisrerTextInput(textInput.value);
248
248
  textInput.value?.focus();
249
- const isTeleportedToBodyFromLocalStorage = agentStore.getLocalStorageItem('isTeleportedToBody') === 'true';
249
+ const isTeleportedToBodyFromLocalStorage = agentStore.getLocalStorageItem('isTeleportedToBody') === 'true' || agentStore.getLocalStorageItem('isTeleportedToBodyBeforeFullScreen') === 'true';
250
250
  if( coreStore.isMobile ) {
251
251
  agentStore.setIsTeleportedToBody(false);
252
252
  } else {
@@ -0,0 +1,127 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
3
+ import vueCustomScrollbar from 'vue-custom-scrollbar'
4
+
5
+ const props = withDefaults(defineProps<{
6
+ enabled?: boolean
7
+ threshold?: number
8
+ behavior?: ScrollBehavior
9
+ }>(), {
10
+ enabled: true,
11
+ threshold: 50,
12
+ behavior: 'instant'
13
+ })
14
+
15
+ const containerRef = ref<HTMLDivElement | null>(null)
16
+ const isUserScrolledUp = ref(false)
17
+
18
+ let lastScrollTop = 0
19
+ let lastScrollHeight = 0
20
+
21
+ function isNearBottom(): boolean {
22
+ const container = containerRef.value
23
+ if (!container) return true
24
+
25
+ const { scrollTop, scrollHeight, clientHeight } = container
26
+ return scrollHeight - scrollTop - clientHeight <= props.threshold
27
+ }
28
+
29
+ function scrollToBottom(force = false): void {
30
+ const container = containerRef.value
31
+ if (!container) return
32
+
33
+ if (isUserScrolledUp.value && !force) return
34
+
35
+ container.scrollTo({
36
+ top: container.scrollHeight,
37
+ behavior: props.behavior
38
+ })
39
+ }
40
+
41
+
42
+ function hasScrollbar(): boolean {
43
+ const container = containerRef.value
44
+ if (!container) return false
45
+ return container.scrollHeight > container.clientHeight
46
+ }
47
+
48
+
49
+ function handleScroll(): void {
50
+ const container = containerRef.value
51
+ if (!container) return
52
+
53
+ const { scrollTop, scrollHeight, clientHeight } = container
54
+
55
+ if (scrollHeight <= clientHeight) {
56
+ isUserScrolledUp.value = false
57
+ lastScrollTop = 0
58
+ lastScrollHeight = scrollHeight
59
+ return
60
+ }
61
+
62
+ if (isNearBottom()) {
63
+ isUserScrolledUp.value = false
64
+ } else {
65
+ const isScrollingUp = scrollTop < lastScrollTop
66
+ const isContentUnchanged = scrollHeight === lastScrollHeight
67
+
68
+ if (isScrollingUp && isContentUnchanged) {
69
+ isUserScrolledUp.value = true
70
+ }
71
+ }
72
+
73
+ lastScrollTop = scrollTop
74
+ lastScrollHeight = scrollHeight
75
+ }
76
+
77
+ let observer: MutationObserver | null = null
78
+
79
+ onMounted(() => {
80
+ if (!containerRef.value) return
81
+
82
+ lastScrollTop = containerRef.value.scrollTop
83
+ lastScrollHeight = containerRef.value.scrollHeight
84
+
85
+ observer = new MutationObserver(() => {
86
+ nextTick(() => {
87
+ if (!containerRef.value) return
88
+
89
+ if (!hasScrollbar()) {
90
+ isUserScrolledUp.value = false
91
+ }
92
+
93
+ lastScrollHeight = containerRef.value.scrollHeight
94
+
95
+ if (props.enabled && !isUserScrolledUp.value) {
96
+ scrollToBottom()
97
+ }
98
+ })
99
+ })
100
+
101
+ observer.observe(containerRef.value, {
102
+ childList: true,
103
+ subtree: true,
104
+ characterData: true
105
+ })
106
+ })
107
+
108
+ onUnmounted(() => {
109
+ observer?.disconnect()
110
+ })
111
+
112
+ defineExpose({
113
+ scrollToBottom: () => scrollToBottom(true),
114
+ isUserScrolledUp: () => isUserScrolledUp.value,
115
+ container: containerRef
116
+ })
117
+ </script>
118
+
119
+ <template>
120
+ <div
121
+ ref="containerRef"
122
+ class="auto-scroll-container h-full"
123
+ @scroll="handleScroll"
124
+ >
125
+ <slot />
126
+ </div>
127
+ </template>
@@ -1,5 +1,5 @@
1
1
  import { defineStore } from 'pinia';
2
- import { IAgentSession, ISessionsListItem, IMessage } from '../types';
2
+ import { IAgentSession, ISessionsListItem, IMessage, IPart } from '../types';
3
3
  import { ref, nextTick, computed, watch, onMounted, shallowRef } from 'vue';
4
4
  import { callAdminForthApi } from '@/utils';
5
5
  import { useAdminforth } from '@/adminforth';
@@ -93,7 +93,9 @@ export const useAgentStore = defineStore('agent', () => {
93
93
  })
94
94
  onMounted(() => {
95
95
  const chatWidthBeforeFullScreen = parseInt(getLocalStorageItem('chatWidthBeforeFullScreen') || '0', 10);
96
- if (chatWidthBeforeFullScreen) {
96
+ if (chatWidthBeforeFullScreen && (chatWidthBeforeFullScreen > MAX_WIDTH || chatWidthBeforeFullScreen < MIN_WIDTH)) {
97
+ setChatWidth(remToPx(DEFAULT_CHAT_WIDTH));
98
+ } else if (chatWidthBeforeFullScreen) {
97
99
  setChatWidth(remToPx(chatWidthBeforeFullScreen));
98
100
  } else {
99
101
  const savedChatWidth = parseInt(getLocalStorageItem('chatWidth') || '0', 10);
@@ -105,7 +107,7 @@ export const useAgentStore = defineStore('agent', () => {
105
107
  }
106
108
  }
107
109
  }
108
- isTeleportedToBody.value = getLocalStorageItem('isTeleportedToBody') === 'true';
110
+ setIsTeleportedToBody(getLocalStorageItem('isTeleportedToBody') === 'true' || getLocalStorageItem('isTeleportedToBodyBeforeFullScreen') === 'true');
109
111
  lastSessionId.value = getLocalStorageItem('lastSessionId');
110
112
  if (lastSessionId.value && lastSessionId.value !== 'pre-session') {
111
113
  setActiveSession(lastSessionId.value);
@@ -510,7 +512,7 @@ export const useAgentStore = defineStore('agent', () => {
510
512
  if (currentSession.value) {
511
513
  currentSession.value.messages = currentChat.value?.messages.map((m: any) => ({
512
514
  role: m.role,
513
- text: m.parts.map((p: any) => p.text).join(' '),
515
+ text: m.parts.map((p: IPart) => p.type === 'text' ? p.text : '').join(''),
514
516
  })) || [];
515
517
  sessions.value[currentSession.value.sessionId] = currentSession.value;
516
518
  }
@@ -522,8 +524,10 @@ export const useAgentStore = defineStore('agent', () => {
522
524
  if (!sessions.value[sessionId]) {
523
525
  await fetchSession(sessionId);
524
526
  }
527
+ console.log('Set active session from sessions', sessionId, sessions.value[sessionId]);
525
528
  currentSession.value = sessions.value[sessionId];
526
529
  setCurrentChat(sessionId);
530
+ console.log('Set active session chat', sessionId, currentSession.value);
527
531
  currentChat.value.messages = currentSession.value?.messages.map((m: any) => ({
528
532
  role: m.role,
529
533
  parts:[{
@@ -0,0 +1,109 @@
1
+ <template>
2
+ <button @click="scrollContainer.scrollToBottom(); recalculateScroll();">
3
+ <IconArrowDownOutline
4
+ class="absolute z-10 bottom-32 left-1/2 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
+
10
+ <SessionsHistory
11
+ :class="agentStore.isSessionHistoryOpen ? 'translate-x-0' : '-translate-x-full'"
12
+ />
13
+ <div
14
+ v-if="agentStore.isSessionHistoryOpen"
15
+ @click="agentStore.setSessionHistoryOpen(false)"
16
+ class="absolute bg-black/10 backdrop-blur-md z-10 h-full w-full"
17
+ >
18
+
19
+ </div>
20
+ <CustomAutoScrollContainer
21
+ :enabled="!showScrollToBottomButton"
22
+ class="relative flex flex-col overflow-y-auto translate-x-[-50%] left-1/2"
23
+ ref="scrollContainer"
24
+ :threshold="10"
25
+ behavior="smooth"
26
+ :style="{
27
+ maxWidth: agentStore.isFullScreen ? agentStore.MAX_WIDTH+'rem' : '100%',
28
+ transition: `
29
+ max-width ${agentTransitions.TRANSITION_DURATION}ms ease-in-out,
30
+ transform ${agentTransitions.TRANSITION_DURATION}ms ease-in-out
31
+ `
32
+ }"
33
+ >
34
+
35
+ <div
36
+ v-for="(message, index) in props.messages" :key="message.id"
37
+ class="flex flex-col w-full"
38
+ :class="message.role === 'user' ? 'self-end' : 'self-start'"
39
+ >
40
+ <MessageRenderer :message="message" :isLastMessageInChat="index === props.messages.length - 1"/>
41
+ </div>
42
+ <div
43
+ v-if="props.messages.length === 0"
44
+ class="flex-1 flex flex-col items-center justify-center text-gray-400 tracking-widest text-xl font-medium"
45
+ >
46
+ <p>{{ $t('Start the conversation') }}</p>
47
+ <p class="tracking-normal text-base text">{{ $t('Give any input to begin') }}</p>
48
+ </div>
49
+ </CustomAutoScrollContainer>
50
+ </template>
51
+
52
+
53
+ <script setup lang="ts">
54
+ import Message from './Message.vue';
55
+ import type { IMessage, IPart } from '../types';
56
+ import { useTemplateRef, ref, defineAsyncComponent, onMounted, onUnmounted, watch, computed } from 'vue';
57
+ import { IconArrowDownOutline } from '@iconify-prerendered/vue-flowbite';
58
+ import SessionsHistory from '../SessionsHistory.vue';
59
+ import { useAgentStore } from '../composables/useAgentStore';
60
+ import ToolsGroup from './ToolsGroup.vue';
61
+ import { useAgentTransitions } from '../composables/useAgentTransitions';
62
+ import { getMessageParts } from '../utils';
63
+ import MessageRenderer from './MessageRenderer.vue';
64
+ import CustomAutoScrollContainer from '../CustomAutoScrollContainer.vue';
65
+
66
+ const scrollContainer = useTemplateRef('scrollContainer');
67
+ const showScrollToBottomButton = ref(false);
68
+ const innerScrollContainerRef = ref(null);
69
+ const agentStore = useAgentStore();
70
+ const agentTransitions = useAgentTransitions();
71
+ const clicks = ref(0);
72
+
73
+ function recalculateScroll() {
74
+ if (scrollContainer.value) {
75
+ const isScrolledUp = scrollContainer.value.isUserScrolledUp();
76
+ showScrollToBottomButton.value = !!isScrolledUp;
77
+ }
78
+ }
79
+
80
+ onMounted(async () => {
81
+ await import('@incremark/theme/styles.css')
82
+ await agentStore.fetchPlaceholderMessages()
83
+ });
84
+
85
+ onUnmounted(() => {
86
+ agentStore.stopPlaceholderAnimation();
87
+ });
88
+
89
+ watch(scrollContainer, () => {
90
+ if (scrollContainer.value) {
91
+ innerScrollContainerRef.value = scrollContainer.value.container;
92
+
93
+ innerScrollContainerRef.value.addEventListener('scroll', () => {
94
+ recalculateScroll();
95
+ });
96
+ }
97
+ })
98
+
99
+ watch(clicks, () => {
100
+ recalculateScroll();
101
+ })
102
+
103
+
104
+
105
+ const props = defineProps<{
106
+ messages: IMessage[]
107
+ }>();
108
+
109
+ </script>
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <ProcessingTimeline
3
+ :message="message"
4
+ :isLastMessageInChat="isLastMessageInChat"
5
+ />
6
+ <template
7
+ v-for="(part, index) in getMessageParts(message)"
8
+ :key="part.type"
9
+ >
10
+
11
+ <TextRenderer
12
+ v-if="part.type === 'text'"
13
+ :message="part.text"
14
+ :role="props.message.role"
15
+ />
16
+ </template>
17
+
18
+ </template>
19
+
20
+
21
+
22
+
23
+ <script setup lang="ts">
24
+ import TextRenderer from './TextRenderer.vue';
25
+ import type { IMessage } from '../types';
26
+ import { getMessageParts } from '../utils';
27
+ import ProcessingTimeline from './ProcessingTimeline.vue';
28
+
29
+ const props = defineProps<{
30
+ message: IMessage
31
+ isLastMessageInChat: boolean
32
+ }>();
33
+ </script>
@@ -0,0 +1,190 @@
1
+ <template>
2
+ <template v-if="ToolOrReasoningParts.length > 0 || isResponseInProgress || showFakeThinkingMessage">
3
+ <div
4
+ class="ml-2 px-4 flex items-center gap-1 cursor-pointer select-none hover:opacity-80 tracking-wide font-medium text-sm"
5
+ @click="isExpanded = !isExpanded"
6
+ >
7
+ Thoughts
8
+ <span v-if="thinkingDuration > 0">({{ (thinkingDuration/1000).toFixed(2) }} s)</span>
9
+ <ThreeDotsAnimation v-if="isResponseInProgress || showFakeThinkingMessage" />
10
+ <IconAngleDownOutline
11
+ :class="isExpanded ? 'rotate-180' : 'rotate-0'"
12
+ class="transition-transform duration-200"
13
+ />
14
+ </div>
15
+ <transition name="expand" class="max-h-96 overflow-y-auto mb-4 pt-1">
16
+ <CustomAutoScrollContainer
17
+ :enabled="true"
18
+ behavior="smooth"
19
+ v-if="ToolOrReasoningParts.length > 0"
20
+ v-show="isExpanded"
21
+ class="mask-y"
22
+ >
23
+ <ol class="ml-8 relative border-l border-l-2 border-black border-default">
24
+ <li class="mb-6 ms-2 z-50" v-for="(part, index) in ToolOrReasoningParts" :key="index">
25
+ <ReasoningRenderer v-if="part.type === 'reasoning'" :state="part.state" :text="part.text" />
26
+ <ToolsGroup v-else :toolGroup="groupToolCallParts(message, part)" />
27
+ </li>
28
+ </ol>
29
+ </CustomAutoScrollContainer>
30
+ </transition>
31
+ </template>
32
+ </template>
33
+
34
+
35
+
36
+ <script setup lang="ts">
37
+ import type { IFormattedToolCallPart, IMessage, IPart, IToolGroup } from '../types';
38
+ import { ref, computed, watch, defineAsyncComponent, onMounted } from 'vue';
39
+ import ReasoningRenderer from './ReasoningRenderer.vue';
40
+ import { IconAngleDownOutline } from '@iconify-prerendered/vue-flowbite';
41
+ import ThreeDotsAnimation from './ThreeDotsAnimation.vue';
42
+ import { useAgentStore } from '../composables/useAgentStore';
43
+ import { getMessageParts } from '../utils';
44
+ import ToolsGroup from './ToolsGroup.vue';
45
+ import CustomAutoScrollContainer from '../CustomAutoScrollContainer.vue';
46
+
47
+ const props = defineProps<{
48
+ message: IMessage
49
+ isLastMessageInChat: boolean
50
+ }>()
51
+
52
+ // const AutoScrollContainer = defineAsyncComponent(() => import('@incremark/vue').then(module => module.AutoScrollContainer))
53
+ const agentStore = useAgentStore();
54
+ const thinkingStartTime = ref<number | null>(null);
55
+ const thinkingDuration = ref(0);
56
+
57
+ onMounted(() => {
58
+ thinkingStartTime.value = Date.now();
59
+ })
60
+
61
+ const ToolOrReasoningParts = computed(() => {
62
+ return props.message.parts.filter((part: IPart) => part.type === 'data-tool-call' || part.type === 'reasoning');
63
+ });
64
+ const isExpanded = ref(true);
65
+
66
+ const isResponseInProgress = computed(() =>{
67
+ return props.isLastMessageInChat && agentStore.isResponseInProgress;
68
+ });
69
+
70
+ watch(isResponseInProgress, (newValue: boolean) => {
71
+ if (!newValue) {
72
+ isExpanded.value = false;
73
+ thinkingDuration.value = Date.now() - (thinkingStartTime.value ?? Date.now());
74
+ }
75
+ });
76
+
77
+ const showFakeThinkingMessage = computed(() => {
78
+ if (props.message.parts.length === 0) return true;
79
+ return false;
80
+ })
81
+
82
+ const formatToolCallPart = (part: IPart, currentMessage: IMessage): IFormattedToolCallPart | null => {
83
+ if (part.type !== 'data-tool-call' || part.data?.phase !== 'start') {
84
+ return null;
85
+ }
86
+
87
+ const finishedPart = currentMessage.parts.find(candidate => {
88
+ return candidate.type === 'data-tool-call'
89
+ && candidate.data?.toolCallId === part.data?.toolCallId
90
+ && candidate.data?.phase === 'end';
91
+ });
92
+
93
+ return {
94
+ type: 'data-tool-call',
95
+ toolInfo: {
96
+ toolCallId: part.data.toolCallId,
97
+ toolName: part.data.toolName,
98
+ phase: finishedPart ? 'end' : 'start',
99
+ durationMs: finishedPart?.data?.durationMs,
100
+ input: part.data.input,
101
+ output: finishedPart?.data?.output,
102
+ }
103
+ };
104
+ };
105
+
106
+ const getVisibleTimelineParts = (message: IMessage) => {
107
+ return getMessageParts(message).filter(part => {
108
+ return part.type === 'reasoning' || (part.type === 'data-tool-call' && part.data?.phase === 'start');
109
+ });
110
+ };
111
+
112
+ const groupToolCallParts = (message: IMessage, currentPart: IPart): IToolGroup[] => {
113
+ if (currentPart.type !== 'data-tool-call') {
114
+ return [];
115
+ }
116
+
117
+ const visibleParts = getVisibleTimelineParts(message);
118
+ const currentPartIndex = visibleParts.findIndex(part => part === currentPart);
119
+
120
+ if (currentPartIndex === -1) {
121
+ return [];
122
+ }
123
+
124
+ if (currentPartIndex > 0 && visibleParts[currentPartIndex - 1]?.type === 'data-tool-call') {
125
+ return [];
126
+ }
127
+
128
+ const groupedParts: IToolGroup[] = [];
129
+
130
+ for (let index = currentPartIndex; index < visibleParts.length; index += 1) {
131
+ const part = visibleParts[index];
132
+
133
+ if (part.type === 'reasoning') {
134
+ break;
135
+ }
136
+
137
+ const formattedPart = formatToolCallPart(part, message);
138
+
139
+ if (!formattedPart) {
140
+ continue;
141
+ }
142
+
143
+ const lastGroup = groupedParts[groupedParts.length - 1];
144
+
145
+ if (lastGroup?.title === formattedPart.toolInfo.toolName) {
146
+ lastGroup.groupedTools.push(formattedPart);
147
+ continue;
148
+ }
149
+
150
+ groupedParts.push({
151
+ title: formattedPart.toolInfo.toolName,
152
+ groupedTools: [formattedPart],
153
+ });
154
+ }
155
+
156
+ return groupedParts;
157
+ };
158
+
159
+
160
+ </script>
161
+
162
+ <style scoped>
163
+ .expand-enter-active,
164
+ .expand-leave-active {
165
+ transition: all 0.3s ease;
166
+ }
167
+
168
+ .expand-enter-from,
169
+ .expand-leave-to {
170
+ opacity: 0;
171
+ max-height: 0;
172
+ }
173
+
174
+ .expand-enter-to,
175
+ .expand-leave-from {
176
+ opacity: 1;
177
+ max-height: 384px;
178
+ }
179
+
180
+ .mask-y {
181
+ mask-image: linear-gradient(
182
+ to bottom,
183
+ transparent,
184
+ black 20px,
185
+ black calc(100% - 20px),
186
+ transparent
187
+ );
188
+ }
189
+
190
+ </style>
@@ -0,0 +1,87 @@
1
+ <template>
2
+ <span class="bg-lightNavbar absolute flex items-center justify-center w-5 h-5 bg-brand-softer rounded-full -start-[0.68rem] ring-4 ring-lightNavbar ring-default">
3
+ <div class="w-5 h-5 rounded-full flex items-center justify-center">
4
+ <IconBrainOutline class="w-4 h-4" />
5
+ </div>
6
+ </span>
7
+ <h3
8
+ class="flex items-center mb-1 text-sm my-2 ml-3 gap-1 cursor-pointer select-none hover:opacity-80"
9
+ @click="isExpanded = !isExpanded"
10
+ >
11
+ <span class="font-semibold">{{ reasoningTitle }}</span>
12
+ <ThreeDotsAnimation v-if="isStreaming"/>
13
+ <IconAngleDownOutline
14
+ :class="isExpanded ? 'rotate-180' : 'rotate-0'"
15
+ class="transition-transform duration-200"
16
+ />
17
+ </h3>
18
+ <transition name="expand">
19
+ <div v-show="isExpanded" class="overflow-hidden mb-4 text-sm mr-48 max-h-64 pl-4 ">
20
+ <AutoScrollContainer
21
+ :enabled="true"
22
+ >
23
+ <IncremarkContent
24
+ :content="reasoningText"
25
+ />
26
+ </AutoScrollContainer>
27
+ </div>
28
+ </transition>
29
+ </template>
30
+
31
+
32
+
33
+ <script setup lang="ts">
34
+ import { IconBrainOutline, IconAngleDownOutline } from '@iconify-prerendered/vue-flowbite';
35
+ import type { IPart } from '../types';
36
+ import { ref, computed, watch, defineAsyncComponent } from 'vue';
37
+ import ThreeDotsAnimation from './ThreeDotsAnimation.vue';
38
+ import { extractTitleAndTextFromReasoning } from '../utils';
39
+ import { useAgentStore } from '../composables/useAgentStore';
40
+
41
+
42
+ const IncremarkContent = defineAsyncComponent(() => import('@incremark/vue').then(module => module.IncremarkContent))
43
+ const AutoScrollContainer = defineAsyncComponent(() => import('@incremark/vue').then(module => module.AutoScrollContainer))
44
+
45
+ const props = defineProps<{
46
+ state?: IPart['state']
47
+ text?: string
48
+ }>();
49
+
50
+ const agentStore = useAgentStore();
51
+
52
+ const isStreaming = computed(() => props.state === 'streaming');
53
+ const isExpanded = ref(true);
54
+ const parsedReasoning = computed(() => extractTitleAndTextFromReasoning(props.text ?? ''));
55
+ const reasoningTitle = computed(() => parsedReasoning.value.title ?? '');
56
+ const reasoningText = computed(() => parsedReasoning.value.body);
57
+
58
+ watch(() => props.state, (newValue: IPart['state']) => {
59
+ if ( newValue !== 'streaming') {
60
+ isExpanded.value = false;
61
+ }
62
+ });
63
+
64
+
65
+
66
+ </script>
67
+
68
+
69
+ <style scoped>
70
+ .expand-enter-active,
71
+ .expand-leave-active {
72
+ transition: all 0.3s ease;
73
+ }
74
+
75
+ .expand-enter-from,
76
+ .expand-leave-to {
77
+ opacity: 0;
78
+ max-height: 0;
79
+ }
80
+
81
+ .expand-enter-to,
82
+ .expand-leave-from {
83
+ opacity: 1;
84
+ max-height: 256px;
85
+ }
86
+
87
+ </style>