/** * @license * Copyright 2023 Nuraly, Laabidi Aymen * SPDX-License-Identifier: MIT */ import type { ChatbotPlugin } from '../core/types.js'; import type { ChatbotMessage, ChatbotArtifact, ChatbotArtifactMetadata } from '../chatbot.types.js'; import { ChatPluginBase } from './chat-plugin.js'; /** * Artifact Plugin - extracts fenced code blocks from bot messages and replaces * them with clickable placeholder cards. Works with a right-side artifact * panel rendered by the chatbot component. * * The plugin operates on *completed* bot messages (`onMessageReceived`) so * that streaming chunk-splitting cannot break partially-received code fences. * While streaming is in progress the code blocks render normally via the * MarkdownPlugin; once the full message arrives the blocks are collapsed into * compact cards. * * @example * ```typescript * const artifactPlugin = new ArtifactPlugin(); * const controller = new ChatbotCoreController({ * plugins: [new MarkdownPlugin(), artifactPlugin], * // ... * }); * ``` */ export declare class ArtifactPlugin extends ChatPluginBase implements ChatbotPlugin { readonly id = "artifact"; readonly name = "Artifact Plugin"; readonly version = "1.0.0"; /** All extracted artifacts keyed by their id */ private readonly artifacts; /** Reference to the core controller (set via onInit) */ private controller; onInit(controller: any): void; /** * Post-process a complete bot message (called from both onMessageReceived * for addMessage()-based messages and from the processing:end listener * for streamed messages). */ onMessageReceived(message: ChatbotMessage): Promise; /** * Detect fenced code blocks, extract them as artifacts, and replace each * block in the message text with a compact clickable placeholder card. */ private processMessage; /** * Match a fenced block to a host-supplied artifact row: by position first, * then by language + content, then by content alone. Returns undefined when * no row corresponds (fence is then a plain auto-extracted artifact). */ private matchArtifactRow; /** * Insert or merge an artifact by id. Existing metadata is preserved and the * incoming metadata merged on top (incoming key wins; an absent incoming key * keeps the existing value). A non-default title is never overwritten by a * generated " Snippet N". */ private upsertArtifact; private isGeneratedTitle; /** Reflect a merged artifact back into its message's stored entries. */ private syncStoredEntry; /** * Rebuild the artifact Map entries from a message that was already processed. * This is needed after thread switches: the message text/metadata are persisted * in the thread, but the in-memory artifact Map is not. */ private rebuildArtifactsFromMetadata; /** * Upsert an artifact, by `id`. Two modes: * * - **New id** (or no id): creates the artifact and drops a clickable card * into the target bot message. Requires `messageId` and `content`. * - **Existing id**: MERGES the supplied `metadata` onto the stored entry and * keeps a non-default `title`; it does NOT add a second card. This lets a * host attach `metadata.previousContent` after fence extraction already * created the artifact, so the diff view sticks. Precedence: * `message.artifacts[].metadata` > host `addArtifact` metadata > `{}`. * * The consumer ships pure data; lumenui renders the diff. * * @returns the stored artifact, or undefined when a new artifact cannot be * created (no/invalid target message or missing content). */ addArtifact(input: { id?: string; messageId?: string; language?: string; content?: string; title?: string; metadata?: ChatbotArtifactMetadata; }): ChatbotArtifact | undefined; /** Get an artifact by its id */ getArtifact(id: string): ChatbotArtifact | undefined; /** Get all stored artifacts */ getAllArtifacts(): ChatbotArtifact[]; /** Get all artifacts belonging to a specific message */ getArtifactsForMessage(messageId: string): ChatbotArtifact[]; /** * Force artifact extraction on a specific message NOW, regardless of * streaming state. Useful when structured content arrives via a * side-channel (e.g. tool result events delivered out-of-band) and the * caller wants the artifact card to appear before the stream's * `processing:end` fires. * *

Idempotent: if the message has already been processed * (`metadata.hasArtifacts === true`), this is a no-op. The regular * `processing:end` handler will also skip the message later for the * same reason, so calling this method early does NOT cause duplicate * processing on stream completion. * *

Typical usage from outside the plugin: * ```typescript * const artifactPlugin = new ArtifactPlugin(); * const controller = new ChatbotCoreController({ * plugins: [new MarkdownPlugin(), artifactPlugin], * // ... * }); * * // When a tool result arrives via a custom side-channel: * provider.onMessage('TOOL_RESULT_JSON', (msg) => { * const messages = controller.getMessages(); * const lastBotMsg = [...messages].reverse() * .find(m => m.sender === ChatbotSender.Bot); * if (lastBotMsg) { * // 1) Append the fenced content to the bot message text * controller.messageHandler.appendToBotMessage( * lastBotMsg.id, * '\n\n```json\n' + msg.content + '\n```\n\n' * ); * // 2) Trigger artifact extraction right away * artifactPlugin.processNow(lastBotMsg.id); * } * }); * ``` * * @param messageId id of the bot message to process. If not found, or * not a bot message, returns false. * @returns true when the message was processed (or rebuilt from * metadata); false when skipped or message not found. */ processNow(messageId: string): boolean; /** * Try to extract a meaningful title from the first comment line of the code. * Falls back to a generic "Code Snippet #N" title. */ private extractTitle; /** Render a compact clickable placeholder card for an artifact */ private renderPlaceholderCard; /** CSS for the placeholder cards (injected once via style tag) */ private getStyles; } //# sourceMappingURL=artifact-plugin.d.ts.map