@adecore/agents-react 0.28.0 → 0.29.1

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.
Files changed (30) hide show
  1. package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +19 -9
  2. package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +4 -2
  3. package/dist/chat/ui/intelligent-ui/UiReply.js +3 -3
  4. package/dist/chat/ui/intelligent-ui/renderers/chart.js +2 -2
  5. package/dist/chat/ui/intelligent-ui/renderers/choices.js +13 -8
  6. package/dist/chat/ui/intelligent-ui/renderers/content.js +15 -14
  7. package/dist/chat/ui/intelligent-ui/renderers/data.js +19 -6
  8. package/dist/chat/ui/intelligent-ui/renderers/inputs.js +10 -9
  9. package/dist/chat/ui/intelligent-ui/renderers/links.js +5 -3
  10. package/dist/chat/ui/intelligent-ui/renderers/map.js +7 -7
  11. package/dist/chat/ui/intelligent-ui/renderers/structure.js +5 -5
  12. package/dist/chat/ui/intelligent-ui/renderers/text.js +25 -14
  13. package/dist/chat/ui/intelligent-ui/tones.d.ts +2 -0
  14. package/dist/chat/ui/intelligent-ui/tones.js +16 -0
  15. package/dist/theme.css +38 -0
  16. package/package.json +8 -8
  17. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +26 -13
  18. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +7 -4
  19. package/src/chat/ui/intelligent-ui/UiReply.tsx +3 -3
  20. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +2 -2
  21. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +33 -19
  22. package/src/chat/ui/intelligent-ui/renderers/content.tsx +36 -23
  23. package/src/chat/ui/intelligent-ui/renderers/data.tsx +61 -19
  24. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +21 -14
  25. package/src/chat/ui/intelligent-ui/renderers/links.tsx +7 -4
  26. package/src/chat/ui/intelligent-ui/renderers/map.tsx +15 -12
  27. package/src/chat/ui/intelligent-ui/renderers/structure.tsx +6 -6
  28. package/src/chat/ui/intelligent-ui/renderers/text.tsx +63 -42
  29. package/src/chat/ui/intelligent-ui/tones.ts +18 -0
  30. package/src/theme.css +38 -0
@@ -7,7 +7,7 @@ import { Icon, Meter, Pill, Spinner } from '@adecore/ui';
7
7
  import { formatNumber, formatPercent } from '@adecore/ui/format';
8
8
  import { UiHeadContext, type UiRenderContext, type UiRendererProps } from '../render-context';
9
9
  import { uiEventTime, uiNodeLabel } from '../node-text';
10
- import { UI_TONE_ICONS, UI_TONE_PILL, UI_TONE_SURFACE, UI_TONE_TEXT } from '../tones';
10
+ import { UI_STEP_PILL, UI_TONE_FILL, UI_TONE_ICONS, UI_TONE_PILL, UI_TONE_SURFACE, UI_TONE_TEXT } from '../tones';
11
11
 
12
12
  type StepState = UiProps<'Step'>['state'];
13
13
 
@@ -26,43 +26,56 @@ const STEP_TONES: Readonly<Record<StepState, string>> = {
26
26
  skipped: 'text-text-faint'
27
27
  };
28
28
 
29
+ /* A detail longer than this reads as a sentence, which a tag cannot wrap. */
30
+ const DETAIL_AS_TAG = 24;
31
+
29
32
  /* Whether the block still changes: while it streams, or while it reads live data. Only then does anything spin. */
30
33
  function isLive(context: UiRenderContext): boolean {
31
34
  return context.phase === 'streaming' || (context.live !== undefined && context.live !== null && context.live.state !== 'refused');
32
35
  }
33
36
 
34
- /* A badge longer than this is a sentence, which a tag cannot wrap; it stands under the title as text instead. */
35
- const BADGE_AS_TAG = 32;
37
+ /* A Summary and a Callout share one row: the icon in a square, and beside it text that centers on the square while it is shorter and starts at its top once it is taller. */
38
+ const ICON_ROW = 'flex items-start gap-3 rounded-xl p-3';
39
+ const ICON_SQUARE = 'grid size-8.5 shrink-0 place-items-center rounded-lg';
40
+ const ICON_TEXT = 'flex min-h-8.5 min-w-0 grow flex-col justify-center gap-0.5';
36
41
 
