@theoremjs/react 0.3.4-next.542.g59bcbcf → 0.3.4-next.548.gbb156b2
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/README.md +2 -0
- package/client/transcript-groups.d.ts +13 -1
- package/client/transcript-groups.js +30 -2
- package/hooks/use-theorem-chat.d.ts +1 -1
- package/package.json +3 -3
- package/ui/ChatComposerBar.d.ts +9 -1
- package/ui/ChatComposerBar.js +12 -1
- package/ui/ChatTranscript.d.ts +6 -0
- package/ui/ChatTranscript.js +94 -25
- package/ui/ConsoleFrame.d.ts +2 -2
- package/ui/ConsoleFrame.js +4 -3
- package/ui/labels.d.ts +5 -0
- package/ui/labels.js +5 -0
package/README.md
CHANGED
|
@@ -435,6 +435,8 @@ function MyChat() {
|
|
|
435
435
|
|
|
436
436
|
To show the transcript and the composer, use `ChatTranscript` and `ChatComposerBar` from `/ui`. You can also draw your own from `chat.blocks`.
|
|
437
437
|
|
|
438
|
+
`ChatTranscript` places `citation` blocks after the answer text by default (the Sources strip under the prose). Pass `bodyKindPlacement={{}}` when citations must stay in stream order between text segments (inline citations).
|
|
439
|
+
|
|
438
440
|
The hooks and the client write no English text. They return kinds, codes, and states. You write the words.
|
|
439
441
|
|
|
440
442
|
- A failure is a `ClientFailure`: `{ error, errorKind, errorInternal? }`. Show `error` to the user. It is the wording of the profile. `errorKind` and `errorInternal` are for your code.
|
|
@@ -58,6 +58,16 @@ export type ComposedAssistantTurn = {
|
|
|
58
58
|
/** True when the disclosure control should appear. */
|
|
59
59
|
hasTrace: boolean;
|
|
60
60
|
};
|
|
61
|
+
/** Pin a body kind before or after kinds with no placement (stream order among peers). */
|
|
62
|
+
export type BodyKindPlacement = 'first' | 'last';
|
|
63
|
+
export type ComposeAssistantTurnOptions = {
|
|
64
|
+
/**
|
|
65
|
+
* Where body kinds sit relative to each other. Omit a kind (or pass `null`)
|
|
66
|
+
* to keep stream order among peers. Pass `{}` for pure stream order.
|
|
67
|
+
* When the option is omitted entirely, {@link DEFAULT_BODY_KIND_PLACEMENT} applies.
|
|
68
|
+
*/
|
|
69
|
+
bodyKindPlacement?: Partial<Record<TranscriptBlock['kind'], BodyKindPlacement | null | undefined>>;
|
|
70
|
+
};
|
|
61
71
|
/**
|
|
62
72
|
* Blocks as turns: neighbours from the same side share one, unless a block is
|
|
63
73
|
* in `breaks`, which always starts its own. A message whose turn stopped short
|
|
@@ -86,8 +96,10 @@ export declare function workStatus(args: {
|
|
|
86
96
|
* - `text` before/between tools → narration; text and answer kinds after the
|
|
87
97
|
* latest tool → body (while streaming too, so the answer streams formatted)
|
|
88
98
|
* - No tools: thoughts still go to trace; remaining kinds → body
|
|
99
|
+
* - Body kinds then follow {@link ComposeAssistantTurnOptions.bodyKindPlacement}
|
|
100
|
+
* (default: citations last)
|
|
89
101
|
*/
|
|
90
|
-
export declare function composeAssistantTurn(blocks: readonly TranscriptBlock[]): ComposedAssistantTurn;
|
|
102
|
+
export declare function composeAssistantTurn(blocks: readonly TranscriptBlock[], options?: ComposeAssistantTurnOptions): ComposedAssistantTurn;
|
|
91
103
|
/** The trailing user group while its reply has not started streaming back. */
|
|
92
104
|
export declare function pendingPromptOf(groups: readonly TranscriptTurnGroup[]): TranscriptTurnGroup | undefined;
|
|
93
105
|
export type AssistantTurnTiming = {
|
|
@@ -16,6 +16,31 @@ function addUsage(used, more) {
|
|
|
16
16
|
const partial = !used.cost || !more.cost || used.cost.partial || more.cost.partial;
|
|
17
17
|
return { total, cost: { usd, ...(partial ? { partial: true } : {}) } };
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Theorem's Sources strip: citations under the answer. For inline citations that
|
|
21
|
+
* must stay between text segments, pass `{ bodyKindPlacement: {} }`.
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_BODY_KIND_PLACEMENT = {
|
|
24
|
+
citation: 'last',
|
|
25
|
+
};
|
|
26
|
+
/** Stable-partition body blocks by {@link BodyKindPlacement}; peers keep stream order. */
|
|
27
|
+
function placeBodyBlocks(body, placement) {
|
|
28
|
+
if (!placement)
|
|
29
|
+
return [...body];
|
|
30
|
+
const first = [];
|
|
31
|
+
const middle = [];
|
|
32
|
+
const last = [];
|
|
33
|
+
for (const block of body) {
|
|
34
|
+
const place = placement[block.kind];
|
|
35
|
+
if (place === 'first')
|
|
36
|
+
first.push(block);
|
|
37
|
+
else if (place === 'last')
|
|
38
|
+
last.push(block);
|
|
39
|
+
else
|
|
40
|
+
middle.push(block);
|
|
41
|
+
}
|
|
42
|
+
return [...first, ...middle, ...last];
|
|
43
|
+
}
|
|
19
44
|
const WORDED = new Set(WORDED_STOP_KINDS);
|
|
20
45
|
const USER_KINDS = new Set(['user-text', 'user-attachment', 'user-voice']);
|
|
21
46
|
function isUserTranscriptBlock(block) {
|
|
@@ -139,8 +164,10 @@ function classifyNonGateBlock(block, args, trace, body) {
|
|
|
139
164
|
* - `text` before/between tools → narration; text and answer kinds after the
|
|
140
165
|
* latest tool → body (while streaming too, so the answer streams formatted)
|
|
141
166
|
* - No tools: thoughts still go to trace; remaining kinds → body
|
|
167
|
+
* - Body kinds then follow {@link ComposeAssistantTurnOptions.bodyKindPlacement}
|
|
168
|
+
* (default: citations last)
|
|
142
169
|
*/
|
|
143
|
-
export function composeAssistantTurn(blocks) {
|
|
170
|
+
export function composeAssistantTurn(blocks, options) {
|
|
144
171
|
const visible = blocks.filter((block) => !isHiddenTranscriptBlock(block));
|
|
145
172
|
// why: One card at a time: the gate answered next. The step's later gates wait in the trace.
|
|
146
173
|
const nextGate = visible.find(isGatedTool);
|
|
@@ -153,10 +180,11 @@ export function composeAssistantTurn(blocks) {
|
|
|
153
180
|
for (const [index, block] of nonGate.entries()) {
|
|
154
181
|
classifyNonGateBlock(block, { hasTools, index, lastToolIndex }, trace, body);
|
|
155
182
|
}
|
|
183
|
+
const placement = options?.bodyKindPlacement ?? DEFAULT_BODY_KIND_PLACEMENT;
|
|
156
184
|
return {
|
|
157
185
|
trace,
|
|
158
186
|
gatedTools,
|
|
159
|
-
body,
|
|
187
|
+
body: placeBodyBlocks(body, placement),
|
|
160
188
|
hasTrace: trace.length > 0,
|
|
161
189
|
};
|
|
162
190
|
}
|
|
@@ -10,7 +10,7 @@ export type UseTheoremChatOptions = ChatOptions;
|
|
|
10
10
|
export declare function useTheoremChat(options: UseTheoremChatOptions): {
|
|
11
11
|
slotChoices: {
|
|
12
12
|
onChange: (name: string, value: string) => void;
|
|
13
|
-
declared: Record<string,
|
|
13
|
+
declared: Record<string, import("@theoremjs/web/client").ChatSlotDecl>;
|
|
14
14
|
values: Record<string, string>;
|
|
15
15
|
} | undefined;
|
|
16
16
|
setDraftText: (value: import("@theoremjs/web/client").ChatUpdate<string>) => void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theoremjs/react",
|
|
3
|
-
"version": "0.3.4-next.
|
|
3
|
+
"version": "0.3.4-next.548.gbb156b2",
|
|
4
4
|
"description": "React bindings for Theorem: headless hooks + transport, an Astryx chat UI, and a Web-standard server handler.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"@stylexjs/stylex": "0.19.0",
|
|
61
61
|
"react": "^19.0.0",
|
|
62
62
|
"react-dom": "^19.0.0",
|
|
63
|
-
"@theoremjs/agents": "^0.3.4-next.
|
|
64
|
-
"@theoremjs/web": "^0.3.4-next.
|
|
63
|
+
"@theoremjs/agents": "^0.3.4-next.548.gbb156b2",
|
|
64
|
+
"@theoremjs/web": "^0.3.4-next.548.gbb156b2"
|
|
65
65
|
},
|
|
66
66
|
"peerDependenciesMeta": {
|
|
67
67
|
"@astryxdesign/core": {
|
package/ui/ChatComposerBar.d.ts
CHANGED
|
@@ -4,8 +4,16 @@ import type { ClientFailure } from '@theoremjs/web/client';
|
|
|
4
4
|
/** A slot the profile declares: its allowed values, the one chosen, and how to choose another. */
|
|
5
5
|
/** The effort selector's icon. The studio's Agents list shows the same one. */
|
|
6
6
|
export declare const EFFORT_ICON: import("react").ForwardRefExoticComponent<import("@tabler/icons-react").IconProps & import("react").RefAttributes<SVGSVGElement>>;
|
|
7
|
+
export type SlotChoiceOption = {
|
|
8
|
+
value: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
};
|
|
11
|
+
export type SlotDeclView = {
|
|
12
|
+
label?: string;
|
|
13
|
+
choices: readonly SlotChoiceOption[];
|
|
14
|
+
};
|
|
7
15
|
export type SlotChoices = {
|
|
8
|
-
declared: Readonly<Record<string,
|
|
16
|
+
declared: Readonly<Record<string, SlotDeclView>>;
|
|
9
17
|
values: Readonly<Record<string, string>>;
|
|
10
18
|
onChange: (name: string, value: string) => void;
|
|
11
19
|
};
|
package/ui/ChatComposerBar.js
CHANGED
|
@@ -93,7 +93,18 @@ function SlotSelectors(props) {
|
|
|
93
93
|
if (!props.choices)
|
|
94
94
|
return null;
|
|
95
95
|
const { declared, values, onChange } = props.choices;
|
|
96
|
-
return Object.entries(declared).map(([name,
|
|
96
|
+
return Object.entries(declared).map(([name, decl]) => {
|
|
97
|
+
// why: Group title belongs in the open menu (section heading), not beside the
|
|
98
|
+
// closed trigger — same chrome as Model/Effort. Descriptions stay Astryx
|
|
99
|
+
// secondary + one-line ellipsis; wrap would blow the composer dropdown open.
|
|
100
|
+
const title = decl.label?.trim() || name;
|
|
101
|
+
const choices = decl.choices.map((choice) => ({
|
|
102
|
+
value: choice.value,
|
|
103
|
+
label: choice.value,
|
|
104
|
+
description: choice.description,
|
|
105
|
+
}));
|
|
106
|
+
return (_jsx(Selector, { label: title, isLabelHidden: true, size: "sm", variant: "ghost", startIcon: _jsx(IconAdjustmentsHorizontal, { size: 14 }), placement: "above", isDisabled: props.isDisabled, value: values[name] ?? '', options: [{ type: 'section', title, options: choices }], onChange: (value) => onChange(name, value) }, name));
|
|
107
|
+
});
|
|
97
108
|
}
|
|
98
109
|
function GenerationSelect(props) {
|
|
99
110
|
const { models, efforts } = generationOptions(props.iface, props.selectedModel);
|
package/ui/ChatTranscript.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { LabelOverrides } from '@theoremjs/agents';
|
|
|
3
3
|
import { type TranscriptBlock } from '@theoremjs/agents/interface';
|
|
4
4
|
import type { AnsweringGate } from '@theoremjs/web/client';
|
|
5
5
|
import { type ReactNode } from 'react';
|
|
6
|
+
import { type ComposeAssistantTurnOptions } from '../client/transcript-groups.ts';
|
|
6
7
|
import { type ToolDecision } from './ToolGateCard.tsx';
|
|
7
8
|
export type ChatTranscriptProps = {
|
|
8
9
|
blocks: readonly TranscriptBlock[];
|
|
@@ -32,6 +33,11 @@ export type ChatTranscriptProps = {
|
|
|
32
33
|
* on an agent tool's call row what the agent it ran used. Off by default.
|
|
33
34
|
*/
|
|
34
35
|
usage?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Where body kinds sit in a reply. Default puts `citation` last (Sources under
|
|
38
|
+
* the prose). Pass `{}` to keep stream order for inline citations.
|
|
39
|
+
*/
|
|
40
|
+
bodyKindPlacement?: ComposeAssistantTurnOptions['bodyKindPlacement'];
|
|
35
41
|
/** The profile's labels (`iface.labels`): a stopped turn says why in its words. */
|
|
36
42
|
labels?: LabelOverrides;
|
|
37
43
|
};
|
package/ui/ChatTranscript.js
CHANGED
|
@@ -4,12 +4,14 @@ import { ChatMessage, ChatMessageBubble, ChatMessageList, ChatMessageMetadata, C
|
|
|
4
4
|
import { Citation } from '@astryxdesign/core/Citation';
|
|
5
5
|
import { ClickableCard } from '@astryxdesign/core/ClickableCard';
|
|
6
6
|
import { Collapsible } from '@astryxdesign/core/Collapsible';
|
|
7
|
+
import { DropdownMenu } from '@astryxdesign/core/DropdownMenu';
|
|
7
8
|
import { HStack } from '@astryxdesign/core/HStack';
|
|
8
9
|
import { Icon } from '@astryxdesign/core/Icon';
|
|
9
10
|
import { IconButton } from '@astryxdesign/core/IconButton';
|
|
10
11
|
import { useLocale } from '@astryxdesign/core/i18n';
|
|
11
12
|
import { useLightbox } from '@astryxdesign/core/Lightbox';
|
|
12
13
|
import { Markdown } from '@astryxdesign/core/Markdown';
|
|
14
|
+
import { OverflowList } from '@astryxdesign/core/OverflowList';
|
|
13
15
|
import { Skeleton } from '@astryxdesign/core/Skeleton';
|
|
14
16
|
import { Text } from '@astryxdesign/core/Text';
|
|
15
17
|
import { Thumbnail } from '@astryxdesign/core/Thumbnail';
|
|
@@ -292,27 +294,96 @@ function MediaBlock({ block }) {
|
|
|
292
294
|
}
|
|
293
295
|
return _jsx(Text, { color: "secondary", children: block.mimeType });
|
|
294
296
|
}
|
|
295
|
-
/**
|
|
297
|
+
/**
|
|
298
|
+
* Runs of images/videos become one gallery row; consecutive citation blocks
|
|
299
|
+
* become one sources row; everything else is a row of its own.
|
|
300
|
+
*/
|
|
296
301
|
function bodyRows(t, body) {
|
|
297
302
|
const rows = [];
|
|
298
303
|
for (const block of body) {
|
|
299
304
|
const item = galleryItemOf(t, block);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
rows.push({ kind: 'gallery', items: [item] });
|
|
305
|
+
if (item)
|
|
306
|
+
addToGallery(rows, item);
|
|
307
|
+
else if (block.kind === 'citation')
|
|
308
|
+
addToSources(rows, citationsFromBlock(block));
|
|
305
309
|
else
|
|
306
310
|
rows.push({ kind: 'block', block });
|
|
307
311
|
}
|
|
308
312
|
return rows;
|
|
309
313
|
}
|
|
310
|
-
function
|
|
314
|
+
function addToGallery(rows, item) {
|
|
315
|
+
const last = rows.at(-1);
|
|
316
|
+
if (last?.kind === 'gallery')
|
|
317
|
+
last.items.push(item);
|
|
318
|
+
else
|
|
319
|
+
rows.push({ kind: 'gallery', items: [item] });
|
|
320
|
+
}
|
|
321
|
+
function addToSources(rows, citations) {
|
|
322
|
+
if (citations.length === 0)
|
|
323
|
+
return;
|
|
324
|
+
const last = rows.at(-1);
|
|
325
|
+
if (last?.kind === 'sources')
|
|
326
|
+
last.citations.push(...citations);
|
|
327
|
+
else
|
|
328
|
+
rows.push({ kind: 'sources', citations: [...citations] });
|
|
329
|
+
}
|
|
330
|
+
/** Keeps the overflow chip looking like a Citation, not a default button. */
|
|
331
|
+
const SOURCES_MORE_TRIGGER = {
|
|
332
|
+
appearance: 'none',
|
|
333
|
+
background: 'none',
|
|
334
|
+
border: 'none',
|
|
335
|
+
padding: 0,
|
|
336
|
+
margin: 0,
|
|
337
|
+
font: 'inherit',
|
|
338
|
+
color: 'inherit',
|
|
339
|
+
cursor: 'pointer',
|
|
340
|
+
display: 'inline-flex',
|
|
341
|
+
verticalAlign: 'baseline',
|
|
342
|
+
};
|
|
343
|
+
/** Favicon size matches Citation’s icon slot. */
|
|
344
|
+
const SOURCE_FAVICON = {
|
|
345
|
+
width: 16,
|
|
346
|
+
height: 16,
|
|
347
|
+
borderRadius: 9999,
|
|
348
|
+
display: 'block',
|
|
349
|
+
};
|
|
350
|
+
/**
|
|
351
|
+
* As wide as the row, and no wider — same as studio’s Add agent menu
|
|
352
|
+
* (`studio/ui/studio-screen.tsx` `ROW_WIDE` / `oneLine`).
|
|
353
|
+
*/
|
|
354
|
+
const SOURCE_TITLE_WIDE = { inlineSize: 0, minInlineSize: '100%' };
|
|
355
|
+
/** One-line source title; what does not fit shows on hover. */
|
|
356
|
+
function sourceTitle(title) {
|
|
357
|
+
return (_jsx(Text, { maxLines: 1, hasTruncateTooltip: "end", style: SOURCE_TITLE_WIDE, children: title }));
|
|
358
|
+
}
|
|
359
|
+
/** One chip; the rest open in a scrollable DropdownMenu with favicons. */
|
|
360
|
+
function Sources({ citations }) {
|
|
311
361
|
const t = useLabels();
|
|
312
|
-
const citations = citationsFromBlock(block);
|
|
313
362
|
if (citations.length === 0)
|
|
314
363
|
return null;
|
|
315
|
-
return (_jsx(
|
|
364
|
+
return (_jsx(OverflowList, { gap: 1, maxVisibleItems: 1, "aria-label": t('@theorem.transcript.sources'), overflowRenderer: (hidden) => {
|
|
365
|
+
const more = hidden.flatMap(({ index }) => {
|
|
366
|
+
const citation = citations[index];
|
|
367
|
+
return citation ? [citation] : [];
|
|
368
|
+
});
|
|
369
|
+
if (more.length === 0)
|
|
370
|
+
return null;
|
|
371
|
+
const favicon = more.find((citation) => citation.icon)?.icon;
|
|
372
|
+
const moreLabel = t('@theorem.transcript.sources_more', { count: more.length });
|
|
373
|
+
return (_jsx(DropdownMenu, { hasChevron: false,
|
|
374
|
+
// why: menuWidth is a floor; a fixed width is what makes titles ellipsize.
|
|
375
|
+
style: { width: 280 }, placement: "below", alignment: "start", renderTrigger: (props) => (_jsx("button", { type: "button", ...props, style: SOURCES_MORE_TRIGGER, "aria-label": moreLabel, children: _jsx(Citation, { source: {
|
|
376
|
+
title: moreLabel,
|
|
377
|
+
...(favicon ? { src: favicon } : {}),
|
|
378
|
+
}, number: citations.length - more.length + 1 }) })), items: more.map((citation) => ({
|
|
379
|
+
id: citation.key,
|
|
380
|
+
label: sourceTitle(citation.title),
|
|
381
|
+
...(citation.icon
|
|
382
|
+
? { icon: _jsx("img", { src: citation.icon, alt: "", style: SOURCE_FAVICON }) }
|
|
383
|
+
: {}),
|
|
384
|
+
...(citation.href ? { href: citation.href, target: '_blank' } : {}),
|
|
385
|
+
})) }));
|
|
386
|
+
}, children: citations.map((citation, i) => (_jsx(Citation, { source: { title: citation.title, url: citation.href, src: citation.icon }, number: i + 1 }, citation.key))) }));
|
|
316
387
|
}
|
|
317
388
|
/**
|
|
318
389
|
* Streamed text (the reply, or the latest thought). Astryx reveals it while
|
|
@@ -330,13 +401,12 @@ function BodyBlock({ block, streaming }) {
|
|
|
330
401
|
return _jsx(ToolCall, { tool: block.tool });
|
|
331
402
|
return _jsx(ResultBlock, { block: block });
|
|
332
403
|
}
|
|
333
|
-
/** Non-streaming answer rows: errors,
|
|
404
|
+
/** Non-streaming answer rows: errors, structured output, and media. Citations coalesce in bodyRows. */
|
|
334
405
|
function ResultBlock({ block }) {
|
|
335
406
|
switch (block.kind) {
|
|
336
407
|
case 'error':
|
|
337
|
-
return null;
|
|
338
408
|
case 'citation':
|
|
339
|
-
return
|
|
409
|
+
return null;
|
|
340
410
|
case 'structured':
|
|
341
411
|
return _jsx(ShapedData, { value: block.value });
|
|
342
412
|
case 'media':
|
|
@@ -345,21 +415,16 @@ function ResultBlock({ block }) {
|
|
|
345
415
|
return null;
|
|
346
416
|
}
|
|
347
417
|
}
|
|
348
|
-
// why: Astryx
|
|
349
|
-
//
|
|
350
|
-
const CODE_FONT_KEPT = {
|
|
351
|
-
display: 'contents',
|
|
352
|
-
'--theorem-font-code': 'var(--font-family-code)',
|
|
353
|
-
};
|
|
418
|
+
// why: Astryx draws call names in the code font; labels are sentences, so the
|
|
419
|
+
// whole tool call (row and expanded detail) uses the body font instead.
|
|
354
420
|
const CODE_FONT_AS_BODY = { '--font-family-code': 'var(--font-family-body)' };
|
|
355
|
-
const CODE_FONT_RESTORED = { '--font-family-code': 'var(--theorem-font-code)' };
|
|
356
421
|
function ToolCalls({ calls }) {
|
|
357
|
-
return
|
|
422
|
+
return _jsx(ChatToolCalls, { calls: calls, style: CODE_FONT_AS_BODY });
|
|
358
423
|
}
|
|
359
424
|
/** A call's detail: what it ran with, then what came back. */
|
|
360
425
|
function toolDetail(t, tool, result) {
|
|
361
426
|
const input = tool.edited ? (_jsx(ShapedData, { value: tool.edited.to, title: t('@theorem.transcript.tool_input_edited') })) : (_jsx(ShapedData, { value: tool.arguments, title: t('@theorem.transcript.tool_input') }));
|
|
362
|
-
return (_jsxs(VStack, { gap: 2,
|
|
427
|
+
return (_jsxs(VStack, { gap: 2, children: [_jsx(Text, { size: "sm", color: "secondary", children: tool.name }), input, result] }));
|
|
363
428
|
}
|
|
364
429
|
/** How long a finished call ran, when both ends were seen. */
|
|
365
430
|
function toolDuration(t, tool) {
|
|
@@ -493,6 +558,8 @@ function TurnStatus(props) {
|
|
|
493
558
|
function BodyRowView({ row, streaming, imageOutput, }) {
|
|
494
559
|
if (row.kind === 'block')
|
|
495
560
|
return _jsx(BodyBlock, { block: row.block, streaming: streaming });
|
|
561
|
+
if (row.kind === 'sources')
|
|
562
|
+
return _jsx(Sources, { citations: row.citations });
|
|
496
563
|
if (imageOutput)
|
|
497
564
|
return _jsx(GeneratedGallery, { items: row.items, ratio: imageOutput.ratio });
|
|
498
565
|
return _jsx(MediaGallery, { items: row.items });
|
|
@@ -508,11 +575,13 @@ function AssistantTurn(props) {
|
|
|
508
575
|
const chrome = useChromeReveal(props.isNewest === true);
|
|
509
576
|
const showUsage = useContext(ShowUsage);
|
|
510
577
|
const elapsedMs = useTurnElapsed(props.streaming, props.startedAt, props.workedMs);
|
|
511
|
-
const { trace, gatedTools, body, hasTrace } = composeAssistantTurn(props.blocks
|
|
578
|
+
const { trace, gatedTools, body, hasTrace } = composeAssistantTurn(props.blocks, props.bodyKindPlacement !== undefined
|
|
579
|
+
? { bodyKindPlacement: props.bodyKindPlacement }
|
|
580
|
+
: undefined);
|
|
512
581
|
const rows = bodyRows(t, body);
|
|
513
582
|
const status = workStatusLabel(t, workStatus({ streaming: props.streaming, hasTrace, elapsedMs }));
|
|
514
583
|
const copyText = assistantTurnCopyText(body.length > 0 ? body : props.blocks);
|
|
515
|
-
return (_jsx(ChatMessage, { sender: "assistant", name: props.handle, ...chrome.handlers, metadata: props.streaming ? undefined : (_jsx(MessageChrome, { sender: "assistant", at: props.at, copyText: copyText, error: props.error, stopped: props.stopped, usage: showUsage ? props.usage : undefined, revealed: chrome.revealed })), children: _jsxs(VStack, { gap: 3, width: "100%", children: [_jsx(TurnStatus, { status: status, trace: trace, hasTrace: hasTrace, streaming: props.streaming }), gatedTools.map((block) => (_jsx(GateCard, { block: block, handle: props.handle, handlers: props.handlers }, block.id))), keyedByContent(rows, (row) =>
|
|
584
|
+
return (_jsx(ChatMessage, { sender: "assistant", name: props.handle, ...chrome.handlers, metadata: props.streaming ? undefined : (_jsx(MessageChrome, { sender: "assistant", at: props.at, copyText: copyText, error: props.error, stopped: props.stopped, usage: showUsage ? props.usage : undefined, revealed: chrome.revealed })), children: _jsxs(VStack, { gap: 3, width: "100%", children: [_jsx(TurnStatus, { status: status, trace: trace, hasTrace: hasTrace, streaming: props.streaming }), gatedTools.map((block) => (_jsx(GateCard, { block: block, handle: props.handle, handlers: props.handlers }, block.id))), keyedByContent(rows, (row) => row.kind === 'gallery' || row.kind === 'sources' ? row.kind : row.block.kind).map(({ item: row, index: i, key }) => (_jsx(BodyRowView
|
|
516
585
|
// why: Not the block id: the committed turn re-mints ids, and a
|
|
517
586
|
// remount would cut the text reveal short.
|
|
518
587
|
, { row: row, streaming: props.streaming && i === rows.length - 1, imageOutput: props.imageOutput }, key))), _jsx(PendingImage, { imageOutput: props.imageOutput, streaming: props.streaming, rows: rows })] }) }));
|
|
@@ -543,7 +612,7 @@ function isBareFailure(group) {
|
|
|
543
612
|
export function ChatTranscript(props) {
|
|
544
613
|
return (_jsx(TheoremLabelsProvider, { children: _jsx(ShowUsage.Provider, { value: props.usage === true, children: _jsx(ChatTranscriptBody, { ...props }) }) }));
|
|
545
614
|
}
|
|
546
|
-
function ChatTranscriptBody({ blocks, handle, streaming = false, delivery, onToolDecision, answering = null, onAuthenticated, emptyState, dividers, imageOutput, labels, }) {
|
|
615
|
+
function ChatTranscriptBody({ blocks, handle, streaming = false, delivery, onToolDecision, answering = null, onAuthenticated, emptyState, dividers, imageOutput, bodyKindPlacement, labels, }) {
|
|
547
616
|
const groups = useMemo(() => groupTranscriptBlocks(blocks, dividers && new Set(Object.keys(dividers))), [blocks, dividers]);
|
|
548
617
|
const timeOf = useFirstSeen(groups.map((group, index) => (group.kind === 'user' ? group.key : replyKey(groups, index))));
|
|
549
618
|
const spans = useTurnSpans(streaming, groups.findLast((group) => group.kind === 'user')?.key);
|
|
@@ -556,7 +625,7 @@ function ChatTranscriptBody({ blocks, handle, streaming = false, delivery, onToo
|
|
|
556
625
|
onToolDecision,
|
|
557
626
|
onAuthenticated,
|
|
558
627
|
};
|
|
559
|
-
const turn = { handle, handlers, imageOutput };
|
|
628
|
+
const turn = { handle, handlers, imageOutput, bodyKindPlacement };
|
|
560
629
|
const lastUser = groups.findLastIndex((group) => group.kind === 'user');
|
|
561
630
|
const userTurn = (group, index) => {
|
|
562
631
|
const next = groups[index + 1];
|
package/ui/ConsoleFrame.d.ts
CHANGED
|
@@ -53,8 +53,8 @@ export type ConsoleFrameProps = {
|
|
|
53
53
|
children: ReactNode;
|
|
54
54
|
};
|
|
55
55
|
/**
|
|
56
|
-
* A console's page, for decisions and hosts alike: the trace
|
|
57
|
-
*
|
|
56
|
+
* A console's page, for decisions and hosts alike: the trace toggle floats over
|
|
57
|
+
* the content (same as chat), and one raised panel scrolls inside.
|
|
58
58
|
*/
|
|
59
59
|
export declare function ConsoleFrame({ inspector, maxWidth, flush, columns, requestLabel, className, style, children, }: ConsoleFrameProps): import("react").JSX.Element;
|
|
60
60
|
/** One card of a response with several: its request and result sit in it without cards of their own. */
|
package/ui/ConsoleFrame.js
CHANGED
|
@@ -41,12 +41,13 @@ function ConsoleSingle({ flush, wide, maxWidth, children, }) {
|
|
|
41
41
|
return (_jsx(VStack, { height: "100%", paddingInlineStart: PANEL_EDGE, style: { ...(flush ? FLUSH_CARD : undefined), paddingInlineEnd: paneEdge(PANEL_EDGE) }, children: _jsx(Card, { variant: "transparent", height: "100%", padding: 0, style: flush ? { overflowY: 'auto' } : { ...RAISED, overflowY: 'auto' }, children: _jsx(Center, { axis: "horizontal", width: "100%", minHeight: "100%", children: _jsx(VStack, { width: "100%", maxWidth: wide ? undefined : maxWidth, gap: 3, padding: 4, children: children }) }) }) }));
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
-
* A console's page, for decisions and hosts alike: the trace
|
|
45
|
-
*
|
|
44
|
+
* A console's page, for decisions and hosts alike: the trace toggle floats over
|
|
45
|
+
* the content (same as chat), and one raised panel scrolls inside.
|
|
46
46
|
*/
|
|
47
47
|
export function ConsoleFrame({ inspector, maxWidth, flush = false, columns = false, requestLabel, className, style, children, }) {
|
|
48
48
|
const layoutRef = useRef(null);
|
|
49
|
-
|
|
49
|
+
const header = inspector.toggle ? (_jsx(SidePanelHeader, { floating: true, children: inspector.toggle })) : undefined;
|
|
50
|
+
return (_jsx(WithTrace, { inspector: inspector, children: _jsx(Layout, { ref: layoutRef, height: "fill", className: className, style: { position: 'relative', ...style }, header: header, content: _jsx(LayoutContent, { padding: 0, isScrollable: false, children: _jsx(ConsoleBody, { layoutRef: layoutRef, flush: flush, columns: columns, maxWidth: maxWidth, requestLabel: requestLabel, children: children }) }) }) }));
|
|
50
51
|
}
|
|
51
52
|
/** The console's panels: a request and response side by side when there is room, else one column. */
|
|
52
53
|
function ConsoleBody({ layoutRef, flush, columns, maxWidth, requestLabel, children, }) {
|
package/ui/labels.d.ts
CHANGED
|
@@ -220,6 +220,11 @@ export declare const THEOREM_UI_CATALOG: {
|
|
|
220
220
|
readonly defaultMessage: "Sources";
|
|
221
221
|
readonly description: "The row of source chips (screen readers).";
|
|
222
222
|
};
|
|
223
|
+
readonly '@theorem.transcript.sources_more': {
|
|
224
|
+
readonly defaultMessage: "{count, plural, one {+# other} other {+# others}}";
|
|
225
|
+
readonly description: "Overflow chip that opens the menu of remaining source citations.";
|
|
226
|
+
readonly params: readonly ["count"];
|
|
227
|
+
};
|
|
223
228
|
readonly '@theorem.transcript.tool_input': {
|
|
224
229
|
readonly defaultMessage: "Input";
|
|
225
230
|
readonly description: "A tool call's arguments, in its detail.";
|
package/ui/labels.js
CHANGED
|
@@ -204,6 +204,11 @@ export const THEOREM_UI_CATALOG = {
|
|
|
204
204
|
defaultMessage: 'Sources',
|
|
205
205
|
description: 'The row of source chips (screen readers).',
|
|
206
206
|
},
|
|
207
|
+
'@theorem.transcript.sources_more': {
|
|
208
|
+
defaultMessage: '{count, plural, one {+# other} other {+# others}}',
|
|
209
|
+
description: 'Overflow chip that opens the menu of remaining source citations.',
|
|
210
|
+
params: ['count'],
|
|
211
|
+
},
|
|
207
212
|
'@theorem.transcript.tool_input': {
|
|
208
213
|
defaultMessage: 'Input',
|
|
209
214
|
description: "A tool call's arguments, in its detail.",
|