update chatbox
This commit is contained in:
100
src/components/ai-assistant/blocks.js
Normal file
100
src/components/ai-assistant/blocks.js
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Response blocks.
|
||||
*
|
||||
* Answers are documents, not strings. A capability decides how its information
|
||||
* is best communicated — a comparison is a table, a health check is a status
|
||||
* list, a funnel is a funnel — and the renderer draws it with design-system
|
||||
* components. That is what turns a reply into a small interactive report rather
|
||||
* than a wall of chat text.
|
||||
*
|
||||
* Blocks are plain data, so they serialize: a future remote service can return
|
||||
* exactly this shape and nothing in the UI changes.
|
||||
*/
|
||||
|
||||
/** Assembles a document, flattening nested arrays and dropping empty slots. */
|
||||
export const doc = (...blocks) => ({
|
||||
blocks: blocks.flat(Infinity).filter(Boolean),
|
||||
});
|
||||
|
||||
/** A short paragraph. Supports `**bold**` and `_italic_` inline. */
|
||||
export const text = (value) => value && { type: 'text', text: value };
|
||||
|
||||
/** A section label inside a response. */
|
||||
export const heading = (value, sub) => value && { type: 'heading', text: value, sub };
|
||||
|
||||
/** Headline figures. `items: [{ label, value, delta?, sub?, tone? }]` */
|
||||
export const kpis = (items) => items?.length && { type: 'kpis', items };
|
||||
|
||||
/** Pass/warn checks. `items: [{ label, ok, value?, note? }]` */
|
||||
export const status = (items) => items?.length && { type: 'status', items };
|
||||
|
||||
/** Labelled bars. `items: [{ label, value, weight?, max?, tone? }]` */
|
||||
export const meters = (items) => items?.length && { type: 'meters', items };
|
||||
|
||||
/**
|
||||
* A comparison or record table.
|
||||
* `columns: [{ key, label, align?, width? }]`, `rows: [{ ...cells }]`
|
||||
* A cell may be a string, a number, or `{ value, tone, badge }`.
|
||||
*/
|
||||
export const table = (columns, rows, options = {}) =>
|
||||
rows?.length && { type: 'table', columns, rows, ...options };
|
||||
|
||||
/** Stage progression. `steps: [{ label, count, rate?, lost? }]` */
|
||||
export const funnel = (steps) => steps?.length && { type: 'funnel', steps };
|
||||
|
||||
/** Bulleted or numbered points. */
|
||||
export const list = (items, options = {}) =>
|
||||
items?.filter(Boolean).length && { type: 'list', items: items.filter(Boolean), ...options };
|
||||
|
||||
/** Findings. `items: [{ tone, title, body }]` — tone: info|success|warning|risk */
|
||||
export const insights = (items) =>
|
||||
items?.filter(Boolean).length && { type: 'insights', items: items.filter(Boolean) };
|
||||
|
||||
/** Recommended next steps. `items: [{ title, body }]` */
|
||||
export const actions = (items) =>
|
||||
items?.filter(Boolean).length && { type: 'actions', items: items.filter(Boolean) };
|
||||
|
||||
/** Inline tags. `items: [{ label, tone }]` or plain strings. */
|
||||
export const badges = (items) =>
|
||||
items?.filter(Boolean).length && {
|
||||
type: 'badges',
|
||||
items: items.filter(Boolean).map((i) => (typeof i === 'string' ? { label: i } : i)),
|
||||
};
|
||||
|
||||
/** Chronology. `items: [{ title, description?, timestamp?, tone?, current? }]` */
|
||||
export const timeline = (items) => items?.length && { type: 'timeline', items };
|
||||
|
||||
/** A caveat. Always the last word on a claim, never the headline. */
|
||||
export const note = (value) => value && { type: 'note', text: value };
|
||||
|
||||
/** A single sentence answer — used by short free-text replies. */
|
||||
export const answer = (value) => doc(text(value));
|
||||
|
||||
/* ── Streaming ──────────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Splits a document into the progressive snapshots the provider streams.
|
||||
*
|
||||
* Text and heading blocks reveal word by word; structured blocks appear whole,
|
||||
* because a half-drawn table reads as broken rather than as arriving. The result
|
||||
* is a response that composes itself in a readable order.
|
||||
*/
|
||||
export function toSnapshots({ blocks }) {
|
||||
const snapshots = [];
|
||||
const settled = [];
|
||||
|
||||
for (const block of blocks) {
|
||||
if (block.type === 'text' || block.type === 'heading') {
|
||||
const tokens = String(block.text).split(/(\s+)/);
|
||||
// Three words per snapshot: fast enough to feel live, coarse enough to
|
||||
// avoid a reflow per character.
|
||||
for (let i = 6; i < tokens.length; i += 6) {
|
||||
snapshots.push([...settled, { ...block, text: tokens.slice(0, i).join('') }]);
|
||||
}
|
||||
}
|
||||
settled.push(block);
|
||||
snapshots.push([...settled]);
|
||||
}
|
||||
|
||||
return snapshots.length ? snapshots : [[]];
|
||||
}
|
||||
Reference in New Issue
Block a user