@adminforth/agent 1.21.0 → 1.22.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.
Files changed (38) hide show
  1. package/build.log +12 -6
  2. package/custom/ChatSurface.vue +3 -2
  3. package/custom/CustomAutoScrollContainer.vue +127 -0
  4. package/custom/SessionsHistory.vue +11 -2
  5. package/custom/composables/useAgentStore.ts +6 -4
  6. package/custom/conversation_area/ConversationArea.vue +106 -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/types.ts +11 -1
  17. package/custom/utils.ts +29 -0
  18. package/dist/custom/ChatSurface.vue +3 -2
  19. package/dist/custom/CustomAutoScrollContainer.vue +127 -0
  20. package/dist/custom/SessionsHistory.vue +11 -2
  21. package/dist/custom/composables/useAgentStore.ts +6 -4
  22. package/dist/custom/conversation_area/ConversationArea.vue +106 -0
  23. package/dist/custom/conversation_area/MessageRenderer.vue +33 -0
  24. package/dist/custom/conversation_area/ProcessingTimeline.vue +190 -0
  25. package/dist/custom/conversation_area/ReasoningRenderer.vue +87 -0
  26. package/{custom/Message.vue → dist/custom/conversation_area/TextRenderer.vue} +14 -102
  27. package/dist/custom/conversation_area/ThreeDotsAnimation.vue +35 -0
  28. package/dist/custom/{ToolRenderer.vue → conversation_area/ToolRenderer.vue} +65 -13
  29. package/dist/custom/conversation_area/ToolsGroup.vue +63 -0
  30. package/dist/custom/package.json +2 -1
  31. package/dist/custom/pnpm-lock.yaml +18 -0
  32. package/dist/custom/types.ts +11 -1
  33. package/dist/custom/utils.ts +29 -0
  34. package/package.json +1 -1
  35. package/custom/ConversationArea.vue +0 -198
  36. package/custom/ToolsGroup.vue +0 -67
  37. package/dist/custom/ConversationArea.vue +0 -198
  38. package/dist/custom/ToolsGroup.vue +0 -67
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,784 bytes received 451 bytes 378,470.00 bytes/sec
36
- total size is 186,932 speedup is 0.99
41
+ sent 199,157 bytes received 562 bytes 399,438.00 bytes/sec
42
+ total size is 196,887 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';
@@ -195,6 +195,7 @@ const props = defineProps<{
195
195
  defaultModeName: string | null;
196
196
  stickByDefault: boolean;
197
197
  }
198
+ adminUser: any
198
199
  }>();
199
200
 
200
201
  const chatSurface = useTemplateRef('chatSurface');
@@ -246,7 +247,7 @@ onMounted(async () => {
246
247
  agentStore.setAvailableModes(props.meta.modes, props.meta.defaultModeName);
247
248
  agentStore.regisrerTextInput(textInput.value);
248
249
  textInput.value?.focus();
249
- const isTeleportedToBodyFromLocalStorage = agentStore.getLocalStorageItem('isTeleportedToBody') === 'true';
250
+ const isTeleportedToBodyFromLocalStorage = agentStore.getLocalStorageItem('isTeleportedToBody') === 'true' || agentStore.getLocalStorageItem('isTeleportedToBodyBeforeFullScreen') === 'true';
250
251
  if( coreStore.isMobile ) {
251
252
  agentStore.setIsTeleportedToBody(false);
252
253
  } 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>
@@ -8,7 +8,7 @@
8
8
  <h3 :class="h3Style">{{ $t('Chat history') }}</h3>
9
9
  <div class="w-full flex items-center justify-center">
10
10
  <Button
11
- @click="agentStore.createPreSession(); agentStore.setSessionHistoryOpen(false); agentStore.focusTextInput();"
11
+ @click="agentStore.createPreSession(); agentStore.setSessionHistoryOpen(false); agentStore.focusTextInput(); recalculateScroll();"
12
12
  :disabled="agentStore.isResponseInProgress"
13
13
  class="w-[90%] my-2 mb-4 rounded-3xl text-gray-800 dark:text-gray-200"
14
14
  >
@@ -34,7 +34,7 @@
34
34
  'bg-lightPrimary/20 hover:bg-lightPrimary/20 dark:bg-darkPrimary/20 dark:hover:bg-darkPrimary/20': agentStore.activeSessionId === session.sessionId,
35
35
  'cursor-default opacity-50 pointer-events-none': agentStore.isResponseInProgress,
36
36
  }"
37
- @click="agentStore.setActiveSession(session.sessionId); agentStore.setSessionHistoryOpen(false);"
37
+ @click="agentStore.setActiveSession(session.sessionId); agentStore.setSessionHistoryOpen(false); recalculateScroll();"
38
38
  :disabled="agentStore.isResponseInProgress"
39
39
  >
40
40
  <p class="truncate">{{ session.title || session.sessionId }}</p>
@@ -99,4 +99,13 @@ const groupedSessions = computed(() => {
99
99
  return Array.from(groups.values());
100
100
  });
101
101
 
102
+ const emit = defineEmits<{
103
+ (e: 'recalculateScroll'): void
104
+ }>()
105
+
106
+ function recalculateScroll() {
107
+ // Emit an event to notify the parent component to recalculate scroll
108
+ emit('recalculateScroll');
109
+ }
110
+
102
111
  </script>
@@ -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
  }
