@adminforth/agent 3.0.0 → 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.
@@ -1,7 +1,9 @@
1
1
  <template>
2
+ <!-- in full screen the controls line up with the centered column of the conversation -->
2
3
  <div
3
4
  ref="headerRef"
4
5
  class="af-agent-header flex items-center gap-0.5 h-12 px-2 border-b border-lightNavbarBorder dark:border-darkNavbarBorder"
6
+ :style="agentStore.isFullScreen ? { paddingInline: `max(0.5rem, calc((100% - ${agentStore.MAX_WIDTH}rem) / 2 + 0.5rem))` } : undefined"
5
7
  >
6
8
  <Button
7
9
  variant="ghost"
@@ -14,8 +14,8 @@
14
14
  ref="chatSurface"
15
15
  id="adminforth-agent-chat-surface"
16
16
  class="fixed top-0 right-0 z-40 flex h-screen border-l border-lightNavbarBorder bg-lightNavbar dark:border-darkNavbarBorder dark:bg-darkNavbar"
17
- :class="[agentStore.isChatOpen ? 'translate-x-0' : 'translate-x-full', !agentStore.isTeleportedToBody ? 'shadow-popover dark:shadow-darkPopover' : '']"
18
- :style="{ width: agentStore.chatWidth + 'rem' }"
17
+ :class="[agentStore.isChatOpen ? 'translate-x-0' : 'translate-x-full', !agentStore.isTeleportedToBody && !agentStore.isFullScreen ? 'shadow-popover dark:shadow-darkPopover' : '']"
18
+ :style="{ width: agentStore.isFullScreen ? '100%' : agentStore.chatWidth + 'rem' }"
19
19
  >
20
20
  <div
21
21
  v-if="!(coreStore.isMobile || agentStore.isFullScreen)"
@@ -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
- const INITIAL_SCROLL_DEBOUNCE = 200
47
- let initialScrollDone = false
48
- let initialScrollTimer: ReturnType<typeof setTimeout> | null = null
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: () => scrollToBottom(true),
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
- nextTick(() => scrollToBottom(true))
110
- scheduleInitialScrollEnd()
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
- if (initialScrollTimer) clearTimeout(initialScrollTimer)
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
- // the chart sits on the surface of the code block, so it takes the theme of the page
209
- theme: codeTheme.value === 'dark' ? 'dark' : undefined,
210
- config: { background: 'transparent' },
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/agent",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",