@adminforth/agent 2.9.0 → 2.10.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.
@@ -26,20 +26,20 @@
26
26
  <button
27
27
  v-if="canCopy"
28
28
  type="button"
29
- class="flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-lightPrimary dark:hover:text-darkPrimary opacity-60 group-hover/msg:opacity-100 transition-opacity duration-150 hover:scale-110"
29
+ :class="ACTION_BUTTON_CLASSES"
30
30
  :title="$t('Copy message')"
31
31
  @click="copyMessageToClipboard"
32
32
  >
33
- <IconFileCopySolid class="w-5 h-5" />
33
+ <IconFileCopyOutline class="w-4 h-4" />
34
34
  </button>
35
35
  <button
36
36
  v-if="canEdit"
37
37
  type="button"
38
- class="flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-lightPrimary dark:hover:text-darkPrimary opacity-60 group-hover/msg:opacity-100 transition-opacity duration-150 hover:scale-110"
38
+ :class="ACTION_BUTTON_CLASSES"
39
39
  :title="$t('Edit message')"
40
40
  @click="agentStore.startEditMessage(message)"
41
41
  >
42
- <IconPenSolid class="w-5 h-5" />
42
+ <IconPenOutline class="w-4 h-4" />
43
43
  </button>
44
44
  </div>
45
45
  </Transition>
@@ -83,7 +83,7 @@
83
83
 
84
84
  <script setup lang="ts">
85
85
  import { computed, ref } from 'vue';
86
- import { IconPenSolid, IconFileCopySolid } from '@iconify-prerendered/vue-flowbite';
86
+ import { IconPenOutline, IconFileCopyOutline } from '@iconify-prerendered/vue-flowbite';
87
87
  import TextRenderer from './TextRenderer.vue';
88
88
  import type { IMessage } from '../types';
89
89
  import { getMessageParts } from '../utils';
@@ -99,6 +99,10 @@
99
99
  isLastMessageInChat: boolean
100
100
  }>();
101
101
 
102
+ const ACTION_BUTTON_CLASSES = `flex items-center justify-center w-7 h-7 rounded-md transition-colors duration-150
103
+ text-gray-500 hover:bg-gray-100 hover:text-gray-800
104
+ dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200`;
105
+
102
106
  const agentStore = useAgentStore();
103
107
  const { alert } = useAdminforth();
104
108
  const showEditButton = ref(false);
@@ -1,58 +1,69 @@
1
1
  <template>
2
- <template v-if="ToolOrReasoningParts.length > 0 || inProgress">
3
- <div
4
- class="shine-text-container ml-2 px-4 flex items-center gap-1 select-none hover:opacity-80 tracking-wide font-medium text-sm text-listTableHeadingText dark:text-darkListTableHeadingText"
5
- :class="[ToolOrReasoningParts.length > 0 ? 'cursor-pointer' : '']"
2
+ <template v-if="timelineParts.length > 0 || inProgress">
3
+ <button
4
+ type="button"
5
+ class="z-20 self-start flex items-center gap-1.5 mx-4 my-1 select-none text-[0.9375rem] font-medium transition-colors duration-150
6
+ text-gray-600 dark:text-gray-400"
7
+ :class="timelineParts.length > 0 ? 'hover:text-gray-900 dark:hover:text-gray-200' : 'cursor-default'"
6
8
  @click="isExpanded = !isExpanded"
7
9
  >
8
- <p
9
- :class="[
10
- inProgress ? `
11
- shine-text text-listTableHeadingText dark:text-darkListTableHeadingText
12
- bg-[linear-gradient(-65deg,currentColor,_rgb(120,120,120)_50%,currentColor)]
13
- dark:bg-[linear-gradient(-65deg,currentColor,_rgb(250,250,250,0.8)_70%,currentColor)]
14
- `
15
- : '']"
16
- >
17
- {{ inProgress ? $t('Thinking') : $t('Thoughts') }}
18
- </p>
19
- <ThreeDotsAnimation v-if="inProgress" />
20
- <span v-if="thinkingDuration > 0">({{ (thinkingDuration/1000).toFixed(2) }} s)</span>
21
- <IconAngleDownOutline
22
- v-if="ToolOrReasoningParts.length > 0"
23
- :class="isExpanded ? 'rotate-180' : 'rotate-0'"
24
- class="transition-transform duration-200"
10
+ <IconChevronRight
11
+ v-if="timelineParts.length > 0"
12
+ class="w-4 h-4 transition-transform duration-200"
13
+ :class="isExpanded ? 'rotate-90' : 'rotate-0'"
25
14
  />
