@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
121 lines • 8.49 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import React from 'react';
import { UiActionChip } from '../genui/ui-action-chip.js';
import { useTranscriptItem, } from '../transcript/use-chat-transcript.js';
import { useAmbientRenderUi } from './ambient-ui.js';
export { formatRelativeTime } from '../transcript/model.js';
/**
* Styling-agnostic renderer for a single v7 `UIMessage`. Ports flip's full chat-item UX —
* copy, fork, regenerate, inline edit-and-resubmit, collapsible reasoning, usage line, relative
* timestamp, and tool grouping — but with ALL styling left to `classNames` and every action
* surfaced as a callback, so no design-system / icon / date library is pulled in (relative time
* uses Intl.RelativeTimeFormat).
*
* All of the logic lives in `useTranscriptItem`; this is one rendering of it. An app that wants
* different markup drives that hook and writes its own.
*/
export function MessageItem({ message, isStreaming = false, editable, onEditSubmit, regeneratable, onRegenerate, onFork, usage, createdAt, ...slots }) {
const item = useTranscriptItem({
message,
isStreaming,
...(editable !== undefined ? { editable } : {}),
...(onEditSubmit ? { onEditSubmit } : {}),
...(onFork ? { onFork } : {}),
...(regeneratable !== undefined ? { regeneratable } : {}),
...(onRegenerate ? { onRegenerate } : {}),
...(usage != null ? { getUsage: () => usage } : {}),
...(createdAt != null ? { getCreatedAt: () => createdAt } : {}),
});
return _jsx(MessageItemView, { item: item, ...slots });
}
/** The default markup for one modelled message — what `MessageItem` renders once it has an item. */
export function MessageItemView({ item, ...given }) {
// Inside a `<GenuiProvider>`, pushed components draw without a `renderUi` of their own.
const ambientRenderUi = useAmbientRenderUi();
const slots = given.renderUi === undefined && ambientRenderUi !== null
? { ...given, renderUi: ambientRenderUi }
: given;
const { classNames } = slots;
const actionRow = (_jsxs("div", { className: classNames?.actions, "data-actions-for": item.role, children: [item.copy.available ? (_jsx("button", { type: "button", onClick: item.copy.copy, className: classNames?.actionButton, title: item.copy.copied ? 'Copied!' : 'Copy message text', children: item.copy.copied ? 'Copied' : 'Copy' })) : null, item.fork.available ? (_jsx("button", { type: "button", onClick: item.fork.run, className: classNames?.actionButton, title: "Fork thread from this message", children: "Fork" })) : null, item.regenerate.available ? (_jsx("button", { type: "button", onClick: item.regenerate.run, className: classNames?.actionButton, title: "Regenerate response", children: "Regenerate" })) : null, slots.meta, !item.isUser && item.usage ? (_jsxs("span", { className: classNames?.usage, title: `Input ${item.usage.inputTokens.toLocaleString()} · Output ${item.usage.outputTokens.toLocaleString()} tokens`, children: [item.usage.costLabel, " \u00B7 ", item.usage.tokensLabel] })) : null, item.timestamp ? (_jsx("span", { className: classNames?.timestamp, title: item.timestamp.absolute, children: item.timestamp.relative })) : null] }));
if (item.edit.available) {
return _jsx(EditableUserBubble, { item: item, slots: slots, actions: actionRow });
}
const className = joinClasses(classNames?.root, classNames?.byRole?.[item.role]);
return (_jsxs("div", { className: className, "data-role": item.role, "data-streaming": item.isStreaming ? 'true' : undefined, children: [_jsx("div", { children: renderBlocks(item, slots) }), actionRow] }));
}
/** Blocks the model has no opinion on are skipped — a renderer only draws what it can draw. */
function renderBlocks(item, slots) {
const nodes = [];
for (const block of item.blocks) {
if (block.kind === 'text') {
nodes.push(_jsx("div", { className: slots.classNames?.text, children: block.uiAction !== undefined ? (_jsx(UiActionChip, { action: block.uiAction })) : slots.renderText ? (slots.renderText(block.text, { isStreaming: item.isStreaming })) : (block.text) }, block.key));
continue;
}
if (block.kind === 'reasoning') {
nodes.push(renderReasoningBlock(block, slots));
continue;
}
if (block.kind === 'files') {
nodes.push(renderFilesBlock(block, slots));
continue;
}
if (block.kind === 'tools') {
nodes.push(...renderToolBlock(item, block, slots));
continue;
}
if (block.kind === 'ui' && slots.renderUi) {
nodes.push(_jsx("div", { "data-slot": "ui", "data-component": block.component, children: slots.renderUi(block) }, block.key));
}
}
return nodes;
}
/**
* Files as content. An image is shown; anything else is a labelled link, because a renderer that
* cannot display a format is more useful pointing at it than guessing.
*/
function renderFilesBlock(block, slots) {
return (_jsx("div", { className: slots.classNames?.files, "data-files": "true", children: slots.renderFiles
? slots.renderFiles(block.files)
: block.files.map((file) => (_jsx("a", { href: file.url, target: "_blank", rel: "noreferrer", className: slots.classNames?.file, title: file.filename ?? file.mediaType, children: file.isImage ? (_jsx("img", { src: file.url, alt: file.filename ?? 'image', className: slots.classNames?.fileImage })) : ((file.filename ?? file.mediaType)) }, file.url))) }, block.key));
}
function renderReasoningBlock(block, slots) {
return (_jsxs("div", { className: slots.classNames?.reasoning, "data-reasoning": "true", "data-streaming": block.isStreaming ? 'true' : undefined, children: [_jsx("button", { type: "button", className: slots.classNames?.reasoningToggle, onClick: () => block.toggle(), "aria-expanded": block.isOpen, children: slots.reasoningLabel ?? 'Reasoning' }), block.isOpen ? (_jsx("div", { className: slots.classNames?.reasoningText, children: slots.renderReasoning
? slots.renderReasoning(block.text, {
isStreaming: block.isStreaming,
isOpen: block.isOpen,
})
: block.text })) : null] }, block.key));
}
function renderToolBlock(item, block, slots) {
if (slots.renderToolGroup) {
return [
_jsx(React.Fragment, { children: slots.renderToolGroup(block.parts, block.key) }, block.key),
];
}
if (!slots.renderToolPart) {
return [];
}
const renderToolPart = slots.renderToolPart;
return block.parts.map((part) => {
const partKey = `${item.id}-${part.toolCallId}`;
return _jsx(React.Fragment, { children: renderToolPart(part, partKey) }, partKey);
});
}
function EditableUserBubble({ item, slots, actions }) {
const { classNames } = slots;
const { edit } = item;
// A UI action is a chip, not prose: nothing to edit.
if (item.uiAction !== null) {
return (_jsxs("div", { className: joinClasses(classNames?.root, classNames?.byRole?.user), "data-role": "user", children: [_jsx("div", { className: classNames?.text, children: _jsx(UiActionChip, { action: item.uiAction }) }), actions] }));
}
if (!edit.isEditing) {
return (_jsxs("div", { className: joinClasses(classNames?.root, classNames?.byRole?.user), "data-role": "user", children: [_jsx("div", { className: classNames?.text, children: slots.renderText ? slots.renderText(item.text, { isStreaming: false }) : item.text }), _jsx("button", { type: "button", onClick: edit.start, className: classNames?.actionButton, title: "Edit & resubmit", children: "Edit" }), actions] }));
}
const textareaProps = edit.getTextareaProps();
return (_jsxs("div", { "data-editing": "true", children: [_jsx("textarea", { ...textareaProps, rows: Math.min(8, edit.draft.split('\n').length + 1), className: classNames?.editTextarea }), _jsxs("div", { className: classNames?.editActions, children: [_jsx("button", { type: "button", onClick: edit.cancel, children: "Cancel" }), _jsx("button", { type: "button", onClick: edit.save, disabled: !edit.canSave, children: "Resend" })] })] }));
}
function joinClasses(...entries) {
const joined = entries.filter((entry) => entry).join(' ');
return joined === '' ? undefined : joined;
}
//# sourceMappingURL=message-item.js.map