@@ -0,0 +1,106 @@
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
+ @recalculateScroll="recalculateScroll"
13
+ />
14
+ <div
15
+ v-if="agentStore.isSessionHistoryOpen"
16
+ @click="agentStore.setSessionHistoryOpen(false)"
17
+ class="absolute bg-black/10 backdrop-blur-md z-10 h-full w-full"
18
+ >
19
+
20
+ </div>
21
+ <CustomAutoScrollContainer
22
+ :enabled="!showScrollToBottomButton"
23
+ class="relative flex flex-col overflow-y-auto translate-x-[-50%] left-1/2"
24
+ ref="scrollContainer"
25
+ :threshold="10"
26
+ behavior="smooth"
27
+ :style="{
28
+ maxWidth: agentStore.isFullScreen ? agentStore.MAX_WIDTH+'rem' : '100%',
29
+ transition: `
30
+ max-width ${agentTransitions.TRANSITION_DURATION}ms ease-in-out,
31
+ transform ${agentTransitions.TRANSITION_DURATION}ms ease-in-out
32
+ `
33
+ }"
34
+ >
35
+
36
+ <div
37
+ v-for="(message, index) in props.messages" :key="message.id"
38
+ class="flex flex-col w-full"
39
+ :class="message.role === 'user' ? 'self-end' : 'self-start'"
40
+ >
41
+ <MessageRenderer :message="message" :isLastMessageInChat="index === props.messages.length - 1"/>
42
+ </div>
43
+ <div
44
+ v-if="props.messages.length === 0"
45
+ class="flex-1 flex flex-col items-center justify-center text-gray-400 tracking-widest text-xl font-medium"
46
+ >
47
+ <p>{{ $t('Start the conversation') }}</p>
48
+ <p class="tracking-normal text-base text">{{ $t('Give any input to begin') }}</p>
49
+ </div>
50
+ </CustomAutoScrollContainer>
51
+ </template>
52
+
53
+
54
+ <script setup lang="ts">
55
+ import type { IMessage, IPart } from '../types';
56
+ import { useTemplateRef, ref, defineAsyncComponent, onMounted, onUnmounted, watch, computed, nextTick } from 'vue';
57
+ import { IconArrowDownOutline } from '@iconify-prerendered/vue-flowbite';
58
+ import SessionsHistory from '../SessionsHistory.vue';
59
+ import { useAgentStore } from '../composables/useAgentStore';
60
+ import { useAgentTransitions } from '../composables/useAgentTransitions';
61
+ import MessageRenderer from './MessageRenderer.vue';
62
+ import CustomAutoScrollContainer from '../CustomAutoScrollContainer.vue';
63
+
64
+ const props = defineProps<{
65
+ messages: IMessage[]
66
+ }>();
67
+
68
+ const scrollContainer = useTemplateRef('scrollContainer');
69
+ const showScrollToBottomButton = ref(false);
70
+ const innerScrollContainerRef = ref(null);
71
+ const agentStore = useAgentStore();
72
+ const agentTransitions = useAgentTransitions();
73
+ const clicks = ref(0);
74
+
75
+ function recalculateScroll() {
76
+ if (scrollContainer.value) {
77
+ const isScrolledUp = scrollContainer.value.isUserScrolledUp();
78
+ showScrollToBottomButton.value = !!isScrolledUp;
79
+ }
80
+ }
81
+
82
+ onMounted(async () => {
83
+ await import('@incremark/theme/styles.css')
84
+ await agentStore.fetchPlaceholderMessages()
85
+ });
86
+
87
+ onUnmounted(() => {
88
+ agentStore.stopPlaceholderAnimation();
89
+ });
90
+
91
+ watch(scrollContainer, () => {
92
+ if (scrollContainer.value) {
93
+ innerScrollContainerRef.value = scrollContainer.value.container;
94
+
95
+ innerScrollContainerRef.value.addEventListener('scroll', () => {
96
+ recalculateScroll();
97
+ });
98
+ }
99
+ })
100
+
101
+ watch(clicks, () => {
102
+ recalculateScroll();
103
+ })
104
+
105
+
106
+ </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>