26
- </div>
27
- <transition name="expand" class="sm:max-h-96 max-h-48 overflow-y-auto mb-4 pt-1">
28
- <CustomAutoScrollContainer
29
- ref="scrollContainerRef"
30
- :enabled="isResponseInProgress"
31
- behavior="smooth"
32
- v-if="ToolOrReasoningParts.length > 0"
15
+ <template v-if="inProgress">
16
+ <span
17
+ class="shine-text text-listTableHeadingText dark:text-darkListTableHeadingText
18
+ bg-[linear-gradient(-65deg,currentColor,_rgb(120,120,120)_50%,currentColor)]
19
+ dark:bg-[linear-gradient(-65deg,currentColor,_rgb(250,250,250,0.8)_70%,currentColor)]"
20
+ >
21
+ {{ $t('Thinking') }}
22
+ </span>
23
+ <ThreeDotsAnimation />
24
+ </template>
25
+ <template v-else>
26
+ <span>{{ thinkingSeconds ? $t('Thought for {seconds}s', { seconds: thinkingSeconds }) : $t('Thoughts') }}</span>
27
+ <span v-if="toolCallsCount">
28
+ · {{ toolCallsCount === 1 ? $t('1 tool') : $t('{count} tools', { count: toolCallsCount }) }}
29
+ </span>
30
+ </template>
31
+ </button>
32
+ <transition name="expand">
33
+ <!-- the box is inset from the message edge, so its own scrollbar never sits on top of the conversation scrollbar -->
34
+ <div
35
+ v-if="timelineParts.length > 0"
33
36
  v-show="isExpanded"
34
- :threshold="5"
35
- :wrapperStyle="{
36
- marginRight: '1rem',
37
- }"
38
- :scrollBarAutoHide="false"
37
+ class="z-20 mx-4 mt-1 mb-3 overflow-hidden rounded-xl border
38
+ border-gray-100 bg-gray-50/70 dark:border-gray-700 dark:bg-gray-800/50"
39
39
  >
40
- <ol class="ml-8 my-2 relative border-l border-l-2 border-black border-default border-listTableHeadingText dark:border-darkListTableHeadingText">
41
- <template v-for="(part, index) in ToolOrReasoningParts" :key="index">
42
- <ReasoningRenderer v-if="part.type === 'reasoning'" :state="part.state" :text="part.text" />
43
- <ToolsGroup v-else-if="part.type==='data-tool-call'" :toolGroup="groupToolCallParts(message, part)" :isExpanded="isExpanded" />
44
- <li v-else-if="part.type === 'data-rendering'" class="mb-6 mx-2 mt-2 px-2 z-50 overflow-hidden">
45
- <span class="bg-lightNavbar dark:bg-darkNavbar absolute flex items-center text-listTableHeadingText dark:text-darkListTableHeadingText justify-center w-5 h-5 rounded-full -start-[0.68rem] ring-4 ring-lightNavbar dark:ring-darkNavbar">
46
- <div class="w-2 h-2 rounded-full bg-current animate-pulse"></div>
47
- </span>
48
- <h3 class="flex items-center mb-1 text-sm ml-3 gap-1 text-listTableHeadingText dark:text-darkListTableHeadingText">
49
- <span class="font-semibold">{{ part.data?.label ?? 'Rendering...' }}</span>
40
+ <CustomAutoScrollContainer
41
+ ref="scrollContainerRef"
42
+ class="max-h-48 sm:max-h-96 overflow-y-auto"
43
+ :enabled="isResponseInProgress"
44
+ behavior="smooth"
45
+ :threshold="5"
46
+ :scrollBarAutoHide="false"
47
+ >
48
+ <!-- wider right padding keeps the row chevrons clear of the box's scrollbar -->
49
+ <ul class="flex flex-col gap-1 pl-2.5 pr-5 py-2">
50
+ <template v-for="(part, index) in timelineParts" :key="index">
51
+ <ReasoningRenderer v-if="part.type === 'reasoning'" :state="part.state" :text="part.text" />
52
+ <ToolRenderer v-else-if="part.type === 'data-tool-call'" :data="formatToolCallPart(part)" />
53
+ <li
54
+ v-else-if="part.type === 'data-rendering'"
55
+ class="flex items-center gap-2 py-0.5 text-[0.8125rem] text-gray-700 dark:text-gray-300"
56
+ >
57
+ <span class="flex items-center justify-center w-4 h-4 shrink-0">
58
+ <span class="w-2 h-2 rounded-full bg-current animate-pulse" />
59
+ </span>
60
+ {{ part.data?.label ?? 'Rendering...' }}
50
61
  <ThreeDotsAnimation />
