@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.
- package/agent/middleware/openAiResponsesContinuation.ts +92 -0
- package/agent/middleware/sequenceDebug.ts +26 -0
- package/agent/simpleAgent.ts +11 -1
- package/build.log +4 -2
- package/custom/Message.vue +7 -3
- package/custom/incremark_code_renderers/IncremarkShikiCodeBlock.vue +78 -3
- package/custom/package.json +2 -1
- package/custom/pnpm-lock.yaml +721 -0
- package/custom/skills/data-analytics/SKILL.md +209 -0
- package/dist/agent/middleware/openAiResponsesContinuation.js +66 -0
- package/dist/agent/middleware/sequenceDebug.js +9 -0
- package/dist/agent/simpleAgent.js +6 -2
- package/dist/custom/Message.vue +7 -3
- package/dist/custom/incremark_code_renderers/IncremarkShikiCodeBlock.vue +78 -3
- package/dist/custom/package.json +2 -1
- package/dist/custom/pnpm-lock.yaml +721 -0
- package/dist/custom/skills/data-analytics/SKILL.md +209 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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 *
|
|
149
|
+
trigger: { tokens: 1024 * 128 },
|
|
146
150
|
keep: { messages: 10 },
|
|
147
151
|
}),
|
|
148
152
|
];
|
package/dist/custom/Message.vue
CHANGED
|
@@ -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="
|
|
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>
|