UNPKG

@kitn.ai/chat

Version:

Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. Authored in SolidJS.

705 lines (619 loc) 28.1 kB
import type { ExampleUsage, StoryUsage } from './types'; // Shared assistant reply used across the snippets. const REPLY = 'Async functions return Result, and the ? operator propagates errors.'; /** Actions on Hover — the bar reveals on hover via the actions-reveal prop. */ const hover: StoryUsage = { intro: 'Render an assistant message with an action bar that reveals on hover. Declare each button as a `<kc-action id icon tooltip>` child of `<kc-message>` and set `actions-reveal="hover"` — the element owns the fade (no consumer CSS). React passes the `actions` array instead; the Solid demo uses the primitives with `group`/`group-hover`.', snippets: { html: `<script type="module"> import 'https://cdn.jsdelivr.net/npm/@kitn.ai/chat/dist/kitn-chat.es.js'; </script> <kc-message id="msg" role="assistant" avatar-fallback="AI" content="${REPLY}" actions-reveal="hover"> <kc-action id="copy" icon="copy" tooltip="Copy"></kc-action> <kc-action id="like" icon="like" tooltip="Good response"></kc-action> <kc-action id="dislike" icon="dislike" tooltip="Bad response"></kc-action> <kc-action id="regenerate" icon="regenerate" tooltip="Regenerate"></kc-action> </kc-message> <script type="module"> document.getElementById('msg').addEventListener('kc-message-action', (e) => { const { messageId, action } = e.detail; console.log(messageId, action); }); </script>`, react: `import { Message } from '@kitn.ai/chat/react'; export function AssistantReply() { return ( <Message actionsReveal="hover" message={{ id: 'm1', role: 'assistant', content: '${REPLY}', actions: ['copy', 'like', 'dislike', 'regenerate'], }} onMessageAction={(e) => { const { messageId, action } = e.detail; console.log(messageId, action); }} /> ); }`, vue: `<script setup> import '@kitn.ai/chat/elements'; function onAction(e) { const { messageId, action } = e.detail; console.log(messageId, action); } </script> <template> <kc-message role="assistant" avatar-fallback="AI" content="${REPLY}" actions-reveal="hover" @kc-message-action="onAction"> <kc-action id="copy" icon="copy" tooltip="Copy"></kc-action> <kc-action id="like" icon="like" tooltip="Good response"></kc-action> <kc-action id="dislike" icon="dislike" tooltip="Bad response"></kc-action> <kc-action id="regenerate" icon="regenerate" tooltip="Regenerate"></kc-action> </kc-message> </template>`, svelte: `<script> import '@kitn.ai/chat/elements'; function onAction(e) { const { messageId, action } = e.detail; console.log(messageId, action); } </script> <kc-message role="assistant" avatar-fallback="AI" content="${REPLY}" actions-reveal="hover" on:kc-message-action={onAction}> <kc-action id="copy" icon="copy" tooltip="Copy"></kc-action> <kc-action id="like" icon="like" tooltip="Good response"></kc-action> <kc-action id="dislike" icon="dislike" tooltip="Bad response"></kc-action> <kc-action id="regenerate" icon="regenerate" tooltip="Regenerate"></kc-action> </kc-message>`, angular: `import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; @Component({ selector: 'app-reply', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: \` <kc-message role="assistant" avatar-fallback="AI" content="${REPLY}" actions-reveal="hover" (kc-message-action)="onAction($event)"> <kc-action id="copy" icon="copy" tooltip="Copy"></kc-action> <kc-action id="like" icon="like" tooltip="Good response"></kc-action> <kc-action id="dislike" icon="dislike" tooltip="Bad response"></kc-action> <kc-action id="regenerate" icon="regenerate" tooltip="Regenerate"></kc-action> </kc-message> \`, }) export class ReplyComponent { onAction(e: CustomEvent<{ messageId: string; action: string }>) { console.log(e.detail.messageId, e.detail.action); } }`, solid: `import { Message, MessageAvatar, MessageContent, MessageActions, Button, Tooltip } from '@kitn.ai/chat'; import { Copy, ThumbsUp, ThumbsDown, RefreshCw } from 'lucide-solid'; export function AssistantReply() { return ( <Message> <MessageAvatar src="" fallback="AI" alt="Assistant" /> {/* group + group-hover reveals the bar on row hover */} <div class="group flex-1 space-y-2"> <MessageContent markdown>${REPLY}</MessageContent> <MessageActions class="opacity-0 group-hover:opacity-100 transition-opacity"> <Tooltip content="Copy"><Button variant="ghost" size="icon-sm" aria-label="Copy"><Copy class="size-3.5" /></Button></Tooltip> <Tooltip content="Good response"><Button variant="ghost" size="icon-sm" aria-label="Good response"><ThumbsUp class="size-3.5" /></Button></Tooltip> <Tooltip content="Bad response"><Button variant="ghost" size="icon-sm" aria-label="Bad response"><ThumbsDown class="size-3.5" /></Button></Tooltip> <Tooltip content="Regenerate"><Button variant="ghost" size="icon-sm" aria-label="Regenerate"><RefreshCw class="size-3.5" /></Button></Tooltip> </MessageActions> </div> </Message> ); }`, }, }; /** Always Visible Actions — bar always shown; custom actions via descriptor objects. */ const alwaysVisible: StoryUsage = { intro: "Keep the bar always visible (the default). Declare each button as a `<kc-action id icon tooltip>` child — built-in-style (copy/like/…) and custom (share/bookmark) are all just `<kc-action>` elements, and `<kc-message>` fires `messageaction` with the id. (React passes them as the `actions` array.)", snippets: { html: `<script type="module"> import 'https://cdn.jsdelivr.net/npm/@kitn.ai/chat/dist/kitn-chat.es.js'; </script> <kc-message id="msg" role="assistant" avatar-fallback="AI" content="${REPLY}"> <kc-action id="copy" icon="copy" tooltip="Copy"></kc-action> <kc-action id="like" icon="like" tooltip="Good response"></kc-action> <kc-action id="dislike" icon="dislike" tooltip="Bad response"></kc-action> <kc-action id="regenerate" icon="regenerate" tooltip="Regenerate"></kc-action> <kc-action id="share" icon="share" tooltip="Share"></kc-action> <kc-action id="bookmark" icon="bookmark" tooltip="Bookmark"></kc-action> </kc-message> <script type="module"> document.getElementById('msg').addEventListener('kc-message-action', (e) => console.log(e.detail)); </script>`, react: `import { Message } from '@kitn.ai/chat/react'; export function AssistantReply() { return ( <Message message={{ id: 'm1', role: 'assistant', content: '${REPLY}', actions: [ 'copy', 'like', 'dislike', 'regenerate', { id: 'share', label: 'Share', icon: 'share' }, { id: 'bookmark', label: 'Bookmark', icon: 'bookmark' }, ], }} onMessageAction={(e) => console.log(e.detail)} /> ); }`, vue: `<script setup> import '@kitn.ai/chat/elements'; </script> <template> <kc-message role="assistant" avatar-fallback="AI" content="${REPLY}" @kc-message-action="(e) => console.log(e.detail)"> <kc-action id="copy" icon="copy" tooltip="Copy"></kc-action> <kc-action id="like" icon="like" tooltip="Good response"></kc-action> <kc-action id="dislike" icon="dislike" tooltip="Bad response"></kc-action> <kc-action id="regenerate" icon="regenerate" tooltip="Regenerate"></kc-action> <kc-action id="share" icon="share" tooltip="Share"></kc-action> <kc-action id="bookmark" icon="bookmark" tooltip="Bookmark"></kc-action> </kc-message> </template>`, svelte: `<script> import '@kitn.ai/chat/elements'; </script> <kc-message role="assistant" avatar-fallback="AI" content="${REPLY}" on:kc-message-action={(e) => console.log(e.detail)}> <kc-action id="copy" icon="copy" tooltip="Copy"></kc-action> <kc-action id="like" icon="like" tooltip="Good response"></kc-action> <kc-action id="dislike" icon="dislike" tooltip="Bad response"></kc-action> <kc-action id="regenerate" icon="regenerate" tooltip="Regenerate"></kc-action> <kc-action id="share" icon="share" tooltip="Share"></kc-action> <kc-action id="bookmark" icon="bookmark" tooltip="Bookmark"></kc-action> </kc-message>`, angular: `import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; @Component({ selector: 'app-reply', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: \` <kc-message role="assistant" avatar-fallback="AI" content="${REPLY}" (kc-message-action)="log($event)"> <kc-action id="copy" icon="copy" tooltip="Copy"></kc-action> <kc-action id="like" icon="like" tooltip="Good response"></kc-action> <kc-action id="dislike" icon="dislike" tooltip="Bad response"></kc-action> <kc-action id="regenerate" icon="regenerate" tooltip="Regenerate"></kc-action> <kc-action id="share" icon="share" tooltip="Share"></kc-action> <kc-action id="bookmark" icon="bookmark" tooltip="Bookmark"></kc-action> </kc-message> \`, }) export class ReplyComponent { log(e: CustomEvent) { console.log(e.detail); } }`, solid: `import { Message, MessageAvatar, MessageContent, MessageActions, Button, Tooltip } from '@kitn.ai/chat'; import { Copy, ThumbsUp, ThumbsDown, RefreshCw, Share, Bookmark } from 'lucide-solid'; export function AssistantReply() { return ( <Message> <MessageAvatar src="" fallback="AI" alt="Assistant" /> <div class="flex-1 space-y-2"> <MessageContent markdown>${REPLY}</MessageContent> <MessageActions> <Tooltip content="Copy"><Button variant="ghost" size="icon-sm" aria-label="Copy"><Copy class="size-3.5" /></Button></Tooltip> <Tooltip content="Good response"><Button variant="ghost" size="icon-sm" aria-label="Good response"><ThumbsUp class="size-3.5" /></Button></Tooltip> <Tooltip content="Bad response"><Button variant="ghost" size="icon-sm" aria-label="Bad response"><ThumbsDown class="size-3.5" /></Button></Tooltip> <Tooltip content="Regenerate"><Button variant="ghost" size="icon-sm" aria-label="Regenerate"><RefreshCw class="size-3.5" /></Button></Tooltip> <Tooltip content="Share"><Button variant="ghost" size="icon-sm" aria-label="Share"><Share class="size-3.5" /></Button></Tooltip> <Tooltip content="Bookmark"><Button variant="ghost" size="icon-sm" aria-label="Bookmark"><Bookmark class="size-3.5" /></Button></Tooltip> </MessageActions> </div> </Message> ); }`, }, }; /** Copy with Confirmation — the copy action swaps to a check for 2s. */ const copyConfirm: StoryUsage = { intro: 'Listen for `messageaction` with `action: "copy"` and write to the clipboard yourself — `<kc-message>` only emits the event; it does not touch the clipboard. In SolidJS you can also swap the icon to a check for 2 seconds as a visual confirmation (see the Solid tab).', snippets: { html: `<script type="module"> import 'https://cdn.jsdelivr.net/npm/@kitn.ai/chat/dist/kitn-chat.es.js'; </script> <kc-message id="msg" role="assistant" avatar-fallback="AI" content="${REPLY}"> <kc-action id="copy" icon="copy" tooltip="Copy"></kc-action> <kc-action id="like" icon="like" tooltip="Good response"></kc-action> <kc-action id="dislike" icon="dislike" tooltip="Bad response"></kc-action> </kc-message> <script type="module"> const msg = document.getElementById('msg'); msg.addEventListener('kc-message-action', (e) => { if (e.detail.action === 'copy') navigator.clipboard.writeText(msg.content); }); </script>`, react: `import { Message } from '@kitn.ai/chat/react'; const content = '${REPLY}'; export function AssistantReply() { return ( <Message message={{ id: 'm1', role: 'assistant', content, actions: ['copy', 'like', 'dislike'] }} onMessageAction={(e) => { if (e.detail.action === 'copy') navigator.clipboard.writeText(content); }} /> ); }`, vue: `<script setup> import '@kitn.ai/chat/elements'; const content = '${REPLY}'; function onAction(e) { if (e.detail.action === 'copy') navigator.clipboard.writeText(content); } </script> <template> <kc-message role="assistant" avatar-fallback="AI" :content="content" @kc-message-action="onAction"> <kc-action id="copy" icon="copy" tooltip="Copy"></kc-action> <kc-action id="like" icon="like" tooltip="Good response"></kc-action> <kc-action id="dislike" icon="dislike" tooltip="Bad response"></kc-action> </kc-message> </template>`, svelte: `<script> import '@kitn.ai/chat/elements'; const content = '${REPLY}'; function onAction(e) { if (e.detail.action === 'copy') navigator.clipboard.writeText(content); } </script> <kc-message role="assistant" avatar-fallback="AI" {content} on:kc-message-action={onAction}> <kc-action id="copy" icon="copy" tooltip="Copy"></kc-action> <kc-action id="like" icon="like" tooltip="Good response"></kc-action> <kc-action id="dislike" icon="dislike" tooltip="Bad response"></kc-action> </kc-message>`, angular: `import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; @Component({ selector: 'app-reply', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: \` <kc-message role="assistant" avatar-fallback="AI" [content]="content" (kc-message-action)="onAction($event)"> <kc-action id="copy" icon="copy" tooltip="Copy"></kc-action> <kc-action id="like" icon="like" tooltip="Good response"></kc-action> <kc-action id="dislike" icon="dislike" tooltip="Bad response"></kc-action> </kc-message> \`, }) export class ReplyComponent { content = '${REPLY}'; onAction(e: CustomEvent<{ messageId: string; action: string }>) { if (e.detail.action === 'copy') navigator.clipboard.writeText(this.content); } }`, solid: `import { createSignal } from 'solid-js'; import { Message, MessageAvatar, MessageContent, MessageActions, Button, Tooltip } from '@kitn.ai/chat'; import { Copy, Check, ThumbsUp, ThumbsDown } from 'lucide-solid'; export function AssistantReply() { const [copied, setCopied] = createSignal(false); const copy = () => { setCopied(true); setTimeout(() => setCopied(false), 2000); }; return ( <Message> <MessageAvatar src="" fallback="AI" alt="Assistant" /> <div class="flex-1 space-y-2"> <MessageContent>${REPLY}</MessageContent> <MessageActions> <Tooltip content={copied() ? 'Copied!' : 'Copy'}> <Button variant="ghost" size="icon-sm" aria-label={copied() ? 'Copied' : 'Copy'} onClick={copy}> {copied() ? <Check class="size-3.5 text-green-500" /> : <Copy class="size-3.5" />} </Button> </Tooltip> <Tooltip content="Good response"> <Button variant="ghost" size="icon-sm" aria-label="Good response"><ThumbsUp class="size-3.5" /></Button> </Tooltip> <Tooltip content="Bad response"> <Button variant="ghost" size="icon-sm" aria-label="Bad response"><ThumbsDown class="size-3.5" /></Button> </Tooltip> </MessageActions> </div> </Message> ); }`, }, }; /** Feedback Bar — the self-contained ask → optional detail → thanks flow. */ const feedbackBar: StoryUsage = { intro: 'Ask for feedback under a reply. `<kc-feedback-bar>` owns the whole flow — it asks, optionally collects a category + comment on a not-helpful vote (`collect-detail`), then confirms with a thank-you **in place** (it does not disappear on a vote; only `close` removes it). It fires `feedback` (`{ value }`), `feedbackdetail` (`{ value, category?, comment? }`), and `close`.', snippets: { html: `<script type="module"> import 'https://cdn.jsdelivr.net/npm/@kitn.ai/chat/dist/kitn-chat.es.js'; </script> <kc-feedback-bar id="fb" bar-title="Was this response helpful?" collect-detail></kc-feedback-bar> <script type="module"> const fb = document.getElementById('fb'); fb.categories = ['Inaccurate', 'Not helpful', 'Unsafe', 'Other']; // chips (a JS property) fb.addEventListener('kc-feedback', (e) => console.log(e.detail.value)); // 'helpful' | 'not-helpful' fb.addEventListener('kc-feedback-detail', (e) => console.log(e.detail)); // { value, category?, comment? } fb.addEventListener('kc-close', () => fb.remove()); </script>`, react: `import { FeedbackBar } from '@kitn.ai/chat/react'; <FeedbackBar barTitle="Was this response helpful?" collectDetail categories={['Inaccurate', 'Not helpful', 'Unsafe', 'Other']} onFeedback={(e) => console.log(e.detail.value)} onFeedbackDetail={(e) => console.log(e.detail)} // { value, category?, comment? } onClose={() => {/* dismiss */}} />`, vue: `<script setup> import '@kitn.ai/chat/elements'; function onFeedback(e) { console.log(e.detail.value); } function onDetail(e) { console.log(e.detail); } </script> <template> <kc-feedback-bar bar-title="Was this response helpful?" collect-detail :categories.prop="['Inaccurate', 'Not helpful', 'Unsafe', 'Other']" @kc-feedback="onFeedback" @kc-feedback-detail="onDetail" /> </template>`, svelte: `<script> import '@kitn.ai/chat/elements'; let fb; $: if (fb) fb.categories = ['Inaccurate', 'Not helpful', 'Unsafe', 'Other']; function onFeedback(e) { console.log(e.detail.value); } function onDetail(e) { console.log(e.detail); } </script> <kc-feedback-bar bind:this={fb} bar-title="Was this response helpful?" collect-detail on:kc-feedback={onFeedback} on:kc-feedback-detail={onDetail} />`, angular: `import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; @Component({ selector: 'app-feedback', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: \` <kc-feedback-bar bar-title="Was this response helpful?" collect-detail [categories]="categories" (kc-feedback)="onFeedback($event)" (kc-feedback-detail)="onDetail($event)"> </kc-feedback-bar> \`, }) export class FeedbackComponent { categories = ['Inaccurate', 'Not helpful', 'Unsafe', 'Other']; onFeedback(e: CustomEvent<{ value: 'helpful' | 'not-helpful' }>) { console.log(e.detail.value); } onDetail(e: CustomEvent<{ value: 'helpful' | 'not-helpful'; category?: string; comment?: string }>) { console.log(e.detail); } }`, solid: `import { createSignal, Show } from 'solid-js'; import { Message, MessageAvatar, MessageContent, FeedbackBar } from '@kitn.ai/chat'; export function AssistantReply() { const [show, setShow] = createSignal(true); return ( <Message> <MessageAvatar src="" fallback="AI" alt="Assistant" /> <div class="flex-1 space-y-3"> <MessageContent markdown>${REPLY}</MessageContent> <Show when={show()}> <FeedbackBar title="Was this response helpful?" collectDetail categories={['Inaccurate', 'Not helpful', 'Unsafe', 'Other']} onFeedback={(value) => console.log(value)} onSubmitDetail={(d) => console.log(d)} onClose={() => setShow(false)} /> </Show> </div> </Message> ); }`, }, }; /** Full Example — all stories combined: avatar + markdown + built-in/custom actions * + copy-to-clipboard with confirmation + a Feedback Bar. */ const fullExample: StoryUsage = { intro: 'Every feature combined: avatar, markdown content, the action bar (copy/regenerate plus custom **Share**/**Bookmark** with tooltips), a copy→clipboard handler with visual confirmation, and a **Feedback Bar** below. Like/dislike are omitted since the Feedback Bar covers that. Icon-only buttons get tooltips automatically; override the text with a `tooltip` field on custom actions. In SolidJS wrap each button with `<Tooltip>` (see the Solid tab).', snippets: { html: `<script type="module"> import 'https://cdn.jsdelivr.net/npm/@kitn.ai/chat/dist/kitn-chat.es.js'; </script> <div style="max-width:42rem; padding:1rem; display:flex; flex-direction:column; gap:0.5rem"> <kc-message id="msg" role="assistant" avatar-fallback="AI" content="Use anyhow::Result for apps and thiserror for libraries." > <kc-action id="copy" icon="copy" tooltip="Copy to clipboard"></kc-action> <kc-action id="regenerate" icon="regenerate" tooltip="Regenerate"></kc-action> <kc-action id="share" icon="share" tooltip="Share this response">Share</kc-action> <kc-action id="bookmark" icon="bookmark" tooltip="Save for later">Bookmark</kc-action> </kc-message> <kc-feedback-bar id="fb" bar-title="Was this response helpful?"></kc-feedback-bar> </div> <script type="module"> const msg = document.getElementById('msg'); const fb = document.getElementById('fb'); msg.addEventListener('kc-message-action', (e) => { const { action } = e.detail; if (action === 'copy') navigator.clipboard.writeText(msg.content); console.log(e.detail); }); // The bar confirms in place on a vote; only close removes it. fb.addEventListener('kc-feedback', (e) => console.log('feedback:', e.detail.value)); fb.addEventListener('kc-close', () => fb.remove()); </script>`, react: `import { useState } from 'react'; import { Message, FeedbackBar } from '@kitn.ai/chat/react'; export function AssistantReply() { const [showFeedback, setShowFeedback] = useState(true); const content = 'Use anyhow::Result for apps and thiserror for libraries.'; return ( <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', maxWidth: '42rem' }}> <Message message={{ id: 'm1', role: 'assistant', content, avatar: { fallback: 'AI', alt: 'Assistant' }, actions: [ 'copy', 'regenerate', { id: 'share', label: 'Share', icon: 'share', tooltip: 'Share this response' }, { id: 'bookmark', label: 'Bookmark', icon: 'bookmark', tooltip: 'Save for later' }, ], }} onMessageAction={(e) => { if (e.detail.action === 'copy') navigator.clipboard.writeText(content); }} /> {showFeedback && ( <FeedbackBar barTitle="Was this response helpful?" onClose={() => setShowFeedback(false)} /> )} </div> ); }`, vue: `<script setup> import { ref } from 'vue'; import '@kitn.ai/chat/elements'; const content = 'Use anyhow::Result for apps and thiserror for libraries.'; const showFeedback = ref(true); function onAction(e) { if (e.detail.action === 'copy') navigator.clipboard.writeText(content); } </script> <template> <div style="display:flex; flex-direction:column; gap:0.5rem; max-width:42rem"> <kc-message role="assistant" avatar-fallback="AI" :content="content" @kc-message-action="onAction"> <kc-action id="copy" icon="copy" tooltip="Copy to clipboard"></kc-action> <kc-action id="regenerate" icon="regenerate" tooltip="Regenerate"></kc-action> <kc-action id="share" icon="share" tooltip="Share this response">Share</kc-action> <kc-action id="bookmark" icon="bookmark" tooltip="Save for later">Bookmark</kc-action> </kc-message> <kc-feedback-bar v-if="showFeedback" bar-title="Was this response helpful?" @kc-close="showFeedback = false" /> </div> </template>`, svelte: `<script> import '@kitn.ai/chat/elements'; const content = 'Use anyhow::Result for apps and thiserror for libraries.'; let showFeedback = true; function onAction(e) { if (e.detail.action === 'copy') navigator.clipboard.writeText(content); } </script> <div style="display:flex; flex-direction:column; gap:0.5rem; max-width:42rem"> <kc-message role="assistant" avatar-fallback="AI" {content} on:kc-message-action={onAction}> <kc-action id="copy" icon="copy" tooltip="Copy to clipboard"></kc-action> <kc-action id="regenerate" icon="regenerate" tooltip="Regenerate"></kc-action> <kc-action id="share" icon="share" tooltip="Share this response">Share</kc-action> <kc-action id="bookmark" icon="bookmark" tooltip="Save for later">Bookmark</kc-action> </kc-message> {#if showFeedback} <kc-feedback-bar bar-title="Was this response helpful?" on:kc-close={() => (showFeedback = false)} /> {/if} </div>`, angular: `import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core'; @Component({ selector: 'app-reply', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: \` <div style="display:flex; flex-direction:column; gap:0.5rem; max-width:42rem"> <kc-message role="assistant" avatar-fallback="AI" [content]="content" (kc-message-action)="onAction($event)"> <kc-action id="copy" icon="copy" tooltip="Copy to clipboard"></kc-action> <kc-action id="regenerate" icon="regenerate" tooltip="Regenerate"></kc-action> <kc-action id="share" icon="share" tooltip="Share this response">Share</kc-action> <kc-action id="bookmark" icon="bookmark" tooltip="Save for later">Bookmark</kc-action> </kc-message> @if (showFeedback()) { <kc-feedback-bar bar-title="Was this response helpful?" (kc-close)="showFeedback.set(false)"> </kc-feedback-bar> } </div> \`, }) export class ReplyComponent { showFeedback = signal(true); content = 'Use anyhow::Result for apps and thiserror for libraries.'; onAction(e: CustomEvent<{ messageId: string; action: string }>) { if (e.detail.action === 'copy') navigator.clipboard.writeText(this.content); } }`, solid: `import { createSignal, Show } from 'solid-js'; import { Message, MessageAvatar, MessageContent, MessageActions, FeedbackBar, Button, Tooltip } from '@kitn.ai/chat'; import { Copy, Check, RefreshCw, Share, Bookmark } from 'lucide-solid'; export function AssistantReply() { const content = 'Use anyhow::Result for apps and thiserror for libraries.'; const [copied, setCopied] = createSignal(false); const [showFeedback, setShowFeedback] = createSignal(true); const handleCopy = () => { navigator.clipboard.writeText(content); setCopied(true); setTimeout(() => setCopied(false), 2000); }; return ( <Message> <MessageAvatar src="" fallback="AI" alt="Assistant" /> <div class="flex-1 space-y-2"> <MessageContent markdown>{content}</MessageContent> <MessageActions> <Tooltip content={copied() ? 'Copied!' : 'Copy'}> <Button variant="ghost" size="icon-sm" aria-label={copied() ? 'Copied' : 'Copy'} onClick={handleCopy}> {copied() ? <Check class="size-3.5 text-green-500" /> : <Copy class="size-3.5" />} </Button> </Tooltip> <Tooltip content="Regenerate"> <Button variant="ghost" size="icon-sm" aria-label="Regenerate"><RefreshCw class="size-3.5" /></Button> </Tooltip> <Tooltip content="Share this response"> <Button variant="ghost" size="icon-sm" aria-label="Share"><Share class="size-3.5" /></Button> </Tooltip> <Tooltip content="Save for later"> <Button variant="ghost" size="icon-sm" aria-label="Bookmark"><Bookmark class="size-3.5" /></Button> </Tooltip> </MessageActions> <Show when={showFeedback()}> <FeedbackBar title="Was this response helpful?" onClose={() => setShowFeedback(false)} /> </Show> </div> </Message> ); }`, }, }; /** * Example: Message Actions — copy / like / dislike / regenerate (and a feedback * bar) on an assistant message. Per-story: the Usage tab shows the snippet for * the story you're on; the example-level fields below are the fallback. */ const messageActions: ExampleUsage = { title: 'Examples/Message Actions', ...hover, // example-level fallback = the headline "Actions on Hover" stories: { 'Actions on Hover': hover, 'Always Visible Actions': alwaysVisible, 'Copy with Confirmation': copyConfirm, 'Feedback Bar': feedbackBar, 'Full Example': fullExample, }, }; export default messageActions;