51
- </h3>
52
- </li>
53
- </template>
54
- </ol>
55
- </CustomAutoScrollContainer>
62
+ </li>
63
+ </template>
64
+ </ul>
65
+ </CustomAutoScrollContainer>
66
+ </div>
56
67
  </transition>
57
68
  </template>
58
69
  </template>
@@ -60,14 +71,13 @@
60
71
 
61
72
 
62
73
  <script setup lang="ts">
63
- import type { IFormattedToolCallPart, IMessage, IPart, IToolGroup } from '../types';
74
+ import type { IFormattedToolCallPart, IMessage, IPart } from '../types';
64
75
  import { ref, computed, watch, onUnmounted, onMounted, defineAsyncComponent } from 'vue';
76
+ import { IconChevronRight } from '@iconify-prerendered/vue-heroicons';
65
77
  import ReasoningRenderer from './ReasoningRenderer.vue';
66
- import { IconAngleDownOutline } from '@iconify-prerendered/vue-flowbite';
78
+ import ToolRenderer from './ToolRenderer.vue';
67
79
  import ThreeDotsAnimation from './ThreeDotsAnimation.vue';
68
80
  import { useAgentStore } from '../composables/useAgentStore';
69
- import { getMessageParts } from '../utils';
70
- import ToolsGroup from './ToolsGroup.vue';
71
81
 
72
82
  const CustomAutoScrollContainer = defineAsyncComponent(() => import('../CustomAutoScrollContainer.vue'));
73
83
 
@@ -83,17 +93,23 @@
83
93
  const innerScrollContainerRef = ref<HTMLElement | null>(null);
84
94
  const isExpanded = ref(true);
85
95
  let isUserScrolled = false;
