@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.
- 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/ConversationArea.vue +4 -7
- package/custom/Message.vue +8 -4
- package/custom/SessionsHistory.vue +3 -3
- package/custom/ToolRenderer.vue +5 -3
- package/custom/ToolsGroup.vue +6 -5
- 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/custom/useAgentStore.ts +9 -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/ConversationArea.vue +4 -7
- package/dist/custom/Message.vue +8 -4
- package/dist/custom/SessionsHistory.vue +3 -3
- package/dist/custom/ToolRenderer.vue +5 -3
- package/dist/custom/ToolsGroup.vue +6 -5
- 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/dist/custom/useAgentStore.ts +9 -0
- package/dist/index.js +9 -0
- package/index.ts +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { AIMessage } from "@langchain/core/messages";
|
|
2
|
+
import { createMiddleware } from "langchain";
|
|
3
|
+
|
|
4
|
+
type OpenAiResponsesMetadata = {
|
|
5
|
+
id?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type OpenAiResponsesContext = {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
turnId: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function getTurnKey(context: OpenAiResponsesContext) {
|
|
14
|
+
return `${context.sessionId}:${context.turnId}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getResponseId(message: AIMessage) {
|
|
18
|
+
const metadata = message.response_metadata as OpenAiResponsesMetadata | undefined;
|
|
19
|
+
return metadata?.id ?? null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getPreviousResponseId(modelSettings?: Record<string, unknown>) {
|
|
23
|
+
return (modelSettings as { previous_response_id?: string } | undefined)
|
|
24
|
+
?.previous_response_id;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getContinuationMessages<T extends { response_metadata?: unknown }>(
|
|
28
|
+
messages: T[],
|
|
29
|
+
previousResponseId: string,
|
|
30
|
+
) {
|
|
31
|
+
let continuationStartIndex: number | null = null;
|
|
32
|
+
|
|
33
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
34
|
+
const message = messages[index];
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
AIMessage.isInstance(message) &&
|
|
38
|
+
(message.response_metadata as OpenAiResponsesMetadata | undefined)?.id ===
|
|
39
|
+
previousResponseId
|
|
40
|
+
) {
|
|
41
|
+
continuationStartIndex = index + 1;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (continuationStartIndex === null) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return messages.slice(continuationStartIndex);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createOpenAiResponsesContinuationMiddleware() {
|
|
54
|
+
const responseIdsByTurn = new Map<string, string>();
|
|
55
|
+
|
|
56
|
+
return createMiddleware({
|
|
57
|
+
name: "OpenAiResponsesContinuationMiddleware",
|
|
58
|
+
async wrapModelCall(request, handler) {
|
|
59
|
+
const context = request.runtime.context as OpenAiResponsesContext;
|
|
60
|
+
const turnKey = getTurnKey(context);
|
|
61
|
+
const previousResponseId =
|
|
62
|
+
getPreviousResponseId(request.modelSettings) ??
|
|
63
|
+
responseIdsByTurn.get(turnKey);
|
|
64
|
+
const continuationMessages = previousResponseId
|
|
65
|
+
? getContinuationMessages(request.messages, previousResponseId)
|
|
66
|
+
: null;
|
|
67
|
+
|
|
68
|
+
const response = await handler(
|
|
69
|
+
previousResponseId && continuationMessages
|
|
70
|
+
? {
|
|
71
|
+
...request,
|
|
72
|
+
messages: continuationMessages,
|
|
73
|
+
modelSettings: {
|
|
74
|
+
...request.modelSettings,
|
|
75
|
+
previous_response_id: previousResponseId,
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
: request,
|
|
79
|
+
) as AIMessage;
|
|
80
|
+
|
|
81
|
+
const responseId = getResponseId(response);
|
|
82
|
+
|
|
83
|
+
if (responseId) {
|
|
84
|
+
responseIdsByTurn.set(turnKey, responseId);
|
|
85
|
+
} else {
|
|
86
|
+
responseIdsByTurn.delete(turnKey);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return response;
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -23,6 +23,8 @@ export type SequenceDebug = {
|
|
|
23
23
|
prompt: string;
|
|
24
24
|
reasoning: string;
|
|
25
25
|
text: string;
|
|
26
|
+
cachedTokens: number;
|
|
27
|
+
responseId: string | null;
|
|
26
28
|
toolCalls: SequenceDebugToolCall[];
|
|
27
29
|
endedAt: string;
|
|
28
30
|
resultType: SequenceDebugResultType;
|
|
@@ -37,9 +39,21 @@ type PendingSequenceDebug = Omit<SequenceDebug, "toolCalls" | "endedAt" | "resul
|
|
|
37
39
|
type SequenceDebugModelCall = {
|
|
38
40
|
reasoning: string;
|
|
39
41
|
text: string;
|
|
42
|
+
cachedTokens: number;
|
|
43
|
+
responseId: string | null;
|
|
40
44
|
resultType: SequenceDebugResultType;
|
|
41
45
|
};
|
|
42
46
|
|
|
47
|
+
type OpenAiUsageMetadata = {
|
|
48
|
+
input_token_details?: {
|
|
49
|
+
cache_read?: number;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type OpenAiResponseMetadata = {
|
|
54
|
+
id?: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
43
57
|
export type SequenceDebugModelCallSink = {
|
|
44
58
|
handleModelCallStart: (prompt: string) => void;
|
|
45
59
|
handleModelCallComplete: (params: SequenceDebugModelCall) => void;
|
|
@@ -58,6 +72,8 @@ function createPendingSequenceDebug(sequenceId: number): PendingSequenceDebug {
|
|
|
58
72
|
prompt: "",
|
|
59
73
|
reasoning: "",
|
|
60
74
|
text: "",
|
|
75
|
+
cachedTokens: 0,
|
|
76
|
+
responseId: null,
|
|
61
77
|
toolCalls: [],
|
|
62
78
|
pendingToolCalls: 0,
|
|
63
79
|
resultType: null,
|
|
@@ -83,6 +99,8 @@ function finalizeSequenceDebug(sequence: PendingSequenceDebug): SequenceDebug {
|
|
|
83
99
|
prompt: sequence.prompt,
|
|
84
100
|
reasoning: sequence.reasoning,
|
|
85
101
|
text: sequence.text,
|
|
102
|
+
cachedTokens: sequence.cachedTokens,
|
|
103
|
+
responseId: sequence.responseId,
|
|
86
104
|
toolCalls: sequence.toolCalls.map(({ completed: _completed, ...toolCall }) => toolCall),
|
|
87
105
|
endedAt: new Date().toISOString(),
|
|
88
106
|
resultType: sequence.resultType ?? "final_text",
|
|
@@ -172,6 +190,12 @@ function extractSequenceResponseDebug(message: AIMessage): SequenceDebugModelCal
|
|
|
172
190
|
return {
|
|
173
191
|
reasoning,
|
|
174
192
|
text: textFromBlocks || (typeof message.content === "string" ? message.content : ""),
|
|
193
|
+
cachedTokens:
|
|
194
|
+
(message.usage_metadata as OpenAiUsageMetadata | undefined)
|
|
195
|
+
?.input_token_details?.cache_read ?? 0,
|
|
196
|
+
responseId:
|
|
197
|
+
(message.response_metadata as OpenAiResponseMetadata | undefined)?.id ??
|
|
198
|
+
null,
|
|
175
199
|
resultType: hasToolCallSignal(message) ? "tool_calls" : "final_text",
|
|
176
200
|
};
|
|
177
201
|
}
|
|
@@ -216,6 +240,8 @@ export function createSequenceDebugCollector(): SequenceDebugCollector {
|
|
|
216
240
|
const sequenceDebug = ensureSequenceDebug();
|
|
217
241
|
sequenceDebug.reasoning = params.reasoning;
|
|
218
242
|
sequenceDebug.text = params.text;
|
|
243
|
+
sequenceDebug.cachedTokens = params.cachedTokens;
|
|
244
|
+
sequenceDebug.responseId = params.responseId;
|
|
219
245
|
sequenceDebug.resultType = params.resultType;
|
|
220
246
|
|
|
221
247
|
if (
|
package/agent/simpleAgent.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
createSequenceDebugMiddleware,
|
|
12
12
|
type SequenceDebugModelCallSink,
|
|
13
13
|
} from "./middleware/sequenceDebug.js";
|
|
14
|
+
import { createOpenAiResponsesContinuationMiddleware } from "./middleware/openAiResponsesContinuation.js";
|
|
14
15
|
import type { ApiBasedTool } from "../apiBasedTools.js";
|
|
15
16
|
import type { ToolCallEventSink } from "./toolCallEvents.js";
|
|
16
17
|
|
|
@@ -199,12 +200,18 @@ export function createAgentChatModel(params: {
|
|
|
199
200
|
const baseURL = options.baseURL ?? options.baseUrl;
|
|
200
201
|
const reasoning = normalizeReasoning(params.reasoning);
|
|
201
202
|
|
|
203
|
+
// @ts-ignore
|
|
202
204
|
return new ChatOpenAI({
|
|
203
205
|
apiKey: options.openAiApiKey,
|
|
204
206
|
model,
|
|
205
207
|
maxTokens: params.maxTokens,
|
|
206
208
|
useResponsesApi: true,
|
|
207
209
|
outputVersion: "v1",
|
|
210
|
+
|
|
211
|
+
promptCacheKey: `adminforth-agent:${model}:system-v1:tools-v1`,
|
|
212
|
+
|
|
213
|
+
promptCacheRetention: "in_memory",
|
|
214
|
+
|
|
208
215
|
...(reasoning ? { reasoning } : {}),
|
|
209
216
|
...(typeof options.timeoutMs === "number"
|
|
210
217
|
? { timeout: options.timeoutMs }
|
|
@@ -250,16 +257,19 @@ export async function callAgent(params: {
|
|
|
250
257
|
|
|
251
258
|
const tools = await createAgentTools(customComponentsDir, apiBasedTools);
|
|
252
259
|
const apiBasedToolsMiddleware = createApiBasedToolsMiddleware(apiBasedTools);
|
|
260
|
+
const openAiResponsesContinuationMiddleware =
|
|
261
|
+
createOpenAiResponsesContinuationMiddleware();
|
|
253
262
|
const sequenceDebugMiddleware = createSequenceDebugMiddleware(
|
|
254
263
|
sequenceDebugSink,
|
|
255
264
|
);
|
|
256
265
|
|
|
257
266
|
const middleware = [
|
|
258
267
|
apiBasedToolsMiddleware,
|
|
268
|
+
openAiResponsesContinuationMiddleware,
|
|
259
269
|
sequenceDebugMiddleware,
|
|
260
270
|
summarizationMiddleware({
|
|
261
271
|
model: summaryModel,
|
|
262
|
-
trigger: { tokens: 1024 *
|
|
272
|
+
trigger: { tokens: 1024 * 128 },
|
|
263
273
|
keep: { messages: 10 },
|
|
264
274
|
}),
|
|
265
275
|
] as const;
|
package/build.log
CHANGED
|
@@ -21,10 +21,12 @@ custom/incremark_code_renderers/incremarkCodeHighlight.ts
|
|
|
21
21
|
custom/incremark_code_renderers/incremarkRenderer.ts
|
|
22
22
|
custom/incremark_code_renderers/renderIncremarkMarkdown.ts
|
|
23
23
|
custom/skills/
|
|
24
|
+
custom/skills/data-analytics/
|
|
25
|
+
custom/skills/data-analytics/SKILL.md
|
|
24
26
|
custom/skills/fetch_data/
|
|
25
27
|
custom/skills/fetch_data/SKILL.md
|
|
26
28
|
custom/skills/mutate_data/
|
|
27
29
|
custom/skills/mutate_data/SKILL.md
|
|
28
30
|
|
|
29
|
-
sent
|
|
30
|
-
total size is
|
|
31
|
+
sent 166,886 bytes received 394 bytes 334,560.00 bytes/sec
|
|
32
|
+
total size is 165,300 speedup is 0.99
|
|
@@ -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
|
|
package/custom/Message.vue
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
-
class="max-w-[80%] flex px-4
|
|
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')
|
|
@@ -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">
|
|
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
|
|
41
|
+
{{ $t('There are no previous chat sessions') }}
|
|
42
42
|
</p>
|
|
43
43
|
</div>
|
|
44
44
|
</template>
|
package/custom/ToolRenderer.vue
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
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
|
|
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">
|
package/custom/ToolsGroup.vue
CHANGED
|
@@ -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="
|
|
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
|
-
-
|
|
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: {
|
|
@@ -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>
|