@adminforth/agent 1.1.0 → 1.2.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.
@@ -0,0 +1,209 @@
1
+ name: data-analytics
2
+ description: Analyze AdminForth resource data, summarize trends, and create charts from fetched rows.
3
+ ---
4
+
5
+ # Involved tools
6
+
7
+ Use `get_resource` first if you need to inspect resource structure and column names.
8
+
9
+ Use `get_resource_data` to fetch data for this skill. This is the main tool for loading rows for analytics, comparisons, distributions, and trend analysis.
10
+
11
+ # Instructions
12
+
13
+ When the user asks for analytics, reports, trends, comparisons, or distributions:
14
+
15
+ - Fetch the underlying rows with `get_resource_data`.
16
+ - Prefer narrow requests: request only the columns you need and use filters, sorting, pagination, and date ranges whenever possible.
17
+ - If the request is ambiguous, clarify the resource, metric, grouping, or date range before fetching data.
18
+ - Compute aggregates from the returned rows yourself: sums, counts, averages, min/max, grouped totals, ratios, and trend deltas.
19
+ - Return a short written summary with the key finding and most important numbers.
20
+ - If a chart would help, produce a Vega-Lite spec.
21
+
22
+ # Charts
23
+
24
+ Use Vega-Lite syntax for charts.
25
+
26
+ Return every chart as valid JSON inside a `vega-lite` fenced code block.
27
+
28
+ Every chart spec should include:
29
+ - `title.text`
30
+ - `title.subtitle`
31
+ - explicit axis titles when axes are used
32
+ - tooltips for the key fields
33
+
34
+ ### Line chart
35
+
36
+ ```vega-lite
37
+ {
38
+ "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
39
+ "title": {
40
+ "text": "Orders by Day",
41
+ "subtitle": "Daily order count for the selected date range"
42
+ },
43
+ "data": {
44
+ "values": [
45
+ { "date": "2026-04-01", "orders": 18 },
46
+ { "date": "2026-04-02", "orders": 25 },
47
+ { "date": "2026-04-03", "orders": 21 },
48
+ { "date": "2026-04-04", "orders": 29 }
49
+ ]
50
+ },
51
+ "mark": { "type": "line", "point": true },
52
+ "encoding": {
53
+ "x": { "field": "date", "type": "temporal", "title": "Date" },
54
+ "y": { "field": "orders", "type": "quantitative", "title": "Orders" },
55
+ "tooltip": [
56
+ { "field": "date", "type": "temporal", "title": "Date" },
57
+ { "field": "orders", "type": "quantitative", "title": "Orders" }
58
+ ]
59
+ }
60
+ }
61
+ ```
62
+
63
+ ### Bar chart
64
+
65
+ ```vega-lite
66
+ {
67
+ "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
68
+ "title": {
69
+ "text": "Revenue by Category",
70
+ "subtitle": "Top categories in the current filtered dataset"
71
+ },
72
+ "data": {
73
+ "values": [
74
+ { "category": "Hardware", "revenue": 42000 },
75
+ { "category": "Software", "revenue": 31500 },
76
+ { "category": "Services", "revenue": 22750 }
77
+ ]
78
+ },
79
+ "mark": "bar",
80
+ "encoding": {
81
+ "x": { "field": "category", "type": "nominal", "title": "Category", "sort": "-y" },
82
+ "y": { "field": "revenue", "type": "quantitative", "title": "Revenue" },
83
+ "tooltip": [
84
+ { "field": "category", "type": "nominal", "title": "Category" },
85
+ { "field": "revenue", "type": "quantitative", "title": "Revenue" }
86
+ ]
87
+ }
88
+ }
89
+ ```
90
+
91
+ ### Area chart
92
+
93
+ ```vega-lite
94
+ {
95
+ "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
96
+ "title": {
97
+ "text": "Monthly Signups",
98
+ "subtitle": "New users accumulated across the last six months"
99
+ },
100
+ "data": {
101
+ "values": [
102
+ { "month": "2025-11-01", "signups": 120 },
103
+ { "month": "2025-12-01", "signups": 155 },
104
+ { "month": "2026-01-01", "signups": 168 },
105
+ { "month": "2026-02-01", "signups": 190 },
106
+ { "month": "2026-03-01", "signups": 214 },
107
+ { "month": "2026-04-01", "signups": 238 }
108
+ ]
109
+ },
110
+ "mark": { "type": "area", "line": true, "point": true },
111
+ "encoding": {
112
+ "x": { "field": "month", "type": "temporal", "title": "Month" },
113
+ "y": { "field": "signups", "type": "quantitative", "title": "Signups" },
114
+ "tooltip": [
115
+ { "field": "month", "type": "temporal", "title": "Month" },
116
+ { "field": "signups", "type": "quantitative", "title": "Signups" }
117
+ ]
118
+ }
119
+ }
120
+ ```
121
+
122
+ ### Scatter plot
123
+
124
+ ```vega-lite
125
+ {
126
+ "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
127
+ "title": {
128
+ "text": "Ad Spend vs Revenue",
129
+ "subtitle": "Campaign-level correlation for the selected month"
130
+ },
131
+ "data": {
132
+ "values": [
133
+ { "campaign": "Search", "spend": 1200, "revenue": 5400 },
134
+ { "campaign": "Social", "spend": 900, "revenue": 3100 },
135
+ { "campaign": "Email", "spend": 350, "revenue": 2200 },
136
+ { "campaign": "Affiliates", "spend": 700, "revenue": 3600 }
137
+ ]
138
+ },
139
+ "mark": { "type": "point", "filled": true, "size": 120 },
140
+ "encoding": {
141
+ "x": { "field": "spend", "type": "quantitative", "title": "Ad Spend" },
142
+ "y": { "field": "revenue", "type": "quantitative", "title": "Revenue" },
143
+ "tooltip": [
144
+ { "field": "campaign", "type": "nominal", "title": "Campaign" },
145
+ { "field": "spend", "type": "quantitative", "title": "Ad Spend" },
146
+ { "field": "revenue", "type": "quantitative", "title": "Revenue" }
147
+ ]
148
+ }
149
+ }
150
+ ```
151
+
152
+ ### Pie chart
153
+
154
+ ```vega-lite
155
+ {
156
+ "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
157
+ "title": {
158
+ "text": "Tickets by Status",
159
+ "subtitle": "Share of tickets in each workflow state"
160
+ },
161
+ "data": {
162
+ "values": [
163
+ { "status": "Open", "count": 42 },
164
+ { "status": "In Progress", "count": 27 },
165
+ { "status": "Resolved", "count": 58 }
166
+ ]
167
+ },
168
+ "mark": { "type": "arc", "innerRadius": 40 },
169
+ "encoding": {
170
+ "theta": { "field": "count", "type": "quantitative", "title": "Tickets" },
171
+ "color": { "field": "status", "type": "nominal", "title": "Status" },
172
+ "tooltip": [
173
+ { "field": "status", "type": "nominal", "title": "Status" },
174
+ { "field": "count", "type": "quantitative", "title": "Tickets" }
175
+ ]
176
+ }
177
+ }
178
+ ```
179
+
180
+ ### Histogram
181
+
182
+ ```vega-lite
183
+ {
184
+ "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
185
+ "title": {
186
+ "text": "Order Value Distribution",
187
+ "subtitle": "Histogram of order totals in the filtered result set"
188
+ },
189
+ "data": {
190
+ "values": [
191
+ { "order_total": 24 },
192
+ { "order_total": 31 },
193
+ { "order_total": 39 },
194
+ { "order_total": 42 },
195
+ { "order_total": 63 },
196
+ { "order_total": 78 },
197
+ { "order_total": 95 }
198
+ ]
199
+ },
200
+ "mark": "bar",
201
+ "encoding": {
202
+ "x": { "bin": true, "field": "order_total", "type": "quantitative", "title": "Order Total" },
203
+ "y": { "aggregate": "count", "type": "quantitative", "title": "Count" },
204
+ "tooltip": [
205
+ { "aggregate": "count", "type": "quantitative", "title": "Count" }
206
+ ]
207
+ }
208
+ }
209
+ ```
@@ -120,6 +120,10 @@ export const useAgentStore = defineStore('agent', () => {
120
120
  });