37
- /* The head of the block when it is the first node, a subheading without an icon further down. */
42
+ /*
43
+ * A tile in its tone; the badge stands under the title in the tone's color. The head of the block
44
+ * carries its icon in a square of its own, a Summary further down is a subheading without one. The
45
+ * icon is the ground's ink in the dark theme, where its tones are too light for white.
46
+ */
38
47
  export function SummaryRenderer({ node, children }: UiRendererProps<UiProps<'Summary'>>) {
39
48
  const headId = useContext(UiHeadContext);
40
49
  const head = headId === undefined || headId === node.id;
41
50
  const { tone, badge } = node.props;
42
- const tag = badge !== undefined && badge !== '' && badge.length <= BADGE_AS_TAG;
51
+ const shown = tone ?? 'neutral';
43
52
  return (
44
- <div className="flex items-start gap-2 px-2">
45
- {head && tone !== undefined && <Icon icon={UI_TONE_ICONS[tone]} size={16} className={clsx('mt-0.75 shrink-0', UI_TONE_TEXT[tone])} />}
46
- <div className="flex min-w-0 grow flex-wrap items-start gap-x-2 gap-y-1">
47
- <div className="max-w-full min-w-0 grow basis-48 text-sm font-semibold text-balance wrap-break-word text-text">{children}</div>
48
- {tag && (
49
- <Pill shape="tag" tone={tone === undefined ? 'muted' : UI_TONE_PILL[tone]}>
50
- {badge}
51
- </Pill>
53
+ <div className={clsx(ICON_ROW, tone === undefined ? 'bg-ui-tile' : UI_TONE_SURFACE[tone])}>
54
+ {head && (
55
+ <span aria-hidden className={clsx(ICON_SQUARE, 'text-accent-text dark:text-surface', UI_TONE_FILL[shown])}>
56
+ <Icon icon={UI_TONE_ICONS[shown]} size={16} />
57
+ </span>
58
+ )}
59
+ <div className={ICON_TEXT}>
60
+ <div className="text-base font-semibold text-balance wrap-break-word text-text">{children}</div>
61
+ {badge !== undefined && badge !== '' && (
62
+ <p className={clsx('text-2xs font-medium wrap-anywhere', tone === undefined ? 'text-text-muted' : UI_TONE_TEXT[tone])}>{badge}</p>
52
63
  )}
53
- {!tag && badge !== undefined && badge !== '' && <p className="w-full text-xs wrap-anywhere text-text-muted">{badge}</p>}
54
64
  </div>
55
65
  </div>
56
66
  );
57
67
  }
58
68
 
69
+ /* A quiet tile whatever its tone: only the square of its icon wears the tone. */
59
70
  export function CalloutRenderer({ node, children }: UiRendererProps<UiProps<'Callout'>>) {
60
71
  const { tone, title } = node.props;
61
72
  return (
62
- <div className={clsx('flex gap-2 rounded-md p-2 text-sm', UI_TONE_SURFACE[tone])}>
63
- <Icon icon={UI_TONE_ICONS[tone]} size={16} className={clsx('mt-0.75 shrink-0', UI_TONE_TEXT[tone])} />
64
- <div className="min-w-0 grow">
65
- {title !== undefined && title !== '' && <div className="font-medium text-text">{title}</div>}
73
+ <div className={clsx('chat-ui-flat bg-ui-tile text-xs', ICON_ROW)}>
74
+ <span aria-hidden className={clsx(ICON_SQUARE, UI_TONE_SURFACE[tone], UI_TONE_TEXT[tone])}>
75
+ <Icon icon={UI_TONE_ICONS[tone]} size={16} />
76
+ </span>
77
+ <div className={ICON_TEXT}>
78
+ {title !== undefined && title !== '' && <div className="font-semibold text-text">{title}</div>}
66
79
  <div className="text-text-muted">{children}</div>
67
80
  </div>
68
81
  </div>
@@ -87,7 +100,7 @@ export function ProgressRenderer({ node, children, context }: UiRendererProps<Ui
87
100
  value === undefined ? null : limit === 100 ? formatPercent(value) : t('blocks.progress.of', { value: formatNumber(value), max: formatNumber(limit) });
88
101
  const label = uiNodeLabel(node);
89
102
  return (
90
- <div className="flex flex-col gap-1 px-2">
103
+ <div className="chat-ui-flat flex flex-col gap-2 rounded-xl bg-ui-tile p-3">
91
104
  <div className="flex items-baseline justify-between gap-3 text-xs">
92
105
  <span className="min-w-0 text-text">{children}</span>
93
106
  {reading !== null ? (
@@ -102,17 +115,17 @@ export function ProgressRenderer({ node, children, context }: UiRendererProps<Ui
102
115
  }
103
116
 
104
117
  export function StepsRenderer({ children }: UiRendererProps<UiProps<'Steps'>>) {
105
- return <ol className="chat-ui-nodes flex flex-col px-2">{children}</ol>;
118
+ return <ol className="chat-ui-nodes flex flex-col gap-1.5">{children}</ol>;
106
119
  }
107
120
 
108
- /* A running step spins only while the block is live; in a quiet block it is a still dot. */
121
+ /* A tile per step with its state in words at the end. A running step spins only while the block is live; in a quiet block it is a still dot. */
109
122
  export function StepRenderer({ node, children, context }: UiRendererProps<UiProps<'Step'>>) {
110
123
  const { t } = useTranslation('agent-chat');
111
124
  const { state, detail } = node.props;
112
125
  const quiet = state === 'pending' || state === 'skipped';
113
126
  return (
114
- <li className="flex min-h-7 items-center gap-2 text-sm">
115
- <span className={clsx('grid size-4 shrink-0 place-items-center', STEP_TONES[state])}>
127
+ <li className="flex min-h-10 items-start gap-3 rounded-xl bg-ui-tile px-3 py-2.5 text-xs">
128
+ <span className={clsx('mt-0.5 grid size-4 shrink-0 place-items-center', STEP_TONES[state])}>
116
129
  {state === 'running' ? (
117
130
  isLive(context) ? (
118
131
  <Spinner size={14} />
@@ -123,21 +136,23 @@ export function StepRenderer({ node, children, context }: UiRendererProps<UiProp
123
136
  <Icon icon={STEP_ICONS[state]} size={14} />
124
137
  )}
125
138
  </span>
126
- <span className="sr-only">{t(`blocks.step.${state}`)}</span>
127
139
  {/* A short detail stays beside the title; one that does not fit beside it takes the line under it. */}
128
- <span className="flex min-w-0 grow flex-wrap items-baseline justify-between gap-x-2">
129
- <span className={clsx('max-w-full min-w-0 grow basis-48 wrap-break-word', quiet ? 'text-text-faint' : 'text-text')}>{children}</span>
130
- {detail !== undefined && detail !== '' && <span className="max-w-full text-xs wrap-anywhere text-text-faint tabular-nums">{detail}</span>}
140
+ <span className="flex min-w-0 grow flex-wrap items-baseline gap-x-2">
141
+ <span className={clsx('max-w-full min-w-0 font-medium wrap-break-word', quiet ? 'text-text-muted' : 'text-text')}>{children}</span>
142
+ {detail !== undefined && detail !== '' && <span className="max-w-full text-2xs wrap-anywhere text-text-muted tabular-nums">{detail}</span>}
131
143
  </span>
144
+ <Pill shape="tag" size="sm" tone={UI_STEP_PILL[state]} className="shrink-0">
145
+ {t(`blocks.step.${state}`)}
146
+ </Pill>
132
147
  </li>
133
148
  );
134
149
  }
135
150
 
136
151
  export function TimelineRenderer({ children }: UiRendererProps<UiProps<'Timeline'>>) {
137
- return <ol className="chat-ui-nodes flex flex-col px-2">{children}</ol>;
152
+ return <ol className="chat-ui-nodes chat-ui-flat flex flex-col rounded-xl bg-ui-tile px-3 py-1.5">{children}</ol>;
138
153
  }
139
154
 
140
- /* Each row draws its own stretch of the line, down to the next dot, so a row that failed to draw leaves the others whole. */
155
+ /* A row of the tile: the time, a dot in the tone, the title, and a short detail as a tag at the end or a longer one under the title. */
141
156
  export function EventRenderer({ node, children }: UiRendererProps<UiProps<'Event'>>) {
142
157
  const { t } = useTranslation('agent-chat');
143
158
  const { at, detail } = node.props;
@@ -145,22 +160,28 @@ export function EventRenderer({ node, children }: UiRendererProps<UiProps<'Event
145
160
  const time = uiEventTime(at);
146
161
  // A neutral dot says nothing the title does not, and a detail is part of what the row says.
147
162
  const label = [time, uiNodeLabel(node), tone === 'neutral' ? '' : t(`blocks.event.tone.${tone}`), detail ?? ''].filter((part) => part !== '').join(', ');
163
+ const described = detail !== undefined && detail !== '';
164
+ const tagged = described && detail.length <= DETAIL_AS_TAG;
148
165
  return (
149
- <li aria-label={label} className="group/event relative flex min-h-7 gap-3 pb-3 last:pb-0">
150
- <span aria-hidden className={clsx('mt-1 size-2 shrink-0 rounded-full bg-current', UI_TONE_TEXT[tone])} />
151
- <span aria-hidden className="absolute top-3.5 bottom-0 left-0.75 w-px bg-border group-last/event:hidden" />
166
+ <li aria-label={label} className="flex min-h-9 items-start gap-3 py-2 text-xs">
167
+ {/* A time nobody could read stays as written and is no `dateTime` for a machine to read. */}
168
+ {parseUiEventTime(at) === null ? (
169
+ <span className="w-24 shrink-0 text-2xs leading-5 wrap-anywhere text-text-muted tabular-nums">{time}</span>
170
+ ) : (
171
+ <time dateTime={at} className="w-24 shrink-0 text-2xs leading-5 text-text-muted tabular-nums">
172
+ {time}
173
+ </time>
174
+ )}
175
+ <span aria-hidden className={clsx('mt-1.5 size-2 shrink-0 rounded-full bg-current', tone === 'neutral' ? 'text-text-faint' : UI_TONE_TEXT[tone])} />
152
176
  <div className="flex min-w-0 grow flex-col gap-0.5">
153
- {/* A time nobody could read stays as written and is no `dateTime` for a machine to read. */}
154
- {parseUiEventTime(at) === null ? (
155
- <span className="text-xs wrap-anywhere text-text-muted tabular-nums">{time}</span>
156
- ) : (
157
- <time dateTime={at} className="text-xs text-text-muted tabular-nums">
158
- {time}
159
- </time>
160
- )}
161
- <div className="text-sm wrap-break-word text-text">{children}</div>
162
- {detail !== undefined && detail !== '' && <div className="text-xs wrap-anywhere text-text-muted">{detail}</div>}
177
+ <div className="wrap-break-word text-text">{children}</div>
178
+ {described && !tagged && <div className="text-2xs wrap-anywhere text-text-muted">{detail}</div>}
163
179
  </div>
180
+ {tagged && (
181
+ <Pill shape="tag" size="sm" tone={UI_TONE_PILL[tone]} className="shrink-0">
182
+ {detail}
183
+ </Pill>
184
+ )}
164
185
  </li>
165
186
  );
166
187
  }
@@ -35,3 +35,21 @@ export const UI_TONE_PILL: Readonly<Record<UiTone, NonNullable<PillProps['tone']
35
35
  warning: 'needsYou',
36
36
  danger: 'error'
37
37
  };
38
+
39
+ /* The tone at full strength, for the square that carries the icon of a Summary. */
40
+ export const UI_TONE_FILL: Readonly<Record<UiTone, string>> = {
41
+ neutral: 'bg-text-muted',
42
+ info: 'bg-status-running',
43
+ success: 'bg-positive',
44
+ warning: 'bg-status-needs-you',
45
+ danger: 'bg-status-error'
46
+ };
47
+
48
+ /* The tag at the end of a step, in words, so a state never rests on its icon alone. */
49
+ export const UI_STEP_PILL: Readonly<Record<'done' | 'running' | 'pending' | 'failed' | 'skipped', NonNullable<PillProps['tone']>>> = {
50
+ done: 'idle',
51
+ running: 'accent',
52
+ pending: 'muted',
53
+ failed: 'error',
54
+ skipped: 'muted'
55
+ };
package/src/theme.css CHANGED
@@ -30,6 +30,12 @@
30
30
  /* The two grounds of the map a page draws without tiles. */
31
31
  --map-land: #f2f1ed;
32
32
  --map-water: #d3dde5;
33
+ /* The parts of an intelligent UI block, from the tokens an app's palette sets: a tile on the thread's
34
+ ground, a card inset in a tile, and the hatch of its band. The tile mixes the ink in so lightly that
35
+ muted text keeps 4.5 to 1 on it; the design's #f0f0f3 did not. */
36
+ --chat-ui-tile: color-mix(in srgb, var(--text) 4.5%, var(--surface));
37
+ --chat-ui-inset: var(--surface);
38
+ --chat-ui-hatch: var(--separator);
33
39
  }
34
40
 
35
41
  [data-theme="dark"] {
@@ -51,6 +57,9 @@
51
57
  --chart-6: #008300;
52
58
  --map-land: #2c3135;
53
59
  --map-water: #1a2026;
60
+ --chat-ui-tile: color-mix(in srgb, var(--text) 3%, var(--surface));
61
+ --chat-ui-inset: var(--surface);
62
+ --chat-ui-hatch: var(--separator);
54
63
  }
55
64
 
56
65
  /* Output and diffs take the terminal's colors when the app loads `@adecore/terminal/terminal.css`,
@@ -82,6 +91,8 @@
82
91
  --color-chat-added: var(--chat-added);
83
92
  --color-chat-removed: var(--chat-removed);
84
93
  --color-map-marker: var(--map-marker);
94
+ --color-ui-tile: var(--chat-ui-tile);
95
+ --color-ui-inset: var(--chat-ui-inset);
85
96
  --color-map-marker-text: var(--map-marker-text);
86
97
  }
87
98
 
@@ -155,6 +166,24 @@
155
166
  @media (prefers-reduced-motion: reduce) {
156
167
  .chat-ui-changed { animation: none; }
157
168
  }
169
+ /*
170
+ * A block stands in a band across the whole thread, hatched above and below, with its tiles at the
171
+ * reading width. The thread's scroller is the container the band measures, so a block anywhere
172
+ * else keeps the width of its column.
173
+ */
174
+ .chat-scroll { container-type: inline-size; }
175
+ .chat-ui-band { position: relative; padding-block: 14px; }
176
+ .chat-ui-band::before, .chat-ui-band::after {
177
+ content: "";
178
+ position: absolute;
179
+ inset-inline: 0;
180
+ height: 14px;
181
+ border-block: 1px solid var(--border);
182
+ background-image: repeating-linear-gradient(-45deg, var(--chat-ui-hatch) 0 1px, transparent 1px 7px);
183
+ }
184
+ .chat-ui-band::before { top: 0; }
185
+ .chat-ui-band::after { bottom: 0; }
186
+ .chat-scroll .chat-ui-band::before, .chat-scroll .chat-ui-band::after { inset-inline: calc(50% - 50cqw); }
158
187
  /* A source row a citation revealed. The row only takes focus from code, since its own button takes a click. */
159
188
  .chat-ui-source:focus { animation: chat-flash 1600ms ease-out both; }
160
189
  /* A visual still on its way says so only once it takes long enough to be noticed. */
@@ -225,6 +254,15 @@
225
254
 
226
255
  /* A code block is shiki's own `<pre>`, which brings a background of its own to override. */
227
256
  .chat-code { position: relative; min-height: 38px; border-radius: 8px; border: 1px solid var(--border); background: var(--chat-code-bg); overflow: hidden; }
257
+ /* A code block in an intelligent UI block is a card inset in its tile. */
258
+ .chat-ui-code .chat-code { background: var(--chat-ui-inset); }
259
+ /* Inside a Section the section is the tile. A tile that only frames its parts goes flat, so they keep the
260
+ section's own margin, and a card among them takes the inset ground so it still reads as one. Unlayered, so it outweighs
261
+ the utilities it overrides. */
262
+ .chat-ui-section-body .bg-ui-tile { background-color: var(--chat-ui-inset); }
263
+ .chat-ui-section-body .chat-ui-flat { background-color: transparent; padding: 0; }
264
+ .chat-ui-section-body .chat-ui-flat .chat-ui-flat-inner { padding-inline: 0; }
265
+ .chat-ui-section-body .chat-ui-flat thead { background-color: var(--chat-ui-tile); }
228
266
  .chat-code pre { margin: 0; padding: round(0.625rem, 1px) round(0.75rem, 1px); overflow: auto; user-select: text; background: transparent !important; font-family: var(--font-mono); font-size: var(--text-code); line-height: var(--text-code--line-height); }
229
267
  .chat-code code { font-family: inherit; }
230
268
  .chat-code-actions { position: absolute; top: 4px; right: 4px; border-radius: 6px; background: var(--surface-raised); color: var(--text-muted); box-shadow: 0 0 0 1px var(--border); }