@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,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
@@ -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';
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/agent",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -1,198 +0,0 @@
1
- <template>
2
- <button @click="scrollContainer.scrollToBottom()">
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
- <AutoScrollContainer
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 in props.messages" :key="message.id"
37
- class="flex flex-col w-full"
38
- :class="message.role === 'user' ? 'self-end' : 'self-start'"
39
- >
40
- <template
41
- v-for="(part, index) in getParts(message)"
42
- :key="part.type"
43
- >
44
- <Message
45
- v-if="part.type !== 'data-tool-call'"
46
- :message="part.text"
47
- :role="message.role"
48
- :type="part.type"
49
- :state="part.state"
50
- :data="part.data"
51
- @toggle-thoughts="() => clicks++"
52
- >
53
- </Message>
54
- <ToolsGroup v-else :toolGroup="groupToolCallParts(message, index, part)" />
55
- </template>
56
- </div>
57
- <!-- Show a placeholder message if the last message is not of type 'text' or 'reasoning' -->
58
- <Message
59
- v-if="props.messages.length > 0 && showFakeThinkingMessage"
60
- :message="''"
61
- :role="props.messages[props.messages.length - 1].role"
62
- type="reasoning"
63
- state="streaming"
64
- />
65
- <div
66
- v-if="props.messages.length === 0"
67
- class="flex-1 flex flex-col items-center justify-center text-gray-400 tracking-widest text-xl font-medium"
68
- >
69
- <p>{{ $t('Start the conversation') }}</p>
70
- <p class="tracking-normal text-base text">{{ $t('Give any input to begin') }}</p>
71
- </div>
72
- </AutoScrollContainer>
73
- </template>
74
-
75
-
76
- <script setup lang="ts">
77
- import Message from './Message.vue';
78
- import type { IMessage, IPart } from './types';
79
- import { useTemplateRef, ref, defineAsyncComponent, onMounted, onUnmounted, watch, computed } from 'vue';
80
- import { IconArrowDownOutline } from '@iconify-prerendered/vue-flowbite';
81
- import SessionsHistory from './SessionsHistory.vue';
82
- import { useAgentStore } from './composables/useAgentStore';
83
- import ToolsGroup from './ToolsGroup.vue';
84
- import { useAgentTransitions } from './composables/useAgentTransitions';
85
-
86
- const scrollContainer = useTemplateRef('scrollContainer');
87
- const showScrollToBottomButton = ref(false);
88
- const innerScrollContainerRef = ref(null);
89
- const AutoScrollContainer = defineAsyncComponent(() => import('@incremark/vue').then(module => module.AutoScrollContainer))
90
- const agentStore = useAgentStore();
91
- const agentTransitions = useAgentTransitions();
92
- const clicks = ref(0);
93
-
94
- function recalculateScroll() {
95
- if (scrollContainer.value) {
96
- const isScrolledUp = scrollContainer.value.isUserScrolledUp();
97
- showScrollToBottomButton.value = !!isScrolledUp;
98
- }
99
- }
100
-
101
- onMounted(async () => {
102
- await import('@incremark/theme/styles.css')
103
- await agentStore.fetchPlaceholderMessages()
104
- });
105
-
106
- onUnmounted(() => {
107
- agentStore.stopPlaceholderAnimation();
108
- });
109
-
110
- watch(scrollContainer, () => {
111
- if (scrollContainer.value) {
112
- innerScrollContainerRef.value = scrollContainer.value.container;
113
-
114
- innerScrollContainerRef.value.addEventListener('scroll', () => {
115
- recalculateScroll();
116
- });
117
- }
118
- })
119
-
120
- watch(clicks, () => {
121
- recalculateScroll();
122
- })
123
-
124
- const showFakeThinkingMessage = computed(() => {
125
- const lastMessage = props.messages[props.messages.length - 1];
126
- if (!lastMessage) return false;
127
- const lastPart = getParts(lastMessage)[getParts(lastMessage).length - 1];
128
- return lastPart?.type !== 'text' && lastPart?.type !== 'reasoning';
129
- })
130
-
131
- const getParts = (message: IMessage) => {
132
- return message.parts?.length
133
- ? message.parts
134
- : [{ text: '', type: 'reasoning', state: 'streaming' }];
135
- };
136
-
137
- const formatToolCallTextPart = ((part: IPart, currentMessage: IMessage) => {
138
- if (part.type === 'data-tool-call') {
139
- if (part.data?.phase === 'start') {
140
- const finishedPart = currentMessage.parts?.find(p => p.type === 'data-tool-call' && p.data?.toolCallId === part.data?.toolCallId && p.data?.phase === 'end');
141
- return {
142
- type: 'text',
143
- toolInfo: {
144
- toolCallId: part.data!.toolCallId,
145
- toolName: part.data!.toolName,
146
- phase: finishedPart ? 'end' : 'start',
147
- durationMs: finishedPart ? finishedPart.data?.durationMs : undefined,
148
- input: part.data!.input,
149
- output: finishedPart ? finishedPart.data!.output : undefined,
150
- }
151
- }
152
- }
153
- }
154
- return null;
155
- });
156
-
157
- const groupToolCallParts = (message: IMessage, currentPartIndex: number, currentPart: IPart) => {
158
- const groupedParts: { title: string; groupedTools: IPart[] }[] = [];
159
- let currentToolName: string | null = null;
160
- const parts = getParts(message);
161
- if (!parts) return [];
162
- const formatedToolParts = parts.map(part => {
163
- return formatToolCallTextPart(part as IPart, message)
164
- });
165
- const currentPartIndexInFormatedParts = formatedToolParts.findIndex(part => part?.toolInfo?.toolCallId === currentPart.data?.toolCallId);
166
- if (currentPartIndexInFormatedParts === -1) {
167
- return [];
168
- }
169
- for( const[index, part] of formatedToolParts.entries()){
170
- if ( index < currentPartIndexInFormatedParts - 1 ) {
171
- continue;
172
- }
173
- if(!part || !part.toolInfo) {
174
- continue;
175
- }
176
- currentToolName = part.toolInfo.toolName;
177
- if (!groupedParts.find(group => group.title === currentToolName)) {
178
- groupedParts.push({
179
- title: currentToolName,
180
- groupedTools: []
181
- })
182
- }
183
- if( formatedToolParts[currentPartIndexInFormatedParts - 1]?.toolInfo.toolName === part.toolInfo.toolName) {
184
- continue;
185
- } else if ( formatedToolParts[currentPartIndexInFormatedParts + 1]?.toolInfo.toolName === part.toolInfo.toolName) {
186
- groupedParts[groupedParts.length - 1].groupedTools.push(formatedToolParts[currentPartIndexInFormatedParts + 1] as IPart);
187
- } else {
188
- groupedParts[groupedParts.length - 1].groupedTools.push(part);
189
- }
190
- }
191
- return groupedParts;
192
- }
193
-
194
- const props = defineProps<{
195
- messages: IMessage[]
196
- }>();
197
-
198
- </script>
@@ -1,67 +0,0 @@
1
- <template>
2
- <template v-for="group in props.toolGroup" :key="group.title">
3
- <div v-if="group.groupedTools.length > 1" class="flex flex-col">
4
- <div class="flex items-center gap-2 px-2 m-2 cursor-pointer hover:opacity-75 break-all font-mono text-sm leading-5 text-lightListTableHeadingText dark:text-darkListTableHeadingText" @click="toggleGroup(group.title)">
5
- <IconCheckOutline class="w-6 h-6 p-1"/>
6
- {{ group.title }} {{ 'x' + group.groupedTools.length }}
7
- <IconAngleDownOutline
8
- class="transition-transform duration-200 hover:scale-105 hover:opacity-75"
9
- :class="expandedGroups.includes(group.title) ? 'rotate-180' : 'rotate-0'"
10
- />
11
- </div>
12
- <transition name="expand">
13
- <div v-show="expandedGroups.includes(group.title)" class="flex flex-col">
14
- <ToolRenderer v-for="part in group.groupedTools" :key="part.text + part.type" :data="part" class="ml-8"/>
15
- </div>
16
- </transition>
17
- </div>
18
- <ToolRenderer v-else-if="group.groupedTools.length > 0" :data="group.groupedTools[0]" />
19
- </template>
20
-
21
- </template>
22
-
23
- <script setup lang="ts">
24
- import ToolRenderer from './ToolRenderer.vue';
25
- import type { IPart } from './types';
26
- import { ref } from 'vue';
27
- import { IconAngleDownOutline, IconCheckOutline } from '@iconify-prerendered/vue-flowbite';
28
-
29
- const props = defineProps<{
30
- toolGroup: {
31
- title: string;
32
- groupedTools: IPart[];
33
- }[]
34
- }>();
35
-
36
- const expandedGroups = ref<string[]>([]);
37
-
38
- function toggleGroup(groupTitle: string) {
39
- if (expandedGroups.value.includes(groupTitle)) {
40
- expandedGroups.value = expandedGroups.value.filter((title: string) => title !== groupTitle);
41
- } else {
42
- expandedGroups.value.push(groupTitle);
43
- }
44
- }
45
-
46
- </script>
47
-
48
- <style scoped>
49
-
50
- .expand-enter-active,
51
- .expand-leave-active {
52
- transition: all 0.3s ease;
53
- }
54
-
55
- .expand-enter-from,
56
- .expand-leave-to {
57
- opacity: 0;
58
- max-height: 0;
59
- }
60
-
61
- .expand-enter-to,
62
- .expand-leave-from {
63
- opacity: 1;
64
- max-height: 288px;
65
- }
66
-
67
- </style>