@adminforth/agent 1.21.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 (36) hide show
  1. package/build.log +12 -6
  2. package/custom/ChatSurface.vue +2 -2
  3. package/custom/CustomAutoScrollContainer.vue +127 -0
  4. package/custom/composables/useAgentStore.ts +8 -4
  5. package/custom/conversation_area/ConversationArea.vue +109 -0
  6. package/custom/conversation_area/MessageRenderer.vue +33 -0
  7. package/custom/conversation_area/ProcessingTimeline.vue +190 -0
  8. package/custom/conversation_area/ReasoningRenderer.vue +87 -0
  9. package/{dist/custom/Message.vue → custom/conversation_area/TextRenderer.vue} +14 -102
  10. package/custom/conversation_area/ThreeDotsAnimation.vue +35 -0
  11. package/custom/{ToolRenderer.vue → conversation_area/ToolRenderer.vue} +65 -13
  12. package/custom/conversation_area/ToolsGroup.vue +63 -0
  13. package/custom/package.json +2 -1
  14. package/custom/pnpm-lock.yaml +18 -0
  15. package/custom/types.ts +11 -1
  16. package/custom/utils.ts +29 -0
  17. package/dist/custom/ChatSurface.vue +2 -2
  18. package/dist/custom/CustomAutoScrollContainer.vue +127 -0
  19. package/dist/custom/composables/useAgentStore.ts +8 -4
  20. package/dist/custom/conversation_area/ConversationArea.vue +109 -0
  21. package/dist/custom/conversation_area/MessageRenderer.vue +33 -0
  22. package/dist/custom/conversation_area/ProcessingTimeline.vue +190 -0
  23. package/dist/custom/conversation_area/ReasoningRenderer.vue +87 -0
  24. package/{custom/Message.vue → dist/custom/conversation_area/TextRenderer.vue} +14 -102
  25. package/dist/custom/conversation_area/ThreeDotsAnimation.vue +35 -0
  26. package/dist/custom/{ToolRenderer.vue → conversation_area/ToolRenderer.vue} +65 -13
  27. package/dist/custom/conversation_area/ToolsGroup.vue +63 -0
  28. package/dist/custom/package.json +2 -1
  29. package/dist/custom/pnpm-lock.yaml +18 -0
  30. package/dist/custom/types.ts +11 -1
  31. package/dist/custom/utils.ts +29 -0
  32. package/package.json +1 -1
  33. package/custom/ConversationArea.vue +0 -198
  34. package/custom/ToolsGroup.vue +0 -67
  35. package/dist/custom/ConversationArea.vue +0 -198
  36. package/dist/custom/ToolsGroup.vue +0 -67
@@ -1,52 +1,21 @@
1
1
  <template>
2
- <div
2
+ <div
3
3
  class="max-w-[80%] flex px-4 m-2 rounded-xl border border-gray-200 dark:border-gray-700"
4
4
  @click="handleMarkdownLinkClick"
5
5
  :class="[
6
6
  hasVegaLite ? 'w-full' : '',
7
7
  props.role === 'user' ? 'bg-lightListTableHeading dark:bg-darkListTableHeading self-end'
8
- : isTypeReasoning || isTypeToolCall ? 'bg-transparent border-none self-start'
9
- : 'bg-blue-100 dark:bg-blue-700/10 self-start'
8
+ : 'border-none self-start'
10
9
  ]"
11
10
  >
12
11
  <IncremarkContent
13
12
  class="text-wrap break-words w-full max-w-full"
14
- v-if="content && props.type === 'text'"
13
+ v-if="content"
15
14
  :content="content"
16
15
  :is-finished="isFinished"
17
16
  :components="incremarkComponents"
18
17
  :incremark-options="incremarkOptions"
19
18
  />
