@adminforth/agent 1.1.1 → 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
+ ```
@@ -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
  ];
@@ -2,12 +2,15 @@
2
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
- :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')
@@ -18,7 +18,13 @@
18
18
 
19
19
  <div class="incremark-shiki-body">
20
20
  <div
21
- v-if="renderedHtml"
21
+ v-if="shouldRenderVega && !renderedHtml"
22
+ ref="vegaContainer"
23
+ class="incremark-vega"
24
+ />
25
+
26
+ <div
27
+ v-else-if="renderedHtml"
22
28
  class="incremark-shiki-html"
23
29
  v-html="renderedHtml"
24
30
  />
@@ -31,6 +37,7 @@
31
37
  <script setup lang="ts">
32
38
  import type { Code } from 'mdast';
33
39
  import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
40
+ import embed from 'vega-embed';
34
41
 
35
42
  import { highlightCodeSnippetHtml, type IncremarkCodeTheme } from './incremarkCodeHighlight';
36
43
 
@@ -53,15 +60,18 @@ const props = withDefaults(defineProps<{
53
60
  const renderedHtml = ref('');
54
61
  const copied = ref(false);
55
62
  const prefersDarkMode = ref(isDarkDocument());
63
+ const vegaContainer = ref<HTMLDivElement | null>(null);
56
64
 
57
65
  let copyResetTimeout: number | null = null;
58
66
  let renderRequestId = 0;
59
67
  let scheduledFrameId: number | null = null;
60
68
  let themeObserver: MutationObserver | null = null;
69
+ let vegaResult: { finalize: () => void } | null = null;
61
70
 
62
71
  const sourceCode = computed(() => props.node.value ?? '');
63
72
  const language = computed(() => props.node.lang?.trim().toLowerCase() || 'text');
64
- const languageLabel = computed(() => props.node.lang?.trim() || 'text');
73
+ const languageLabel = computed(() => language.value === 'vega-lite' ? '' : props.node.lang?.trim() || 'text');
74
+ const shouldRenderVega = computed(() => language.value === 'vega-lite' && props.blockStatus === 'completed');
65
75
  const codeTheme = computed<IncremarkCodeTheme>(() => {
66
76
  const requestedTheme = props.theme ?? (prefersDarkMode.value ? props.darkTheme : props.lightTheme);
67
77
 
@@ -77,7 +87,7 @@ const codeTheme = computed<IncremarkCodeTheme>(() => {
77
87
  });
78
88
 
79
89
  watch(
80
- [sourceCode, language, codeTheme, () => props.disableHighlight],
90
+ [sourceCode, language, codeTheme, () => props.disableHighlight, () => props.blockStatus],
81
91
  () => {
82
92
  scheduleHighlight();
83
93
  },
@@ -86,6 +96,7 @@ watch(
86
96
 
87
97
  onMounted(() => {
88
98
  if (typeof MutationObserver === 'undefined' || typeof document === 'undefined') {
99
+ scheduleHighlight();
89
100
  return;
90
101
  }
91
102
 
@@ -97,10 +108,13 @@ onMounted(() => {
97
108
  attributes: true,
98
109
  attributeFilter: ['class'],
99
110
  });
111
+
112
+ scheduleHighlight();
100
113
  });
101
114
 
102
115
  onBeforeUnmount(() => {
103
116
  renderRequestId += 1;
117
+ clearVega();
104
118
 
105
119
  if (copyResetTimeout !== null) {
106
120
  window.clearTimeout(copyResetTimeout);
@@ -154,6 +168,45 @@ function scheduleHighlight() {
154
168
  async function renderHighlight() {
155
169
  const requestId = ++renderRequestId;
156
170
 
171
+ if (shouldRenderVega.value) {
172
+ renderedHtml.value = '';
173
+
174
+ if (!sourceCode.value || !vegaContainer.value) {
175
+ return;
176
+ }
177
+
178
+ try {
179
+ clearVega();
180
+ const spec = JSON.parse(sourceCode.value);
181
+
182
+ if (spec.width == null) {
183
+ spec.width = 'container';
184
+ }
185
+
186
+ if (spec.autosize == null) {
187
+ spec.autosize = { type: 'fit-x', contains: 'padding' };
188
+ }
189
+
190
+ const result = await embed(vegaContainer.value, spec, {
191
+ actions: false,
192
+ renderer: 'svg',
193
+ });
194
+
195
+ if (requestId !== renderRequestId) {
196
+ result.finalize();
197
+ return;
198
+ }
199
+
200
+ vegaResult = result;
201
+ return;
202
+ } catch (error) {
203
+ clearVega();
204
+ console.error('Failed to render Vega-Lite block', error);
205
+ }
206
+ } else {
207
+ clearVega();
208
+ }
209
+
157
210
  if (!sourceCode.value || props.disableHighlight) {
158
211
  renderedHtml.value = '';
159
212
  return;
@@ -177,6 +230,15 @@ async function renderHighlight() {
177
230
  function isDarkDocument(): boolean {
178
231
  return typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
179
232
  }
233
+
234
+ function clearVega() {
235
+ vegaResult?.finalize();
236
+ vegaResult = null;
237
+
238
+ if (vegaContainer.value) {
239
+ vegaContainer.value.innerHTML = '';
240
+ }
241
+ }
180
242
  </script>
181
243
 
182
244
  <style scoped>
@@ -265,6 +327,11 @@ function isDarkDocument(): boolean {
265
327
  overflow-x: auto;
266
328
  }
267
329
 
330
+ .incremark-vega {
331
+ padding: 18px;
332
+ width: 100%;
333
+ }
334
+
268
335
  .incremark-shiki-fallback {
269
336
  margin: 0;
270
337
  padding: 18px;
@@ -298,4 +365,12 @@ function isDarkDocument(): boolean {
298
365
  :deep(.incremark-shiki-html .line) {
299
366
  min-height: 1.65em;
300
367
  }
368
+
369
+ :deep(.incremark-vega .vega-embed) {
370
+ width: 100%;
371
+ }
372
+
373
+ :deep(.incremark-vega){
374
+ padding: 0;
375
+ }
301
376
  </style>
@@ -21,6 +21,7 @@
21
21
  "ai": "^6.0.158",
22
22
  "dompurify": "^3.3.3",
23
23
  "katex": "^0.16.45",
24
- "marked": "^18.0.0"
24
+ "marked": "^18.0.0",
25
+ "vega-embed": "^7.1.0"
25
26
  }
26
27
  }