121
121
  const blockCloseOfChat = ref(false);
122
122
 
123
+ function sortSessionsListByTimestamp(sessionsList: ISessionsListItem[]) {
124
+ return [...sessionsList].sort((a: ISessionsListItem, b: ISessionsListItem) => b.timestamp.localeCompare(a.timestamp));
125
+ }
126
+
123
127
  async function sendMessage() {
124
128
  const message = trimmedUserMessage.value;
125
129
  if (!message || isResponseInProgress.value) {
@@ -128,6 +132,11 @@ export const useAgentStore = defineStore('agent', () => {
128
132
  if (!currentSession.value || currentSession.value.sessionId === 'pre-session') {
129
133
  await createNewSession(message);
130
134
  }
135
+ currentSession.value.timestamp = new Date().toISOString();
136
+ sessionList.value = sortSessionsListByTimestamp(sessionList.value.map((s: ISessionsListItem) => s.sessionId === currentSession.value?.sessionId ? {
137
+ ...s,
138
+ timestamp: currentSession.value?.timestamp || s.timestamp,
139
+ } : s));
131
140
  lastMessage.value = message;
132
141
  currentChat.value?.sendMessage({
133
142
  text: message,
@@ -0,0 +1,66 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { AIMessage } from "@langchain/core/messages";
11
+ import { createMiddleware } from "langchain";
12
+ function getTurnKey(context) {
13
+ return `${context.sessionId}:${context.turnId}`;
14
+ }
15
+ function getResponseId(message) {
16
+ var _a;
17
+ const metadata = message.response_metadata;
18
+ return (_a = metadata === null || metadata === void 0 ? void 0 : metadata.id) !== null && _a !== void 0 ? _a : null;
19
+ }
20
+ function getPreviousResponseId(modelSettings) {
21
+ return modelSettings === null || modelSettings === void 0 ? void 0 : modelSettings.previous_response_id;
22
+ }
23
+ function getContinuationMessages(messages, previousResponseId) {
24
+ var _a;
25
+ let continuationStartIndex = null;
26
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
27
+ const message = messages[index];
28
+ if (AIMessage.isInstance(message) &&
29
+ ((_a = message.response_metadata) === null || _a === void 0 ? void 0 : _a.id) ===
30
+ previousResponseId) {
31
+ continuationStartIndex = index + 1;
32
+ break;
33
+ }
34
+ }
35
+ if (continuationStartIndex === null) {
36
+ return null;
37
+ }
38
+ return messages.slice(continuationStartIndex);
39
+ }
40
+ export function createOpenAiResponsesContinuationMiddleware() {
41
+ const responseIdsByTurn = new Map();
42
+ return createMiddleware({
43
+ name: "OpenAiResponsesContinuationMiddleware",
44
+ wrapModelCall(request, handler) {
45
+ return __awaiter(this, void 0, void 0, function* () {
46
+ var _a;
47
+ const context = request.runtime.context;
48
+ const turnKey = getTurnKey(context);
49
+ const previousResponseId = (_a = getPreviousResponseId(request.modelSettings)) !== null && _a !== void 0 ? _a : responseIdsByTurn.get(turnKey);
50
+ const continuationMessages = previousResponseId
51
+ ? getContinuationMessages(request.messages, previousResponseId)
52
+ : null;
53
+ const response = yield handler(previousResponseId && continuationMessages
54
+ ? Object.assign(Object.assign({}, request), { messages: continuationMessages, modelSettings: Object.assign(Object.assign({}, request.modelSettings), { previous_response_id: previousResponseId }) }) : request);
55
+ const responseId = getResponseId(response);
56
+ if (responseId) {
57
+ responseIdsByTurn.set(turnKey, responseId);
58
+ }
59
+ else {
60
+ responseIdsByTurn.delete(turnKey);
61
+ }
62
+ return response;
63
+ });
64
+ },
65
+ });
66
+ }
@@ -27,6 +27,8 @@ function createPendingSequenceDebug(sequenceId) {
27
27
  prompt: "",
28
28
  reasoning: "",
29
29
  text: "",
30
+ cachedTokens: 0,
31
+ responseId: null,
30
32
  toolCalls: [],
31
33
  pendingToolCalls: 0,
32
34
  resultType: null,
@@ -47,6 +49,8 @@ function finalizeSequenceDebug(sequence) {
47
49
  prompt: sequence.prompt,
48
50
  reasoning: sequence.reasoning,
49
51
  text: sequence.text,
52
+ cachedTokens: sequence.cachedTokens,
53
+ responseId: sequence.responseId,
50
54
  toolCalls: sequence.toolCalls.map((_a) => {
51
55
  var { completed: _completed } = _a, toolCall = __rest(_a, ["completed"]);
52
56
  return toolCall;
@@ -95,6 +99,7 @@ function hasToolCallSignal(message) {
95
99
  message.additional_kwargs.tool_calls.length > 0));
96
100
  }
97
101
  function extractSequenceResponseDebug(message) {
102
+ var _a, _b, _c, _d, _e;
98
103
  const blocks = getMessageBlocks(message);
99
104
  const reasoning = blocks
100
105
  .filter((block) => (block === null || block === void 0 ? void 0 : block.type) === "reasoning")
@@ -107,6 +112,8 @@ function extractSequenceResponseDebug(message) {
107
112
  return {
108
113
  reasoning,
109
114
  text: textFromBlocks || (typeof message.content === "string" ? message.content : ""),
115
+ cachedTokens: (_c = (_b = (_a = message.usage_metadata) === null || _a === void 0 ? void 0 : _a.input_token_details) === null || _b === void 0 ? void 0 : _b.cache_read) !== null && _c !== void 0 ? _c : 0,
116
+ responseId: (_e = (_d = message.response_metadata) === null || _d === void 0 ? void 0 : _d.id) !== null && _e !== void 0 ? _e : null,
110
117
  resultType: hasToolCallSignal(message) ? "tool_calls" : "final_text",
111
118
  };
112
119
  }
@@ -142,6 +149,8 @@ export function createSequenceDebugCollector() {
142
149
  const sequenceDebug = ensureSequenceDebug();
143
150
  sequenceDebug.reasoning = params.reasoning;
144
151
  sequenceDebug.text = params.text;
152
+ sequenceDebug.cachedTokens = params.cachedTokens;
153
+ sequenceDebug.responseId = params.responseId;
145
154
  sequenceDebug.resultType = params.resultType;
146
155
  if (sequenceDebug.resultType === "final_text" &&
147
156
  sequenceDebug.pendingToolCalls === 0) {
@@ -16,6 +16,7 @@ import { ChatOpenAI } from "@langchain/openai";
16
16
  import { createAgentTools } from "./tools/index.js";
17
17
  import { createApiBasedToolsMiddleware } from "./middleware/apiBasedTools.js";
18
18
  import { createSequenceDebugMiddleware, } from "./middleware/sequenceDebug.js";
19
+ import { createOpenAiResponsesContinuationMiddleware } from "./middleware/openAiResponsesContinuation.js";
19
20
  const checkpointer = new MemorySaver();
20
21
  export const contextSchema = z.object({
21
22
  adminUser: z.custom(),
@@ -121,7 +122,8 @@ export function createAgentChatModel(params) {
121
122
  const model = (_c = (_b = params.modelName) !== null && _b !== void 0 ? _b : options.model) !== null && _c !== void 0 ? _c : "gpt-5-nano";
122
123
  const baseURL = (_d = options.baseURL) !== null && _d !== void 0 ? _d : options.baseUrl;
123
124
  const reasoning = normalizeReasoning(params.reasoning);
124
- return new ChatOpenAI(Object.assign(Object.assign(Object.assign({ apiKey: options.openAiApiKey, model, maxTokens: params.maxTokens, useResponsesApi: true, outputVersion: "v1" }, (reasoning ? { reasoning } : {})), (typeof options.timeoutMs === "number"
125
+ // @ts-ignore
126
+ return new ChatOpenAI(Object.assign(Object.assign(Object.assign({ apiKey: options.openAiApiKey, model, maxTokens: params.maxTokens, useResponsesApi: true, outputVersion: "v1", promptCacheKey: `adminforth-agent:${model}:system-v1:tools-v1`, promptCacheRetention: "in_memory" }, (reasoning ? { reasoning } : {})), (typeof options.timeoutMs === "number"
125
127
  ? { timeout: options.timeoutMs }
126
128
  : {})), (baseURL
127
129
  ? {
@@ -136,13 +138,15 @@ export function callAgent(params) {
136
138
  const { name, model, summaryModel, messages, adminUser, apiBasedTools, customComponentsDir, sessionId, turnId, userTimeZone, emitToolCallEvent, sequenceDebugSink, } = params;
137
139
  const tools = yield createAgentTools(customComponentsDir, apiBasedTools);
138
140
  const apiBasedToolsMiddleware = createApiBasedToolsMiddleware(apiBasedTools);
141
+ const openAiResponsesContinuationMiddleware = createOpenAiResponsesContinuationMiddleware();
139
142
  const sequenceDebugMiddleware = createSequenceDebugMiddleware(sequenceDebugSink);
140
143
  const middleware = [
141
144
  apiBasedToolsMiddleware,
145
+ openAiResponsesContinuationMiddleware,
142
146
  sequenceDebugMiddleware,
143
147
  summarizationMiddleware({
144
148
  model: summaryModel,
145
- trigger: { tokens: 1024 * 8 },
149
+ trigger: { tokens: 1024 * 128 },
146
150
  keep: { messages: 10 },
147
151
  }),
148
152
  ];
@@ -17,9 +17,10 @@
17
17
 
18
18
  </div>
19
19
  <AutoScrollContainer
20
- enabled
20
+ :enabled="!showScrollToBottomButton"
21
21
  class="flex flex-col overflow-y-auto border-t border-gray-200 dark:border-gray-700"
22
22
  ref="scrollContainer"
23
+ :threshold="10"
23
24
  behavior="smooth"
24
25
  >
25
26
 
@@ -57,8 +58,8 @@
57
58
  v-if="props.messages.length === 0"
58
59
  class="flex-1 flex flex-col items-center justify-center text-gray-400 tracking-widest text-xl font-medium"
59
60
  >
60
- <p>Start the conversation</p>
61
- <p class="tracking-normal text-base text">Give any input to begin</p>
61
+ <p>{{ $t('Start the conversation') }}</p>
62
+ <p class="tracking-normal text-base text">{{ $t('Give any input to begin') }}</p>
62
63
  </div>
63
64
  </AutoScrollContainer>
64
65
  </template>
@@ -151,20 +152,16 @@ const groupToolCallParts = (message: IMessage) => {
151
152
  if(!part?.toolInfo) {
152
153
  continue;
153
154
  }
154
- console.log('part', part);
155
155
  if (part.toolInfo.toolName === currentToolName) {
156
- console.log('grouping part with tool name', currentToolName);
157
156
  groupedParts[groupedParts.length - 1].groupedTools.push(part);
158
157
  continue;
159
158
  }
160
159
  currentToolName = part.toolInfo.toolName;
161
- console.log('starting new group with tool name', currentToolName);
162
160
  groupedParts.push({
163
161
  title: currentToolName,
164
162
  groupedTools: [part]
165
163
  });
166
164
  }
167
- console.log('groupedParts', groupedParts);
168
165
  return groupedParts;
169
166
  }
170
167
 
@@ -1,13 +1,16 @@
1
1
  <template>
2
2
  <div
3
- class="max-w-[80%] flex px-4 py-2 m-2 rounded-xl border border-gray-200 dark:border-gray-700"
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
- :class="props.role === 'user' ? 'bg-lightListTableHeading dark:bg-darkListTableHeading self-end'
5
+ :class="[
6
+ hasVegaLite ? 'w-full' : '',
7
+ props.role === 'user' ? 'bg-lightListTableHeading dark:bg-darkListTableHeading self-end'
6
8
  : isTypeReasoning || isTypeToolCall ? 'bg-transparent border-none self-start'
7
- : 'bg-blue-100 dark:bg-blue-700/10 self-start'"
9
+ : 'bg-blue-100 dark:bg-blue-700/10 self-start'
10
+ ]"
8
11
  >
9
12
  <IncremarkContent
10
- class="text-wrap break-words max-w-full"
13
+ class="text-wrap break-words w-full max-w-full"
11
14
  v-if="content && props.type === 'text'"
12
15
  :content="content"
13
16
  :is-finished="isFinished"
@@ -88,6 +91,7 @@
88
91
  const content = computed(() => props.message)
89
92
  const isFinished = computed(() => props.state === 'done')
90
93
  const isThoughtsExpanded = ref(false)
94
+ const hasVegaLite = computed(() => props.type === 'text' && props.message.includes('```vega-lite'))
91
95
 
92
96
  const isTypeReasoning = computed(() => props.type === 'reasoning')
93
97
  const isTypeToolCall = computed(() => props.type === 'data-tool-call')
@@ -6,14 +6,14 @@
6
6
  "
7
7
  >
8
8
  <h3 :class="h3Style">{{ $t('Chat history') }}</h3>
9
- <Button @click="agentStore.createPreSession()" :disabled="agentStore.isResponseInProgress" class="w-[360px] mx-4 my-2 mb-4 rounded-3xl text-gray-800 dark:text-gray-200">
9
+ <Button @click="agentStore.createPreSession(); agentStore.setSessionHistoryOpen(false)" :disabled="agentStore.isResponseInProgress" class="w-[360px] mx-4 my-2 mb-4 rounded-3xl text-gray-800 dark:text-gray-200">
10
10
  <IconPlusOutline class="w-5 h-5" />
11
11
  {{ $t('New chat') }}
12
12
  </Button>
13
13
  <div class="w-full border-b border-gray-200 dark:border-gray-700"/>
14
14
  <div class="absolute w-full h-full flex flex-col items-center justify-center bg-gray-100/50 dark:bg-gray-700/50 z-10" v-if="agentStore.isResponseInProgress">
15
15
  <Spinner class="w-8 h-8" v-if="agentStore.isResponseInProgress" />
16
- <p class="mt-2 text-gray-800 dark:text-gray-200">generation in progress...</p>
16
+ <p class="mt-2 text-gray-800 dark:text-gray-200">{{ $t('Generation in progress...') }}</p>
17
17
  </div>
18
18
  <div v-for="group in groupedSessions" :key="group.dayKey" class="w-full py-2">
19
19
  <div class="px-4 pb-2 text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">
@@ -38,7 +38,7 @@
38
38
  v-if="!groupedSessions || groupedSessions.length === 0"
39
39
  class="w-full h-full flex items-center justify-center text-gray-800 dark:text-gray-200"
40
40
  >
41
- There is no previous chat sessions
41
+ {{ $t('There are no previous chat sessions') }}
42
42
  </p>
43
43
  </div>
44
44
  </template>
@@ -1,5 +1,8 @@
1
1
  <template>
2
- <div v-if="props.data?.toolInfo" class="inline-flex m-2 max-w-[80%] flex-col gap-3 rounded-xl p-2 text-lightListTableHeadingText dark:text-darkListTableHeadingText">
2
+ <div
3
+ class="inline-flex m-2 max-w-[80%] flex-col gap-3 rounded-xl p-2 cursor-pointer text-lightListTableHeadingText dark:text-darkListTableHeadingText hover:opacity-75"
4
+ @click="isInputOutputExpanded = !isInputOutputExpanded"
5
+ >
3
6
  <div class="flex items-center gap-3">
4
7
  <div class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-white/70 dark:bg-blue-700/20">
5
8
  <Spinner v-if="isRunning" class="h-4 w-4" />
@@ -18,8 +21,7 @@
18
21
  <IconAngleDownOutline
19
22
  v-if="hasToolSections"
20
23
  :class="isInputOutputExpanded ? 'rotate-180' : 'rotate-0'"
21
- class="cursor-pointer transition-transform duration-200 hover:scale-105 hover:opacity-75"
22
- @click="isInputOutputExpanded = !isInputOutputExpanded"
24
+ class="cursor-pointer transition-transform duration-200 hover:scale-105"
23
25
  />
24
26
  </div>
25
27
  <transition name="expand">
@@ -1,8 +1,9 @@
1
1
  <template>
2
2
  <template v-for="group in props.toolGroup" :key="group.title">
3
- <div v-if="group.groupedTools.length > 1" class="mb-4 flex flex-col">
4
- <div class="flex items-center gap-2 p-2 m-2 cursor-pointer hover:opacity-75 break-all font-mono text-sm leading-5" @click="toggleGroup(group.title)">
5
- - {{ group.title }} {{ 'x' + group.groupedTools.length }}
3
+ <div v-if="group.groupedTools.length > 1" class="flex flex-col">
4
+ <div class="flex items-center gap-2 p-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
+ <IconMinusOutline class="w-6 h-6 p-1"/>
6
+ {{ group.title }} {{ 'x' + group.groupedTools.length }}
6
7
  <IconAngleDownOutline
7
8
  class="transition-transform duration-200 hover:scale-105 hover:opacity-75"
8
9
  :class="expandedGroups.includes(group.title) ? 'rotate-180' : 'rotate-0'"
@@ -10,7 +11,7 @@
10
11
  </div>
11
12
  <transition name="expand">
12
13
  <div v-show="expandedGroups.includes(group.title)" class="flex flex-col">
13
- <ToolRenderer v-for="part in group.groupedTools" :key="part.text + part.type" :data="part" />
14
+ <ToolRenderer v-for="part in group.groupedTools" :key="part.text + part.type" :data="part" class="ml-8"/>
14
15
  </div>
15
16
  </transition>
16
17
  </div>
@@ -24,7 +25,7 @@ import { Tool } from 'langchain';
24
25
  import ToolRenderer from './ToolRenderer.vue';
25
26
  import type { IPart } from './types';
26
27
  import { ref } from 'vue';
27
- import { IconAngleDownOutline } from '@iconify-prerendered/vue-flowbite';
28
+ import { IconAngleDownOutline, IconMinusOutline } from '@iconify-prerendered/vue-flowbite';
28
29
 
29
30
  const props = defineProps<{
30
31
  toolGroup: {