20
- <!-- reasoning/thinking -->
21
- <div
22
- v-else-if="isTypeReasoning || isStateStreaming"
23
- class="flex flex-col items-start gap-1 text-gray-500 py-2 "
24
- >
25
- <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">
26
- <IconAngleDownOutline
27
- v-if="content"
28
- :class="isThoughtsExpanded ? 'rotate-180' : 'rotate-0'"
29
- class="transition-transform duration-200"
30
- />
31
- {{ isStateStreaming ? 'Thinking' : 'Thoughts' }}
32
- <template v-if="isStateStreaming">
33
- <span class="bounce-dot1 rounded-full w-2 h-2 bg-lightPrimary"></span>
34
- <span class="bounce-dot2 rounded-full w-2 h-2 bg-lightPrimary"></span>
35
- <span class="bounce-dot3 rounded-full w-2 h-2 bg-lightPrimary"></span>
36
- </template>
37
- </div>
38
- <transition name="expand" class="max-h-36 overflow-y-auto">
39
- <p v-show="isThoughtsExpanded" class="overflow-hidden">
40
- {{ content }}
41
- </p>
42
- </transition>
43
- </div>
44
- <div v-else-if="isTypeToolCall && isToolCallStart">
45
- {{ props.data?.toolName }} start
46
- </div>
47
- <div v-else-if="isTypeToolCall && isToolCallEnd">
48
- {{ props.data?.toolName }} end
49
- </div>
50
19
  <p v-else class="text-red-500 py-2">
51
20
  Error occured
52
21
  </p>
@@ -56,12 +25,11 @@
56
25
  <script setup lang="ts">
57
26
  import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue';
58
27
  import { useRouter } from 'vue-router';
59
- import { IconAngleDownOutline } from '@iconify-prerendered/vue-flowbite';
60
- import { useAgentStore } from './composables/useAgentStore';
28
+ import { useAgentStore } from '../composables/useAgentStore';
61
29
  import { useCoreStore } from '@/stores/core';
62
30
 
63
31
  const IncremarkContent = defineAsyncComponent(() => import('@incremark/vue').then(module => module.IncremarkContent))
64
- const ShikiCodeBlock = defineAsyncComponent(() => import('./incremark_code_renderers/IncremarkShikiCodeBlock.vue'))
32
+ const ShikiCodeBlock = defineAsyncComponent(() => import('../incremark_code_renderers/IncremarkShikiCodeBlock.vue'))
65
33
 
66
34
  const agentStore = useAgentStore();
67
35
  const coreStore = useCoreStore();
@@ -84,10 +52,8 @@
84
52
  })
85
53
 
86
54
  const props = defineProps<{
87
- type: string,
88
55
  message: string | undefined,
89
56
  state: string | undefined,
90
- data?: any
91
57
  role: 'user' | 'assistant'
92
58
  }>();
93
59
 
@@ -95,27 +61,23 @@
95
61
 
96
62
  const content = computed(() => props.message)
97
63
  const isFinished = computed(() => props.state === 'done')
98
- const isThoughtsExpanded = ref(false)
99
- const hasVegaLite = computed(() => props.type === 'text' && props.message.includes('```vega-lite'))
64
+ const isThoughtsExpanded = ref(true);
65
+ const hasVegaLite = computed(() => props.message?.includes('```vega-lite'))
100
66
 
101
- const isTypeReasoning = computed(() => props.type === 'reasoning')
102
- const isTypeToolCall = computed(() => props.type === 'data-tool-call')
103
- const isToolCallStart = computed(() => {
104
- if (props.type !== 'data-tool-call') return false;
105
- return props.data?.phase === 'start';
106
- })
107
- const isToolCallEnd = computed(() => {
108
- if (props.type !== 'data-tool-call') return false;
109
- return props.data?.phase === 'end';
110
- })
111
67
  const isStateStreaming = computed(() => props.state === 'streaming')
112
68
 
69
+ watch(isStateStreaming, (newValue: boolean) => {
70
+ if (!newValue) {
71
+ isThoughtsExpanded.value = false;
72
+ }
73
+ })
74
+
113
75
  watch(isThoughtsExpanded, (newValue: boolean) => {
114
76
  emit('toggle-thoughts', newValue);
115
77
  })
