UNPKG

@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
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