@adminforth/agent 3.0.1 → 3.1.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.
|
@@ -42,18 +42,12 @@ const scrollElement = ref<HTMLElement | null>(null)
|
|
|
42
42
|
let lastScrollTop = 0
|
|
43
43
|
let lastScrollHeight = 0
|
|
44
44
|
let observer: MutationObserver | null = null
|
|
45
|
+
let resizeObserver: ResizeObserver | null = null
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
function scheduleInitialScrollEnd(): void {
|
|
51
|
-
if (initialScrollTimer) clearTimeout(initialScrollTimer)
|
|
52
|
-
initialScrollTimer = setTimeout(() => {
|
|
53
|
-
initialScrollDone = true
|
|
54
|
-
initialScrollTimer = null
|
|
55
|
-
}, INITIAL_SCROLL_DEBOUNCE)
|
|
56
|
-
}
|
|
47
|
+
// Markdown, code highlighting and charts render asynchronously and keep growing the content after
|
|
48
|
+
// it appears, with pauses longer than any debounce. So after mount the container stays at the bottom
|
|
49
|
+
// on every resize until the user scrolls away or the parent scrolls it (a sent message).
|
|
50
|
+
let keepAtBottom = false
|
|
57
51
|
|
|
58
52
|
const scrollParams = ref({
|
|
59
53
|
scrollTop: 0,
|
|
@@ -62,7 +56,10 @@ const scrollParams = ref({
|
|
|
62
56
|
});
|
|
63
57
|
|
|
64
58
|
defineExpose({
|
|
65
|
-
scrollToBottom: () =>
|
|
59
|
+
scrollToBottom: () => {
|
|
60
|
+
keepAtBottom = false
|
|
61
|
+
scrollToBottom(true)
|
|
62
|
+
},
|
|
66
63
|
isUserScrolledUp: () => isUserScrolledUp.value,
|
|
67
64
|
container: containerRef,
|
|
68
65
|
handleScroll,
|
|
@@ -90,11 +87,6 @@ onMounted(() => {
|
|
|
90
87
|
scrollParams.value.clientHeight = containerRef.value.scrollEl.clientHeight
|
|
91
88
|
if (props.enabled && !isUserScrolledUp.value) {
|
|
92
89
|
scrollToBottom()
|
|
93
|
-
} else if (props.scrollToBottomOnMount && !initialScrollDone) {
|
|
94
|
-
// Delayed content is still settling in the initial scroll phase: keep
|
|
95
|
-
// pinning to the bottom and restart the quiet-period debounce.
|
|
96
|
-
scrollToBottom(true)
|
|
97
|
-
scheduleInitialScrollEnd()
|
|
98
90
|
}
|
|
99
91
|
})
|
|
100
92
|
})
|
|
@@ -106,14 +98,23 @@ onMounted(() => {
|
|
|
106
98
|
})
|
|
107
99
|
|
|
108
100
|
if (props.scrollToBottomOnMount) {
|
|
109
|
-
|
|
110
|
-
|
|
101
|
+
keepAtBottom = true
|
|
102
|
+
const scrollEl = containerRef.value.scrollEl
|
|
103
|
+
// fires once on observe, which is the initial scroll; instant, the user sees the latest messages right away.
|
|
104
|
+
// While enabled, the mutation observer above follows the content with the behavior of the container
|
|
105
|
+
resizeObserver = new ResizeObserver(() => {
|
|
106
|
+
if (keepAtBottom && !props.enabled) {
|
|
107
|
+
scrollEl.scrollTo({ top: scrollEl.scrollHeight, behavior: 'instant' })
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
resizeObserver.observe(scrollEl)
|
|
111
|
+
resizeObserver.observe(scrollEl.firstElementChild!)
|
|
111
112
|
}
|
|
112
113
|
})
|
|
113
114
|
|
|
114
115
|
onUnmounted(() => {
|
|
115
116
|
observer?.disconnect()
|
|
116
|
-
|
|
117
|
+
resizeObserver?.disconnect()
|
|
117
118
|
})
|
|
118
119
|
|
|
119
120
|
function isNearBottom(customThreshold?: number): boolean {
|
|
@@ -168,6 +169,7 @@ function handleScroll(detectScrollDown = true, customThreshold?: number): void {
|
|
|
168
169
|
const isContentUnchanged = scrollHeight === lastScrollHeight
|
|
169
170
|
if ((isScrollingUp || isScrollingDown) && isContentUnchanged) {
|
|
170
171
|
isUserScrolledUp.value = true
|
|
172
|
+
keepAtBottom = false
|
|
171
173
|
}
|
|
172
174
|
}
|
|
173
175
|
lastScrollTop = scrollTop
|
|
@@ -40,6 +40,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
|
40
40
|
import embed from 'vega-embed';
|
|
41
41
|
|
|
42
42
|
import type { IncremarkCodeTheme } from './incremarkCodeHighlight';
|
|
43
|
+
import { vegaLiteConfig } from './vegaTheme';
|
|
43
44
|
|
|
44
45
|
const props = withDefaults(defineProps<{
|
|
45
46
|
node: Code;
|
|
@@ -205,9 +206,9 @@ async function renderHighlight() {
|
|
|
205
206
|
const result = await embed(vegaContainer.value, spec, {
|
|
206
207
|
actions: false,
|
|
207
208
|
renderer: 'svg',
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
// read on every render, a theme switch re-renders the chart with the tokens of the new theme
|
|
210
|
+
config: vegaLiteConfig(vegaContainer.value),
|
|
211
|
+
tooltip: { theme: 'af' },
|
|
211
212
|
});
|
|
212
213
|
|
|
213
214
|
if (requestId !== renderRequestId) {
|
|
@@ -307,9 +308,22 @@ function clearVega() {
|
|
|
307
308
|
overflow-x: auto;
|
|
308
309
|
}
|
|
309
310
|
|
|
311
|
+
/* tokens for vegaLiteConfig, the chart reads them when it renders */
|
|
310
312
|
.incremark-vega {
|
|
311
|
-
padding: 12px;
|
|
312
313
|
width: 100%;
|
|
314
|
+
--chart-text: theme('colors.lightInputText');
|
|
315
|
+
--chart-muted: theme('colors.lightInputIconColor');
|
|
316
|
+
--chart-grid: theme('colors.lightCardBorder');
|
|
317
|
+
--chart-baseline: theme('colors.lightInputBorder');
|
|
318
|
+
--chart-surface: theme('colors.lightCardBackground');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.dark .incremark-vega {
|
|
322
|
+
--chart-text: theme('colors.darkInputText');
|
|
323
|
+
--chart-muted: theme('colors.darkInputIconColor');
|
|
324
|
+
--chart-grid: theme('colors.darkCardBorder');
|
|
325
|
+
--chart-baseline: theme('colors.darkInputBorder');
|
|
326
|
+
--chart-surface: theme('colors.darkCardBackground');
|
|
313
327
|
}
|
|
314
328
|
|
|
315
329
|
.incremark-shiki-fallback {
|
|
@@ -349,6 +363,24 @@ function clearVega() {
|
|
|
349
363
|
:deep(.incremark-vega .vega-embed) {
|
|
350
364
|
width: 100%;
|
|
351
365
|
}
|
|
366
|
+
</style>
|
|
367
|
+
|
|
368
|
+
<style>
|
|
369
|
+
/* vega-tooltip appends its element to the body, so the "af" tooltip theme is global */
|
|
370
|
+
#vg-tooltip-element.af-theme {
|
|
371
|
+
@apply rounded-default border-0 px-2 py-1.5 text-xs bg-lightDropdownOptionsBackground text-lightInputText shadow-popover dark:bg-darkDropdownOptionsBackground dark:text-darkInputText dark:shadow-darkPopover;
|
|
372
|
+
font-family: inherit;
|
|
373
|
+
}
|
|
352
374
|
|
|
375
|
+
#vg-tooltip-element.af-theme h2 {
|
|
376
|
+
@apply mb-1 text-xs font-semibold;
|
|
377
|
+
}
|
|
353
378
|
|
|
379
|
+
#vg-tooltip-element.af-theme td.key {
|
|
380
|
+
@apply pr-2 text-lightInputIconColor dark:text-darkInputIconColor;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
#vg-tooltip-element.af-theme td.value {
|
|
384
|
+
@apply font-medium tabular-nums;
|
|
385
|
+
}
|
|
354
386
|
</style>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Config } from 'vega-embed';
|
|
2
|
+
import { CHART_COLORS } from '@/afcl/chartUtils';
|
|
3
|
+
|
|
4
|
+
// charts of the agent follow the AFCL charts of the design system: one categorical palette in fixed order,
|
|
5
|
+
// 2px lines, 4px rounded bar ends, recessive horizontal grid, muted axes, no frame around the plot.
|
|
6
|
+
// Colors come from the --chart-* properties of the chart container, which take them from the theme tokens
|
|
7
|
+
export function vegaLiteConfig(container: HTMLElement): Config {
|
|
8
|
+
const style = getComputedStyle(container);
|
|
9
|
+
const token = (name: string) => style.getPropertyValue(`--chart-${name}`).trim();
|
|
10
|
+
const text = token('text');
|
|
11
|
+
const muted = token('muted');
|
|
12
|
+
const surface = token('surface');
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
background: 'transparent',
|
|
16
|
+
// vega measures a container chart by clientWidth, so the space around the chart is vega padding, not CSS padding
|
|
17
|
+
padding: 12,
|
|
18
|
+
font: style.fontFamily,
|
|
19
|
+
view: { stroke: null, continuousHeight: 200 },
|
|
20
|
+
range: { category: CHART_COLORS },
|
|
21
|
+
scale: { bandPaddingInner: 0.3 },
|
|
22
|
+
mark: { color: CHART_COLORS[0] },
|
|
23
|
+
bar: { cornerRadiusEnd: 4, binSpacing: 2 },
|
|
24
|
+
line: { strokeWidth: 2, strokeJoin: 'round', strokeCap: 'round' },
|
|
25
|
+
area: { fillOpacity: 0.12, line: true },
|
|
26
|
+
point: { size: 36, stroke: surface, strokeWidth: 1.5 },
|
|
27
|
+
arc: { stroke: surface, strokeWidth: 2 },
|
|
28
|
+
text: { color: text, fontSize: 11 },
|
|
29
|
+
title: {
|
|
30
|
+
anchor: 'start',
|
|
31
|
+
color: text,
|
|
32
|
+
fontSize: 13,
|
|
33
|
+
fontWeight: 600,
|
|
34
|
+
subtitleColor: muted,
|
|
35
|
+
subtitleFontSize: 12,
|
|
36
|
+
subtitlePadding: 2,
|
|
37
|
+
offset: 16,
|
|
38
|
+
},
|
|
39
|
+
axis: {
|
|
40
|
+
domainColor: token('baseline'),
|
|
41
|
+
gridColor: token('grid'),
|
|
42
|
+
ticks: false,
|
|
43
|
+
labelColor: muted,
|
|
44
|
+
labelFontSize: 11,
|
|
45
|
+
labelPadding: 6,
|
|
46
|
+
labelOverlap: true,
|
|
47
|
+
labelSeparation: 8,
|
|
48
|
+
titleColor: muted,
|
|
49
|
+
titleFontSize: 11,
|
|
50
|
+
titleFontWeight: 500,
|
|
51
|
+
titlePadding: 8,
|
|
52
|
+
},
|
|
53
|
+
axisX: { grid: false },
|
|
54
|
+
axisXDiscrete: { labelAngle: 0 },
|
|
55
|
+
axisY: { domain: false },
|
|
56
|
+
legend: {
|
|
57
|
+
orient: 'bottom',
|
|
58
|
+
direction: 'horizontal',
|
|
59
|
+
labelColor: text,
|
|
60
|
+
labelFontSize: 11,
|
|
61
|
+
titleColor: muted,
|
|
62
|
+
titleFontSize: 11,
|
|
63
|
+
titleFontWeight: 500,
|
|
64
|
+
symbolSize: 64,
|
|
65
|
+
columnPadding: 12,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -15,6 +15,10 @@ Every chart spec should include:
|
|
|
15
15
|
- explicit axis titles when axes are used
|
|
16
16
|
- tooltips for the key fields
|
|
17
17
|
|
|
18
|
+
Don't set `config`, `width`, colors or fonts: the chat draws every chart in the AdminForth theme.
|
|
19
|
+
|
|
20
|
+
Labels of the x axis stay horizontal and the ones which don't fit are hidden, so for more than 6 categories or long category names put the categories on `y` (horizontal bars).
|
|
21
|
+
|
|
18
22
|
### Line chart
|
|
19
23
|
|
|
20
24
|
```vega-lite
|