116
78
 
117
79
  function handleMarkdownLinkClick(event: MouseEvent) {
118
- if (props.type !== 'text' || event.defaultPrevented || event.button !== 0) {
80
+ if (event.defaultPrevented || event.button !== 0) {
119
81
  return;
120
82
  }
121
83
  if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
@@ -134,7 +96,6 @@
134
96
  return;
135
97
  }
136
98
  event.preventDefault();
137
-
138
99
  const internalRoute = resolveInternalRoute(href);
139
100
  if (internalRoute !== null) {
140
101
  if (agentStore.isFullScreen && !coreStore.isMobile) {
@@ -170,55 +131,6 @@
170
131
  }
171
132
  </style>
172
133
 
173
- <style scoped>
174
-
175
- .bounce-dot1 {
176
- animation: bounce 1.5s infinite;
177
- animation-delay: 0s;
178
- }
179
-
180
- .bounce-dot2 {
181
- animation: bounce 1.5s infinite;
182
- animation-delay: 0.1s;
183
- }
184
-
185
- .bounce-dot3 {
186
- animation: bounce 1.5s infinite;
187
- animation-delay: 0.2s;
188
- }
189
-
190
- @keyframes bounce {
191
- 0%, 100% {
192
- transform: translateY(20%);
193
- opacity: 0.3;
194
- animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
195
- }
196
- 50% {
197
- transform: none;
198
- opacity: 1;
199
- animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
200
- }
201
- }
202
-
203
- .expand-enter-active,
204
- .expand-leave-active {
205
- transition: all 0.3s ease;
206
- }
207
-
208
- .expand-enter-from,
209
- .expand-leave-to {
210
- opacity: 0;
211
- max-height: 0;
212
- }
213
-
214
- .expand-enter-to,
215
- .expand-leave-from {
216
- opacity: 1;
217
- max-height: 144px;
218
- }
219
-
220
- </style>
221
-
222
134
  <style lang="scss">
223
135
  .incremark a.incremark-link,
224
136
  .incremark a.incremark-link:visited {
@@ -0,0 +1,35 @@
1
+ <template>
2
+ <span class="bounce-dot1 rounded-full w-2 h-2 bg-lightPrimary"></span>
3
+ <span class="bounce-dot2 rounded-full w-2 h-2 bg-lightPrimary"></span>
4
+ <span class="bounce-dot3 rounded-full w-2 h-2 bg-lightPrimary"></span>
5
+ </template>
6
+
7
+ <style scoped>
8
+ .bounce-dot1 {
9
+ animation: bounce 1.5s infinite;
10
+ animation-delay: 0s;
11
+ }
12
+
13
+ .bounce-dot2 {
14
+ animation: bounce 1.5s infinite;
15
+ animation-delay: 0.1s;
16
+ }
17
+
18
+ .bounce-dot3 {
19
+ animation: bounce 1.5s infinite;
20
+ animation-delay: 0.2s;
21
+ }
22
+
23
+ @keyframes bounce {
24
+ 0%, 100% {
25
+ transform: translateY(20%);
26
+ opacity: 0.3;
27
+ animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
28
+ }
29
+ 50% {
30
+ transform: none;
31
+ opacity: 1;
32
+ animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
33
+ }
34
+ }
35
+ </style>
@@ -1,19 +1,34 @@
1
1
  <template>
2
- <div
3
- class="inline-flex m-2 max-w-[80%] flex-col gap-3 rounded-xl px-2 cursor-pointer text-lightListTableHeadingText dark:text-darkListTableHeadingText hover:opacity-75"
4
- @click="isInputOutputExpanded = !isInputOutputExpanded"
2
+ <div
3
+ ref="toolRendererRef"
4
+ class="py-1 inline-flex justify-center m-2
5
+ flex-col gap-3 rounded-xl px-2 text-lightListTableHeadingText
6
+ dark:text-darkListTableHeadingText select-none
7
+ "
8
+ :class="[
9
+ isInputOutputExpanded ? 'items-start border-none' : '',
10
+ activateShrinkedStyle ? 'border items-center' : '',
11
+ activateFullWidth ? 'w-full' : '',
12
+ ]"
13
+ :style="{
14
+ maxWidth: isAnimatingShrinkFinal ? toolRendererInitialWidth + 'px' : '',
15
+ transition: 'max-width 0.3s ease',
16
+ }"
5
17
  >
6
- <div class="flex items-center gap-3">
18
+ <div
19
+ class="flex items-center gap-1 cursor-pointer"
20
+ @click="toggleInputOutput()"
21
+ >
7
22
  <div class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-white/70 dark:bg-blue-700/20">
8
23
  <Spinner v-if="isRunning" class="h-4 w-4" />
9
24
  <IconCheckOutline v-else class="h-4 w-4 text-lightPrimary dark:text-darkPrimary" />
10
25
  </div>
11
26
 
12
27
  <div class="min-w-0">
13
- <p class="text-xs text-gray-500 dark:text-gray-400 font-bold">
28
+ <!-- <p class="text-xs text-gray-500 dark:text-gray-400 font-bold">
14
29
  {{ statusLabel }}
15
30
  <span v-if="props.data?.toolInfo?.durationMs" class="text-xs">({{ (props.data.toolInfo.durationMs / 1000).toFixed(2) }}s)</span>
16
- </p>
31
+ </p> -->
17
32
  <p class="break-all font-mono text-sm leading-5">
18
33
  {{ props.data?.toolInfo?.toolName }}
19
34
  </p>
@@ -47,12 +62,52 @@
47
62
  </template>
48
63
 
49
64
  <script setup lang="ts">
50
- import { computed, ref } from 'vue';
51
- import { type IPartData } from './types';
65
+ import { computed, ref, watch, onMounted } from 'vue';
66
+ import { type IFormattedToolCallPart } from '../types';
52
67
  import { Spinner } from '@/afcl';
53
68
  import { IconAngleDownOutline, IconCheckOutline } from '@iconify-prerendered/vue-flowbite';
54
69
 
55
70
  const isInputOutputExpanded = ref(false);
71
+ const activateShrinkedStyle = ref(true);
72
+ const isAnimatingShrinkFinal = ref(false);
73
+ const toolRendererInitialWidth = ref<number | null>(null);
74
+ const toolRendererRef = ref<HTMLElement | null>(null);
75
+ const activateFullWidth = ref(false);
76
+ const blockClicksDuringAnimation = ref(false);
77
+ const ANIMATION_DURATION = 300;
78
+
79
+ onMounted(() => {
80
+ if (toolRendererRef.value) {
81
+ toolRendererInitialWidth.value = toolRendererRef.value.offsetWidth;
82
+ }
83
+ });
84
+
85
+ watch(isInputOutputExpanded, (newValue) => {
86
+ if (!newValue) {
87
+ setTimeout(() => {
88
+ activateFullWidth.value = false;
89
+ isAnimatingShrinkFinal.value = true;
90
+ }, ANIMATION_DURATION - 10)
91
+ setTimeout(() => {
92
+ isAnimatingShrinkFinal.value = false;
93
+ }, ANIMATION_DURATION);
94
+ setTimeout(() => {
95
+ activateShrinkedStyle.value = true;
96
+ }, ANIMATION_DURATION + 10);
97
+ } else {
98
+ activateShrinkedStyle.value = false;
99
+ activateFullWidth.value = true;
100
+ }
101
+ });
102
+
103
+ function toggleInputOutput() {
104
+ if (blockClicksDuringAnimation.value) return;
105
+ isInputOutputExpanded.value = !isInputOutputExpanded.value;
106
+ blockClicksDuringAnimation.value = true;
107
+ setTimeout(() => {
108
+ blockClicksDuringAnimation.value = false;
109
+ }, ANIMATION_DURATION);
110
+ }
56
111
 
57
112
  interface IToolSection {
58
113
  label: string;
@@ -63,10 +118,7 @@
63
118
  }
64
119
 
65
120
  const props = defineProps<{
66
- data: {
67
- type: string;
68
- toolInfo: IPartData;
69
- }
121
+ data: IFormattedToolCallPart
70
122
  }>();
71
123
 
72
124
  const isRunning = computed(() => props.data?.toolInfo?.phase === 'start');
@@ -115,7 +167,7 @@
115
167
 
116
168
  .expand-enter-active,
117
169
  .expand-leave-active {
118
- transition: all 0.3s ease;
170
+ transition: all 300ms ease;
119
171
  }
120
172
 
121
173
  .expand-enter-from,
@@ -0,0 +1,63 @@
1
+ <template >
2
+ <template v-if="toolGroup.length > 0">
3
+ <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">
4
+ <div class="w-5 h-5 rounded-full flex items-center justify-center">
5
+ <IconWrenchSolid class="w-4 h-4" />
6
+ </div>
7
+ </span>
8
+ <h3
9
+ class="flex items-center mb-1 text-sm my-2 ml-3 gap-1"
10
+ >
11
+ <span class="font-semibold select-none ">Call tools</span>
12
+ </h3>
13
+ <div class="flex flex-wrap">
14
+ <template v-for="group in props.toolGroup" :key="group.title">
15
+ <ToolRenderer v-for="part in group.groupedTools" :key="part.toolInfo.toolCallId" :data="part"/>
16
+ </template>
17
+ </div>
18
+ </template>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ import ToolRenderer from './ToolRenderer.vue';
23
+ import type { IToolGroup } from '../types';
24
+ import { ref } from 'vue';
25
+ import { IconWrenchSolid } from '@iconify-prerendered/vue-heroicons';
26
+
27
+
28
+ const props = defineProps<{
29
+ toolGroup: IToolGroup[]
30
+ }>();
31
+
32
+ const expandedGroups = ref<string[]>([]);
33
+
34
+ function toggleGroup(groupTitle: string) {
35
+ if (expandedGroups.value.includes(groupTitle)) {
36
+ expandedGroups.value = expandedGroups.value.filter((title: string) => title !== groupTitle);
37
+ } else {
38
+ expandedGroups.value.push(groupTitle);
39
+ }
40
+ }
41
+
42
+ </script>
43
+
44
+ <style scoped>
45
+
46
+ .expand-enter-active,
47
+ .expand-leave-active {
48
+ transition: all 0.3s ease;
49
+ }
50
+
51
+ .expand-enter-from,
52
+ .expand-leave-to {
53
+ opacity: 0;
54
+ max-height: 0;
55
+ }
56
+
57
+ .expand-enter-to,
58
+ .expand-leave-from {
59
+ opacity: 1;
60
+ max-height: 288px;
61
+ }
62
+
63
+ </style>
@@ -21,6 +21,7 @@
21
21
  "dompurify": "^3.3.3",
22
22
  "katex": "^0.16.45",
23
23
  "marked": "^18.0.0",
24
- "vega-embed": "^7.1.0"
24
+ "vega-embed": "^7.1.0",
25
+ "vue-custom-scrollbar": "^2.0.2"
25
26
  }
