|
|
|
|
@@ -121,14 +121,14 @@ const TONE_BADGE = {
|
|
|
|
|
|
|
|
|
|
/* ── Individual blocks ──────────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
const TextBlock = React.memo(/** @param {any} props */ ({ block }) => (
|
|
|
|
|
const TextBlock = React.memo(({ block }: any) => (
|
|
|
|
|
<p className="text-body-sm leading-relaxed text-ink-2">
|
|
|
|
|
<Inline value={block.text} />
|
|
|
|
|
</p>
|
|
|
|
|
));
|
|
|
|
|
TextBlock.displayName = 'TextBlock';
|
|
|
|
|
|
|
|
|
|
const HeadingBlock = React.memo(/** @param {any} props */ ({ block }) => (
|
|
|
|
|
const HeadingBlock = React.memo(({ block }: any) => (
|
|
|
|
|
<div className="space-y-0.5">
|
|
|
|
|
<h4 className="font-heading text-body-sm font-semibold text-ink-1">{block.text}</h4>
|
|
|
|
|
{block.sub && <p className="text-caption text-ink-3">{block.sub}</p>}
|
|
|
|
|
@@ -137,7 +137,7 @@ const HeadingBlock = React.memo(/** @param {any} props */ ({ block }) => (
|
|
|
|
|
HeadingBlock.displayName = 'HeadingBlock';
|
|
|
|
|
|
|
|
|
|
/** Headline figures. Two columns keeps the numbers large in a 380px panel. */
|
|
|
|
|
const KpisBlock = React.memo(/** @param {any} props */ ({ block }) => (
|
|
|
|
|
const KpisBlock = React.memo(({ block }: any) => (
|
|
|
|
|
<div className="grid grid-cols-2 gap-2 [[data-wide]_&]:grid-cols-3">
|
|
|
|
|
{block.items.map((item) => (
|
|
|
|
|
<div
|
|
|
|
|
@@ -172,7 +172,7 @@ const KpisBlock = React.memo(/** @param {any} props */ ({ block }) => (
|
|
|
|
|
KpisBlock.displayName = 'KpisBlock';
|
|
|
|
|
|
|
|
|
|
/** Pass/warn checks. The icon carries the state so colour is not the only cue. */
|
|
|
|
|
const StatusBlock = React.memo(/** @param {any} props */ ({ block }) => (
|
|
|
|
|
const StatusBlock = React.memo(({ block }: any) => (
|
|
|
|
|
<ul className="space-y-1.5">
|
|
|
|
|
{block.items.map((item) => (
|
|
|
|
|
<li key={item.label} className="flex items-start gap-2">
|
|
|
|
|
@@ -204,7 +204,7 @@ const StatusBlock = React.memo(/** @param {any} props */ ({ block }) => (
|
|
|
|
|
));
|
|
|
|
|
StatusBlock.displayName = 'StatusBlock';
|
|
|
|
|
|
|
|
|
|
const MetersBlock = React.memo(/** @param {any} props */ ({ block }) => (
|
|
|
|
|
const MetersBlock = React.memo(({ block }: any) => (
|
|
|
|
|
<div className="space-y-2.5">
|
|
|
|
|
{block.items.map((item) => (
|
|
|
|
|
<div key={item.label} className="space-y-1">
|
|
|
|
|
@@ -226,7 +226,7 @@ MetersBlock.displayName = 'MetersBlock';
|
|
|
|
|
* A table. Scrolls horizontally inside its own container so a wide comparison
|
|
|
|
|
* never widens the panel.
|
|
|
|
|
*/
|
|
|
|
|
const TableBlock = React.memo(/** @param {any} props */ ({ block }) => {
|
|
|
|
|
const TableBlock = React.memo(({ block }: any) => {
|
|
|
|
|
const cell = (value) => {
|
|
|
|
|
if (value == null || value === '') return <span className="text-ink-4">—</span>;
|
|
|
|
|
if (typeof value === 'object') {
|
|
|
|
|
@@ -287,7 +287,7 @@ const TableBlock = React.memo(/** @param {any} props */ ({ block }) => {
|
|
|
|
|
TableBlock.displayName = 'TableBlock';
|
|
|
|
|
|
|
|
|
|
/** Stage progression, widths relative to the largest stage. */
|
|
|
|
|
const FunnelBlock = React.memo(/** @param {any} props */ ({ block }) => {
|
|
|
|
|
const FunnelBlock = React.memo(({ block }: any) => {
|
|
|
|
|
const max = Math.max(...block.steps.map((s) => s.count), 1);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
@@ -314,7 +314,7 @@ const FunnelBlock = React.memo(/** @param {any} props */ ({ block }) => {
|
|
|
|
|
});
|
|
|
|
|
FunnelBlock.displayName = 'FunnelBlock';
|
|
|
|
|
|
|
|
|
|
const ListBlock = React.memo(/** @param {any} props */ ({ block }) => {
|
|
|
|
|
const ListBlock = React.memo(({ block }: any) => {
|
|
|
|
|
const Tag = block.ordered ? 'ol' : 'ul';
|
|
|
|
|
return (
|
|
|
|
|
<Tag className="space-y-1.5">
|
|
|
|
|
@@ -357,7 +357,7 @@ ListBlock.displayName = 'ListBlock';
|
|
|
|
|
* clickable and doing nothing. An item may carry a hint *and* an action: the
|
|
|
|
|
* hint explains the record, the action opens it.
|
|
|
|
|
*/
|
|
|
|
|
const InsightsBlock = React.memo(/** @param {any} props */ ({ block, onPrompt }) => (
|
|
|
|
|
const InsightsBlock = React.memo(({ block, onPrompt }: any) => (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{block.items.map((item, i) => {
|
|
|
|
|
const surface = cn(
|
|
|
|
|
@@ -434,7 +434,7 @@ const InsightsBlock = React.memo(/** @param {any} props */ ({ block, onPrompt })
|
|
|
|
|
InsightsBlock.displayName = 'InsightsBlock';
|
|
|
|
|
|
|
|
|
|
/** Recommended steps — numbered, because order is the recommendation. */
|
|
|
|
|
const ActionsBlock = React.memo(/** @param {any} props */ ({ block }) => (
|
|
|
|
|
const ActionsBlock = React.memo(({ block }: any) => (
|
|
|
|
|
<ol className="space-y-2">
|
|
|
|
|
{block.items.map((item, i) => (
|
|
|
|
|
<li key={i} className="flex gap-2.5 rounded-xl border border-border bg-surface px-3 py-2.5 shadow-xs">
|
|
|
|
|
@@ -453,7 +453,7 @@ const ActionsBlock = React.memo(/** @param {any} props */ ({ block }) => (
|
|
|
|
|
));
|
|
|
|
|
ActionsBlock.displayName = 'ActionsBlock';
|
|
|
|
|
|
|
|
|
|
const BadgesBlock = React.memo(/** @param {any} props */ ({ block }) => (
|
|
|
|
|
const BadgesBlock = React.memo(({ block }: any) => (
|
|
|
|
|
<div className="flex flex-wrap gap-1.5">
|
|
|
|
|
{block.items.map((item) => (
|
|
|
|
|
<Badge key={item.label} variant={TONE_BADGE[item.tone] || 'soft'} size="lg">
|
|
|
|
|
@@ -464,7 +464,7 @@ const BadgesBlock = React.memo(/** @param {any} props */ ({ block }) => (
|
|
|
|
|
));
|
|
|
|
|
BadgesBlock.displayName = 'BadgesBlock';
|
|
|
|
|
|
|
|
|
|
const TimelineBlock = React.memo(/** @param {any} props */ ({ block }) => (
|
|
|
|
|
const TimelineBlock = React.memo(({ block }: any) => (
|
|
|
|
|
<ol className="relative space-y-3">
|
|
|
|
|
{block.items.map((item, i) => (
|
|
|
|
|
<li key={i} className="flex gap-3">
|
|
|
|
|
@@ -497,7 +497,7 @@ const TimelineBlock = React.memo(/** @param {any} props */ ({ block }) => (
|
|
|
|
|
));
|
|
|
|
|
TimelineBlock.displayName = 'TimelineBlock';
|
|
|
|
|
|
|
|
|
|
const NoteBlock = React.memo(/** @param {any} props */ ({ block }) => (
|
|
|
|
|
const NoteBlock = React.memo(({ block }: any) => (
|
|
|
|
|
<p className="border-l-2 border-border pl-2.5 text-caption italic leading-relaxed text-ink-3">
|
|
|
|
|
<Inline value={block.text} />
|
|
|
|
|
</p>
|
|
|
|
|
@@ -514,7 +514,7 @@ NoteBlock.displayName = 'NoteBlock';
|
|
|
|
|
* shape the table does not carry renders nothing rather than something
|
|
|
|
|
* improvised.
|
|
|
|
|
*/
|
|
|
|
|
const SkillSectionBlock = React.memo(/** @param {any} props */ ({ block }) => {
|
|
|
|
|
const SkillSectionBlock = React.memo(({ block }: any) => {
|
|
|
|
|
const section = block.section;
|
|
|
|
|
const Component = SECTION_COMPONENTS[section?.shape || section?.type];
|
|
|
|
|
|
|
|
|
|
@@ -564,7 +564,7 @@ SkillSectionBlock.displayName = 'SkillSectionBlock';
|
|
|
|
|
* - **Not pretend to be finished.** Once approved, the block stays visible
|
|
|
|
|
* and says so. Replacing it with a tick would lose what was agreed to.
|
|
|
|
|
*/
|
|
|
|
|
const ConfirmationBlock = React.memo(/** @param {any} props */ ({ block, onConfirm }) => {
|
|
|
|
|
const ConfirmationBlock = React.memo(({ block, onConfirm }: any) => {
|
|
|
|
|
const [state, setState] = React.useState('pending');
|
|
|
|
|
|
|
|
|
|
const approve = React.useCallback(() => {
|
|
|
|
|
@@ -673,7 +673,7 @@ const RENDERERS = {
|
|
|
|
|
* blocks has consistent rhythm — a heading hugs what follows it, everything
|
|
|
|
|
* else breathes.
|
|
|
|
|
*/
|
|
|
|
|
export const ResponseDocument = React.memo(/** @param {any} props */ ({ blocks = [], streaming = false, onPrompt, onConfirm }) => (
|
|
|
|
|
export const ResponseDocument = React.memo(({ blocks = [], streaming = false, onPrompt, onConfirm }: any) => (
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{blocks.map((block, i) => {
|
|
|
|
|
const Renderer = RENDERERS[block.type];
|