86
- const ToolOrReasoningParts = computed(() => {
96
+ // One step per reasoning block and per tool call (its 'start' part; the matching 'end'
97
+ // part is merged in by formatToolCallPart), plus a rendering step while it is running.
98
+ const timelineParts = computed(() => {
87
99
  return props.message.parts.filter((part: IPart) => {
88
- return part.type === 'data-tool-call'
89
- || part.type === 'reasoning'
100
+ return part.type === 'reasoning'
101
+ || (part.type === 'data-tool-call' && part.data?.phase === 'start')
90
102
  || isActiveRenderingPart(part);
91
103
  });
92
104
  });
105
+ const toolCallsCount = computed(() => timelineParts.value.filter(part => part.type === 'data-tool-call').length);
106
+ const thinkingSeconds = computed(() => (
107
+ thinkingDuration.value > 0 ? Math.max(1, Math.round(thinkingDuration.value / 1000)) : 0
108
+ ));
93
109
  const isResponseInProgress = computed(() =>{
94
- return props.isLastMessageInChat && agentStore.isResponseInProgress;
110
+ return props.isLastMessageInChat && agentStore.isResponseInProgress;
95
111
  });
96
-
112
+
97
113
  const showFakeThinkingMessage = computed(() => {
98
114
  if (props.message.parts.length === 0 && props.isLastMessageInChat) return true;
99
115
  return false;
@@ -141,37 +157,28 @@
141
157
  }
142
158
  });
143
159
 
144
- const formatToolCallPart = (part: IPart, currentMessage: IMessage): IFormattedToolCallPart | null => {
145
- if (part.type !== 'data-tool-call' || part.data?.phase !== 'start') {
146
- return null;
147
- }
148
-
149
- const finishedPart = currentMessage.parts.find(candidate => {
160
+ function formatToolCallPart(part: IPart): IFormattedToolCallPart {
161
+ const data = part.data!;
162
+ const finishedPart = props.message.parts.find(candidate => {
150
163
  return candidate.type === 'data-tool-call'
151
- && candidate.data?.toolCallId === part.data?.toolCallId
164
+ && candidate.data?.toolCallId === data.toolCallId
152
165
  && candidate.data?.phase === 'end';
153
166
  });
154
167
 
155
168
  return {
156
169
  type: 'data-tool-call',
157
170
  toolInfo: {
158
- toolCallId: part.data.toolCallId,
159
- toolName: part.data.toolName,
171
+ toolCallId: data.toolCallId,
172
+ toolName: data.toolName,
160
173
  phase: finishedPart ? 'end' : 'start',
161
174
  durationMs: finishedPart?.data?.durationMs,
162
- input: part.data.input,
175
+ input: data.input,
163
176
  output: finishedPart?.data?.output,
164
- toolInfo: part.data.toolInfo,
177
+ toolInfo: data.toolInfo,
165
178
  error: finishedPart?.data?.error,
166
179
  }
167
180
  };
168
- };
169
-
170
- const getVisibleTimelineParts = (message: IMessage) => {
171
- return getMessageParts(message).filter(part => {
172
- return part.type === 'reasoning' || (part.type === 'data-tool-call' && part.data?.phase === 'start');
173
- });
174
- };
181
+ }
175
182
 
176
183
  function isActiveRenderingPart(part: IPart) {
177
184
  return part.type === 'data-rendering'
@@ -181,55 +188,8 @@
181
188
  });
182
189
  }
183
190
 
184
- const groupToolCallParts = (message: IMessage, currentPart: IPart): IToolGroup[] => {
185
- if (currentPart.type !== 'data-tool-call') {
186
- return [];
187
- }
188
-
189
- const visibleParts = getVisibleTimelineParts(message);
190
- const currentPartIndex = visibleParts.findIndex(part => part === currentPart);
191
-
192
- if (currentPartIndex === -1) {
193
- return [];
194
- }
195
-
196
- if (currentPartIndex > 0 && visibleParts[currentPartIndex - 1]?.type === 'data-tool-call') {
197
- return [];
198
- }
199
-
200
- const groupedParts: IToolGroup[] = [];
201
-
202
- for (let index = currentPartIndex; index < visibleParts.length; index += 1) {
203
- const part = visibleParts[index];
204
-
205
- if (part.type === 'reasoning') {
206
- break;
207
- }
208
-
209
- const formattedPart = formatToolCallPart(part, message);
210
-
211
- if (!formattedPart) {
212
- continue;
213
- }
214
-
215
- const lastGroup = groupedParts[groupedParts.length - 1];
216
-
217
- if (lastGroup?.title === formattedPart.toolInfo.toolName) {
218
- lastGroup.groupedTools.push(formattedPart);
219
- continue;
220
- }
221
-
222
- groupedParts.push({
223
- title: formattedPart.toolInfo.toolName,
224
- groupedTools: [formattedPart],
225
- });
226
- }
227
-
228
- return groupedParts;
229
- };
230
-
231
191
  function handleScroll() {
232
- if (isUserScrolled) {
192
+ if (isUserScrolled) {
233
193
  scrollContainerRef.value?.handleScroll();
234
194
  }
235
195
  }
@@ -1,36 +1,34 @@
1
1
  <template>
2
- <li class="mb-6 mx-2 mt-2 px-2 z-50 overflow-hidden">
3
- <span class="bg-lightNavbar dark:bg-darkNavbar absolute flex items-center text-listTableHeadingText dark:text-darkListTableHeadingText justify-center w-5 h-5 bg-brand-softer rounded-full -start-[0.68rem] ring-4 ring-lightNavbar dark:ring-darkNavbar ring-default">
4
- <div class="w-5 h-5 rounded-full flex items-center justify-center">
5
- <IconBrainOutline class="w-4 h-4" />
6
- </div>
7
- </span>
8
- <h3
9
- class=" flex items-center mb-1 text-sm ml-3 gap-1 cursor-pointer select-none hover:opacity-80 text-listTableHeadingText dark:text-darkListTableHeadingText"
2
+ <li class="overflow-hidden">
3
+ <button
4
+ type="button"
5
+ class="flex items-center gap-2 w-full py-0.5 text-left text-[0.8125rem] leading-5 select-none"
10
6
  @click="isExpanded = !isExpanded"
11
7
  >
12
- <span class="font-semibold">{{ reasoningTitle }}</span>
8
+ <IconBrainOutline class="w-4 h-4 shrink-0 text-gray-500 dark:text-gray-400" />
9
+ <span class="min-w-0 truncate font-medium text-gray-700 dark:text-gray-300">{{ reasoningTitle || $t('Reasoning') }}</span>
13
10
  <ThreeDotsAnimation v-if="isStreaming"/>
14
- <IconAngleDownOutline
11
+ <IconChevronDown
15
12
  :class="isExpanded ? 'rotate-180' : 'rotate-0'"
16
- class="transition-transform duration-200"
13
+ class="ml-auto w-3.5 h-3.5 shrink-0 text-gray-400 transition-transform duration-200"
17
14
  />
18
- </h3>
15
+ </button>
19
16
  <transition name="expand">
20
17
  <IncremarkContent
21
- v-if="isExpanded"
18
+ v-if="isExpanded"
22
19
  v-show="isExpanded"
23
- class="pl-4 text-sm"
20
+ class="ml-6 mb-1 text-sm text-gray-600 dark:text-gray-400"
24
21
  :content="reasoningText"
25
22
  />
26
- </transition>
23
+ </transition>
27
24
  </li>
28
25
  </template>
29
26
 
30
27
 
31
28
 
32
29
  <script setup lang="ts">
33
- import { IconBrainOutline, IconAngleDownOutline } from '@iconify-prerendered/vue-flowbite';
30
+ import { IconBrainOutline } from '@iconify-prerendered/vue-flowbite';
31
+ import { IconChevronDown } from '@iconify-prerendered/vue-heroicons';
34
32
  import type { IPart } from '../types';
35
33
  import { ref, computed, watch, defineAsyncComponent } from 'vue';
36
34
  import ThreeDotsAnimation from './ThreeDotsAnimation.vue';
@@ -1,11 +1,12 @@
1
1
  <template>
2
2
  <div
3
- class="flex items-center justify-center rounded-xl border px-6 border-gray-200 dark:border-gray-700 min-w-0 transition-shadow duration-150 z-20"
3
+ class="flex items-center justify-center min-w-0 transition-shadow duration-150 z-20"
4
4
  @click="handleMarkdownLinkClick"
5
5
  :class="[
6
6
  hasVegaLite ? 'w-full my-2' : 'my-2',
7
- props.role === 'user' ? 'bg-lightListTableHeading dark:bg-darkListTableHeading self-end max-w-[80%] mr-4'
8
- : 'border-none self-start max-w-full',
7
+ props.role === 'user'
8
+ ? 'self-end max-w-[82%] mr-4 px-3.5 py-0.5 rounded-2xl rounded-br-md bg-lightPrimary/10 dark:bg-darkPrimary/20'
9
+ : 'self-start max-w-full px-4',
9
10
  props.editMode ? 'ring-2 ring-lightPrimary dark:ring-darkPrimary w-full max-w-[calc(100%-2rem)] flex-col items-stretch py-3' : ''
10
11
  ]"
11
12
  >
@@ -1,72 +1,111 @@
1
1
  <template>
2
- <div class="mx-4 my-3 max-w-[min(34rem,calc(100%-2rem))] z-20 rounded-lg border border-lightPrimary/30 bg-lightNavbar p-4 text-lightListTableHeadingText shadow-sm dark:border-darkPrimary/40 dark:bg-darkNavbar dark:text-darkListTableHeadingText">
3
- <div class="flex items-start gap-3">
4
- <div class="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-lightPrimary/10 text-lightPrimary dark:bg-darkPrimary/15 dark:text-darkPrimary">
5
- <IconExclamationCircleOutline class="h-5 w-5" />
6
- </div>
2
+ <div
3
+ class="mx-4 my-2 max-w-[min(34rem,calc(100%-2rem))] z-20 rounded-xl border p-3.5"
4
+ :class="isAwaitingDecision
5
+ ? 'border-amber-200 bg-amber-50 dark:border-amber-500/30 dark:bg-amber-500/10'
6
+ : 'border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-800/50'"
7
+ >
8
+ <div class="flex items-start gap-2.5">
9
+ <span
10
+ class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full"
11
+ :class="isAwaitingDecision
12
+ ? 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-300'
13
+ : 'bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-300'"
14
+ >
15
+ <IconShieldExclamation class="h-4 w-4" />
16
+ </span>
7
17
  <div class="min-w-0 flex-1">
8
- <h3 class="text-sm font-semibold leading-5">{{ $t('Approval required') }}</h3>
9
- <p class="mt-1 text-sm leading-5 text-lightListTableText dark:text-darkListTableText">
18
+ <h3 class="text-sm font-semibold leading-5 text-gray-900 dark:text-gray-100">{{ $t('Approval needed') }}</h3>
19
+ <p class="text-[0.8125rem] leading-5 text-gray-600 dark:text-gray-300">
10
20
  {{ $t('Review what the agent is about to do before continuing.') }}
11
21
  </p>
12
- <ul
13
- v-if="data.messages?.length"
14
- class="mt-3 space-y-1 text-sm leading-5 text-lightListTableHeadingText dark:text-darkListTableHeadingText"
22
+ </div>
23
+ </div>
24
+ <ul
25
+ class="mt-3 divide-y rounded-lg border bg-white text-[0.8125rem] leading-5 text-gray-800
26
+ divide-gray-100 border-gray-200 dark:divide-gray-700 dark:border-gray-700 dark:bg-gray-900/40 dark:text-gray-200"
27
+ >
28
+ <li
29
+ v-for="(approval, approvalIndex) in data.approvals"
30
+ :key="approvalIndex"
31
+ class="px-3 py-2.5"
32
+ >
33
+ <p class="font-medium [overflow-wrap:anywhere]">{{ approval.summary }}</p>
34
+ <dl
35
+ v-if="approval.fields.length"
36
+ class="mt-2 grid grid-cols-[minmax(0,auto)_minmax(0,1fr)] gap-x-4 gap-y-1.5"
15
37
  >
16
- <li
17
- v-for="message in data.messages"
18
- :key="message"
19
- class="flex gap-2"
20
- >
21
- <span class="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-lightListTableHeadingText dark:bg-darkListTableHeadingText" />
22
- <span class="min-w-0 whitespace-pre-line break-words">{{ message }}</span>
23
- </li>
24
- </ul>
25
- <div class="mt-4 flex flex-wrap gap-2">
26
- <template v-if="data.status === 'pending'">
27
- <button
28
- type="button"
29
- class="inline-flex h-9 items-center gap-2 rounded-md border border-lightButtonsBorder bg-lightButtonsBackground px-3 text-sm font-medium text-lightButtonsText transition hover:bg-lightButtonsHover dark:border-darkButtonsBorder dark:bg-darkButtonsBackground dark:text-darkButtonsText dark:hover:bg-darkButtonsHover"
30
- @click="submit('approve')"
31
- >
32
- <IconCheckOutline class="h-4 w-4" />
33
- {{ $t('Approve') }}
34
- </button>
35
- <button
36
- type="button"
37
- class="inline-flex h-9 items-center gap-2 rounded-md border border-lightListTableText/20 bg-transparent px-3 text-sm font-medium text-lightListTableHeadingText transition hover:bg-lightListTableText/10 dark:border-darkListTableText/20 dark:text-darkListTableHeadingText dark:hover:bg-darkListTableText/10"
38
- @click="submit('reject')"
39
- >
40
- <IconCloseOutline class="h-4 w-4" />
41
- {{ $t('Reject') }}
42
- </button>
38
+ <template v-for="(field, fieldIndex) in approval.fields" :key="fieldIndex">
39
+ <dt class="text-gray-500 dark:text-gray-400 [overflow-wrap:anywhere]">{{ field.label }}</dt>
40
+ <dd class="flex min-w-0 flex-wrap items-center gap-1.5">
41
+ <template v-if="field.from !== undefined && field.from !== field.to">
42
+ <span
43
+ class="min-w-0 rounded px-1.5 line-through [overflow-wrap:anywhere]
44
+ bg-red-50 text-red-700 decoration-red-700/50 dark:bg-red-500/15 dark:text-red-300 dark:decoration-red-300/50"
45
+ >
46
+ {{ field.from }}
47
+ </span>
48
+ <IconArrowRight class="h-3.5 w-3.5 shrink-0 text-gray-400" />
49
+ <span
50
+ class="min-w-0 rounded px-1.5 font-medium [overflow-wrap:anywhere]
51
+ bg-green-50 text-green-700 dark:bg-green-500/15 dark:text-green-300"
52
+ >
53
+ {{ field.to }}
54
+ </span>
55
+ </template>
56
+ <span v-else class="min-w-0 font-medium [overflow-wrap:anywhere]">{{ field.to }}</span>
57
+ </dd>
43
58
  </template>
44
- <span
45
- v-else
46
- class="inline-flex h-8 items-center gap-2 rounded-md border border-lightListTableText/20 bg-lightListTableText/10 px-2.5 text-sm font-medium text-lightListTableHeadingText dark:border-darkListTableText/20 dark:bg-darkListTableText/10 dark:text-darkListTableHeadingText"
47
- >
48
- <Spinner v-if="data.status === 'processing'" class="h-4 w-4" />
49
- <IconCheckOutline v-else-if="data.status === 'approved'" class="h-4 w-4" />
50
- <IconCloseOutline v-else class="h-4 w-4" />
51
- {{ data.status === 'processing' ? $t('Processing') : data.status === 'approved' ? $t('Approved') : $t('Rejected') }}
52
- </span>
53
- </div>
54
- <p
55
- v-if="data.error"
56
- class="mt-3 text-sm leading-5 text-red-600 dark:text-red-400"
59
+ </dl>
60
+ </li>
61
+ </ul>
62
+ <div class="mt-3 flex flex-wrap gap-2">
63
+ <template v-if="data.status === 'pending'">
64
+ <button
65
+ type="button"
66
+ class="inline-flex h-8 items-center gap-1.5 rounded-lg px-3 text-[0.8125rem] font-medium transition-opacity duration-150
67
+ bg-lightPrimary text-lightPrimaryContrast hover:opacity-90 dark:bg-darkPrimary dark:text-darkPrimaryContrast"
68
+ @click="submit('approve')"
57
69
  >
58
- {{ data.error }}
59
- </p>
60
- </div>
70
+ <IconCheck class="h-4 w-4" />
71
+ {{ $t('Approve') }}
72
+ </button>
73
+ <button
74
+ type="button"
75
+ class="inline-flex h-8 items-center rounded-lg border px-3 text-[0.8125rem] font-medium transition-colors duration-150
76
+ border-gray-300 bg-white text-gray-700 hover:bg-gray-50
77
+ dark:border-gray-600 dark:bg-transparent dark:text-gray-200 dark:hover:bg-gray-700/50"
78
+ @click="submit('reject')"
79
+ >
80
+ {{ $t('Reject') }}
81
+ </button>
82
+ </template>
83
+ <span
84
+ v-else
85
+ class="inline-flex h-7 items-center gap-1.5 rounded-lg px-2.5 text-[0.8125rem] font-medium
86
+ bg-white text-gray-700 dark:bg-gray-900/40 dark:text-gray-200"
87
+ >
88
+ <Spinner v-if="data.status === 'processing'" class="h-4 w-4" />
89
+ <IconCheck v-else-if="data.status === 'approved'" class="h-4 w-4" />
90
+ <IconXMark v-else class="h-4 w-4" />
91
+ {{ data.status === 'processing' ? $t('Processing') : data.status === 'approved' ? $t('Approved') : $t('Rejected') }}
92
+ </span>
61
93
  </div>
94
+ <p
95
+ v-if="data.error"
96
+ class="mt-3 text-sm leading-5 text-red-600 dark:text-red-400"
97
+ >
98
+ {{ data.error }}
99
+ </p>
62
100
  </div>
63
101
  </template>
64
102
 
65
103
  <script setup lang="ts">
104
+ import { computed } from 'vue';
66
105
  import type { IPartData } from '../types';
67
106
  import { useAgentStore } from '../composables/useAgentStore';
68
107
  import { Spinner } from '@/afcl';
69
- import { IconCheckOutline, IconCloseOutline, IconExclamationCircleOutline } from '@iconify-prerendered/vue-flowbite';
108
+ import { IconArrowRight, IconCheck, IconShieldExclamation, IconXMark } from '@iconify-prerendered/vue-heroicons';
70
109
 
71
110
  const props = defineProps<{
72
111
  data: IPartData;
@@ -74,6 +113,8 @@ const props = defineProps<{
74
113
 
75
114
  const agentStore = useAgentStore();
76
115
 
116
+ const isAwaitingDecision = computed(() => props.data.status === 'pending' || props.data.status === 'processing');
117
+
77
118
  function submit(decision: 'approve' | 'reject') {
78
119
  if (!props.data.sessionId || props.data.status !== 'pending') {
79
120
  return;