26
27
  }
@@ -41,6 +41,9 @@ importers:
41
41
  vega-embed:
42
42
  specifier: ^7.1.0
43
43
  version: 7.1.0(vega-lite@6.4.2(vega@6.2.0))(vega@6.2.0)
44
+ vue-custom-scrollbar:
45
+ specifier: ^2.0.2
46
+ version: 2.0.2(vue@3.5.32)
44
47
 
45
48
  packages:
46
49
 
@@ -653,6 +656,9 @@ packages:
653
656
  parse-entities@4.0.2:
654
657
  resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
655
658
 
659
+ perfect-scrollbar@1.5.6:
660
+ resolution: {integrity: sha512-rixgxw3SxyJbCaSpo1n35A/fwI1r2rdwMKOTCg/AcG+xOEyZcE8UHVjpZMFCVImzsFoCZeJTT+M/rdEIQYO2nw==}
661
+
656
662
  picocolors@1.1.1:
657
663
  resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
658
664
 
@@ -873,6 +879,11 @@ packages:
873
879
  vfile@6.0.3:
874
880
  resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
875
881
 
882
+ vue-custom-scrollbar@2.0.2:
883
+ resolution: {integrity: sha512-eRyxGb7UFLLH8P0B8FDux2uPrzNBH0X6IN+A/RB5sfmLq1ym7shbCPVKua1pC7LPqPB7dc86evFXyWO/svrfKA==}
884
+ peerDependencies:
885
+ vue: ^3.3.0
886
+
876
887
  vue@3.5.32:
877
888
  resolution: {integrity: sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==}
878
889
  peerDependencies:
@@ -1755,6 +1766,8 @@ snapshots:
1755
1766
  is-decimal: 2.0.1
1756
1767
  is-hexadecimal: 2.0.1
1757
1768
 
1769
+ perfect-scrollbar@1.5.6: {}
1770
+
1758
1771
  picocolors@1.1.1: {}
1759
1772
 
1760
1773
  postcss@8.5.10:
@@ -2129,6 +2142,11 @@ snapshots:
2129
2142
  '@types/unist': 3.0.3
2130
2143
  vfile-message: 4.0.3
2131
2144
 
2145
+ vue-custom-scrollbar@2.0.2(vue@3.5.32):
2146
+ dependencies:
2147
+ perfect-scrollbar: 1.5.6
2148
+ vue: 3.5.32
2149
+
2132
2150
  vue@3.5.32:
2133
2151
  dependencies:
2134
2152
  '@vue/compiler-dom': 3.5.32
package/custom/types.ts CHANGED
@@ -7,12 +7,22 @@ export interface IPartData {
7
7
  durationMs?: number;
8
8
  }
9
9
  export interface IPart {
10
- type: string;
10
+ type: 'reasoning' | 'data-tool-call' | 'text';
11
11
  text?: string;
12
12
  state?: 'started' | 'thinking' | 'processing' | 'streaming' | 'done';
13
13
  data?: IPartData;
14
14
  }
15
15
 
16
+ export interface IFormattedToolCallPart {
17
+ type: 'data-tool-call';
18
+ toolInfo: IPartData;
19
+ }
20
+
21
+ export interface IToolGroup {
22
+ title: string;
23
+ groupedTools: IFormattedToolCallPart[];
24
+ }
25
+
16
26
  export interface IMessage {
17
27
  id: string;
18
28
  role: 'user' | 'assistant';
package/custom/utils.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { IMessage, IPart } from "./types";
2
+
1
3
  export function remToPx(rem: number): number {
2
4
  const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
3
5
  return rem * rootFontSize;
@@ -7,3 +9,30 @@ export function pxToRem(px: number): number {
7
9
  const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
8
10
  return px / rootFontSize;
9
11
  }
12
+
13
+
14
+ export function getMessageParts(message: IMessage): IPart[] {
15
+ return message.parts?.length
16
+ ? message.parts
17
+ : [{ text: '', type: 'reasoning', state: 'streaming' }];
18
+ }
19
+
20
+ function addNewLineBeforeTitles(text: string): string {
21
+ return text.replace(/(\*\*[^*]+\*\*)/g, '\n\n$1');
22
+ }
23
+
24
+ export function extractTitleAndTextFromReasoning(reasoningText: string): { title: string | null; body: string } {
25
+ const match = reasoningText.match(/^\*\*(.*?)\*\*(.*)$/s);
26
+
27
+ if (!match) {
28
+ return {
29
+ title: null,
30
+ body: addNewLineBeforeTitles(reasoningText)
31
+ };
32
+ }
33
+
34
+ return {
35
+ title: match[1].trim(),
36
+ body: addNewLineBeforeTitles(match[2].trim())
37
+ };
38
+ }
@@ -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>