@kitn.ai/ui 0.15.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -13
- package/bin/mcp.js +22 -0
- package/dist/bash-InADTalH.js +6 -1
- package/dist/components/artifact.d.ts +58 -0
- package/dist/components/attachments.d.ts +67 -0
- package/dist/components/card-fallback.d.ts +10 -0
- package/dist/components/card-renderer.d.ts +13 -0
- package/dist/components/card.d.ts +28 -0
- package/dist/components/chain-of-thought.d.ts +29 -0
- package/dist/components/chat-container.d.ts +19 -0
- package/dist/components/chat-scope-picker.d.ts +9 -0
- package/dist/components/chat-thread.d.ts +91 -0
- package/dist/components/checkpoint.d.ts +20 -0
- package/dist/components/choice-card.d.ts +78 -0
- package/dist/components/code-block.d.ts +16 -0
- package/dist/components/confirm-card.d.ts +57 -0
- package/dist/components/context.d.ts +65 -0
- package/dist/components/conversation-item.d.ts +8 -0
- package/dist/components/conversation-list.d.ts +11 -0
- package/dist/components/dismissed-stub.d.ts +22 -0
- package/dist/components/embed.d.ts +21 -0
- package/dist/components/empty.d.ts +43 -0
- package/dist/components/feedback-bar.d.ts +45 -0
- package/dist/components/file-tree.d.ts +57 -0
- package/dist/components/file-upload.d.ts +16 -0
- package/dist/components/form-widgets.d.ts +31 -0
- package/dist/components/form.d.ts +103 -0
- package/dist/components/image.d.ts +12 -0
- package/dist/components/link-preview.d.ts +23 -0
- package/dist/components/loader.d.ts +60 -0
- package/dist/components/markdown.d.ts +8 -0
- package/dist/components/message-skills.d.ts +15 -0
- package/dist/components/message.d.ts +111 -0
- package/dist/components/model-switcher.d.ts +8 -0
- package/dist/components/prompt-input.d.ts +38 -0
- package/dist/components/prompt-suggestion.d.ts +13 -0
- package/dist/components/reasoning.d.ts +20 -0
- package/dist/components/response-compare-types.d.ts +66 -0
- package/dist/components/response-compare.d.ts +51 -0
- package/dist/components/response-stream.d.ts +14 -0
- package/dist/components/scroll-button.d.ts +7 -0
- package/dist/components/slash-command.d.ts +15 -0
- package/dist/components/source.d.ts +23 -0
- package/dist/components/tasks-card.d.ts +72 -0
- package/dist/components/text-shimmer.d.ts +9 -0
- package/dist/components/thinking-bar.d.ts +10 -0
- package/dist/components/toast.d.ts +48 -0
- package/dist/components/tool.d.ts +16 -0
- package/dist/components/use-card-resolution.d.ts +16 -0
- package/dist/components/voice-input.d.ts +7 -0
- package/dist/core-AYMC6_lb.js +5874 -12
- package/dist/css-M7EaDHN_.js +6 -1
- package/dist/custom-elements.json +260 -11
- package/dist/elements/artifact.d.ts +1 -0
- package/dist/elements/artifact.js +1 -0
- package/dist/elements/attachments.d.ts +1 -0
- package/dist/elements/attachments.js +1 -0
- package/dist/elements/autoloader.d.ts +2 -0
- package/dist/elements/autoloader.js +1 -0
- package/dist/elements/card.d.ts +1 -0
- package/dist/elements/card.js +1 -0
- package/dist/elements/cards.d.ts +1 -0
- package/dist/elements/cards.js +1 -0
- package/dist/elements/chain-of-thought.d.ts +1 -0
- package/dist/elements/chain-of-thought.js +1 -0
- package/dist/elements/chat-scope-picker.d.ts +1 -0
- package/dist/elements/chat-scope-picker.js +1 -0
- package/dist/elements/chat-workspace.d.ts +1 -0
- package/dist/elements/chat-workspace.js +1 -0
- package/dist/elements/chat.d.ts +1 -0
- package/dist/elements/chat.js +1 -0
- package/dist/elements/checkpoint.d.ts +1 -0
- package/dist/elements/checkpoint.js +1 -0
- package/dist/elements/choice.d.ts +1 -0
- package/dist/elements/choice.js +1 -0
- package/dist/elements/chunks/Icon-D_Zmwmwh.js +1 -0
- package/dist/elements/chunks/action-icons-PVfIvcuG.js +1 -0
- package/dist/elements/chunks/attachments-Dp6vqtrc.js +1 -0
- package/dist/elements/chunks/bash-InADTalH.js +1 -0
- package/dist/elements/chunks/button-CKyYD9G4.js +1 -0
- package/dist/elements/chunks/card-Cjho-PYk.js +1 -0
- package/dist/elements/chunks/card-routing-C986KuUA.js +1 -0
- package/dist/elements/chunks/chat-thread-RT808iPf.js +1 -0
- package/dist/elements/chunks/check-CDYaMYrM.js +1 -0
- package/dist/elements/chunks/chevron-down-3SBiq8HF.js +1 -0
- package/dist/elements/chunks/chevron-right-k2UFlIVK.js +1 -0
- package/dist/elements/chunks/cn-Dogu6PbV.js +1 -0
- package/dist/elements/chunks/code-block-BYUmfFSd.js +1 -0
- package/dist/elements/chunks/collapsible-BiNwdOn6.js +1 -0
- package/dist/elements/chunks/context-18g6oi4J.js +1 -0
- package/dist/elements/chunks/conversation-list-D9Y0NVdo.js +1 -0
- package/dist/elements/chunks/core-AYMC6_lb.js +12 -0
- package/dist/elements/chunks/css-M7EaDHN_.js +1 -0
- package/dist/elements/chunks/default-input-CSAoYYMd.js +1 -0
- package/dist/elements/chunks/download-D8K0Je8r.js +1 -0
- package/dist/elements/chunks/dropdown-bUs2iTia.js +1 -0
- package/dist/elements/chunks/engine-javascript-vq0WuIJl.js +141 -0
- package/dist/elements/chunks/external-link-6mA0wxab.js +1 -0
- package/dist/elements/chunks/file-text-DnaYk3Dm.js +1 -0
- package/dist/elements/chunks/file-tree-CgtRLlxK.js +1 -0
- package/dist/elements/chunks/github-dark-dimmed-DUshB20C.js +1 -0
- package/dist/elements/chunks/github-light-JYsPkUQd.js +1 -0
- package/dist/elements/chunks/hover-card-CIGw62gP.js +1 -0
- package/dist/elements/chunks/html-CPZ3oZQ7.js +1 -0
- package/dist/elements/chunks/index-CTBEJeo6.js +1 -0
- package/dist/elements/chunks/javascript-C25yR2R2.js +1 -0
- package/dist/elements/chunks/json-DxJze_jm.js +1 -0
- package/dist/elements/chunks/link-BlGSeoPy.js +1 -0
- package/dist/elements/chunks/loader-DvhR4qa0.js +1 -0
- package/dist/elements/chunks/markdown-CMTGx9r8.js +64 -0
- package/dist/elements/chunks/message-CDZwXjhL.js +1 -0
- package/dist/elements/chunks/message-feedback-CYx81QiA.js +1 -0
- package/dist/elements/chunks/model-switcher-O_tUhoOT.js +1 -0
- package/dist/elements/chunks/overlay-DydXYMQW.js +1 -0
- package/dist/elements/chunks/prompt-suggestion-ldlWV5kd.js +1 -0
- package/dist/elements/chunks/reasoning-2PUnHlmr.js +1 -0
- package/dist/elements/chunks/resizable-B5WNbSeb.js +1 -0
- package/dist/elements/chunks/star-t9tI-P0q.js +1 -0
- package/dist/elements/chunks/store-C6rIooZz.js +1 -0
- package/dist/elements/chunks/svelte--5p79yCD.js +1 -0
- package/dist/elements/chunks/text-shimmer-uh9V_KUh.js +1 -0
- package/dist/elements/chunks/textarea-3nm6Ia9U.js +1 -0
- package/dist/elements/chunks/thumbs-up-BBtguGld.js +1 -0
- package/dist/elements/chunks/toast-store-D8cL3fWl.js +1 -0
- package/dist/elements/chunks/tool-GJ3vwp1U.js +1 -0
- package/dist/elements/chunks/tooltip-DwKAz4XR.js +1 -0
- package/dist/elements/chunks/triangle-alert-Ct1zt2JL.js +1 -0
- package/dist/elements/chunks/tsx-B8rCNbgL.js +1 -0
- package/dist/elements/chunks/typescript-RycA9KXf.js +1 -0
- package/dist/elements/chunks/use-card-resolution-D-_0Alzy.js +1 -0
- package/dist/elements/chunks/video-mVYiM99-.js +1 -0
- package/dist/elements/chunks/vue-BmIZj4XD.js +1 -0
- package/dist/elements/chunks/x-CzxL27Ch.js +1 -0
- package/dist/elements/code-block.d.ts +1 -0
- package/dist/elements/code-block.js +1 -0
- package/dist/elements/compare.d.ts +1 -0
- package/dist/elements/compare.js +1 -0
- package/dist/elements/confirm-card.d.ts +1 -0
- package/dist/elements/confirm-card.js +1 -0
- package/dist/elements/context-meter.d.ts +1 -0
- package/dist/elements/context-meter.js +1 -0
- package/dist/elements/conversation-list.d.ts +1 -0
- package/dist/elements/conversation-list.js +1 -0
- package/dist/elements/embed.d.ts +1 -0
- package/dist/elements/embed.js +1 -0
- package/dist/elements/empty.d.ts +1 -0
- package/dist/elements/empty.js +1 -0
- package/dist/elements/feedback-bar.d.ts +1 -0
- package/dist/elements/feedback-bar.js +1 -0
- package/dist/elements/file-tree.d.ts +1 -0
- package/dist/elements/file-tree.js +1 -0
- package/dist/elements/file-upload.d.ts +1 -0
- package/dist/elements/file-upload.js +1 -0
- package/dist/elements/form.d.ts +1 -0
- package/dist/elements/form.js +1 -0
- package/dist/elements/image.d.ts +1 -0
- package/dist/elements/image.js +1 -0
- package/dist/elements/link-preview.d.ts +1 -0
- package/dist/elements/link-preview.js +1 -0
- package/dist/elements/loader.d.ts +1 -0
- package/dist/elements/loader.js +1 -0
- package/dist/elements/markdown.d.ts +1 -0
- package/dist/elements/markdown.js +1 -0
- package/dist/elements/message-skills.d.ts +1 -0
- package/dist/elements/message-skills.js +1 -0
- package/dist/elements/message.d.ts +1 -0
- package/dist/elements/message.js +1 -0
- package/dist/elements/model-switcher.d.ts +1 -0
- package/dist/elements/model-switcher.js +1 -0
- package/dist/elements/popover.d.ts +1 -0
- package/dist/elements/popover.js +1 -0
- package/dist/elements/prompt-input.d.ts +1 -0
- package/dist/elements/prompt-input.js +1 -0
- package/dist/elements/prompt-suggestions.d.ts +1 -0
- package/dist/elements/prompt-suggestions.js +1 -0
- package/dist/elements/reasoning.d.ts +1 -0
- package/dist/elements/reasoning.js +1 -0
- package/dist/elements/resizable.d.ts +1 -0
- package/dist/elements/resizable.js +1 -0
- package/dist/elements/response-stream.d.ts +1 -0
- package/dist/elements/response-stream.js +14 -0
- package/dist/elements/scroll-button.d.ts +1 -0
- package/dist/elements/scroll-button.js +1 -0
- package/dist/elements/source.d.ts +1 -0
- package/dist/elements/source.js +1 -0
- package/dist/elements/switch.d.ts +1 -0
- package/dist/elements/switch.js +1 -0
- package/dist/elements/tasks.d.ts +1 -0
- package/dist/elements/tasks.js +1 -0
- package/dist/elements/text-shimmer.d.ts +1 -0
- package/dist/elements/text-shimmer.js +1 -0
- package/dist/elements/thinking-bar.d.ts +1 -0
- package/dist/elements/thinking-bar.js +1 -0
- package/dist/elements/toast.d.ts +1 -0
- package/dist/elements/toast.js +1 -0
- package/dist/elements/tool.d.ts +1 -0
- package/dist/elements/tool.js +1 -0
- package/dist/elements/voice-input.d.ts +1 -0
- package/dist/elements/voice-input.js +1 -0
- package/dist/elements.d.ts +768 -0
- package/dist/engine-javascript-vq0WuIJl.js +2516 -14
- package/dist/github-dark-dimmed-DUshB20C.js +4 -1
- package/dist/github-light-JYsPkUQd.js +4 -1
- package/dist/html-CPZ3oZQ7.js +10 -1
- package/dist/index.d.ts +124 -2
- package/dist/index.js +15593 -0
- package/dist/javascript-C25yR2R2.js +6 -1
- package/dist/json-DxJze_jm.js +6 -1
- package/dist/kitn-chat.es.js +1 -77
- package/dist/llms/llms-full.txt +57 -12
- package/dist/llms/llms.txt +3 -3
- package/dist/mcp.es.js +2296 -0
- package/dist/primitives/card-contract.d.ts +119 -0
- package/dist/primitives/card-host.d.ts +12 -0
- package/dist/primitives/card-recovery.d.ts +55 -0
- package/dist/primitives/card-registry.d.ts +17 -0
- package/dist/primitives/card-resolution.d.ts +8 -0
- package/dist/primitives/card-routing.d.ts +12 -0
- package/dist/primitives/card-validate.d.ts +25 -0
- package/dist/primitives/chat-config.d.ts +31 -0
- package/dist/primitives/controllable.d.ts +13 -0
- package/dist/primitives/embed-providers.d.ts +65 -0
- package/dist/primitives/highlighter.d.ts +23 -0
- package/dist/primitives/link-preview.d.ts +47 -0
- package/dist/primitives/message-feedback.d.ts +35 -0
- package/dist/primitives/pdf-preview.d.ts +50 -0
- package/dist/primitives/toast-store.d.ts +88 -0
- package/dist/primitives/use-auto-resize.d.ts +8 -0
- package/dist/primitives/use-resize-observer.d.ts +1 -0
- package/dist/primitives/use-stick-to-bottom.d.ts +5 -0
- package/dist/primitives/use-text-stream.d.ts +19 -0
- package/dist/primitives/use-voice-recorder.d.ts +9 -0
- package/dist/react/index.d.ts +1198 -0
- package/dist/react/runtime.d.ts +15 -0
- package/dist/react.js +275 -0
- package/dist/register-JyTglR6m.js +1 -0
- package/dist/register-impl-C6wlSsts.js +77 -0
- package/dist/remote/host-embed.d.ts +33 -0
- package/dist/remote/index.d.ts +2 -0
- package/dist/remote/origin.d.ts +5 -0
- package/dist/remote/provider-runtime.d.ts +27 -0
- package/dist/remote/provider.d.ts +2 -0
- package/dist/remote/validate.d.ts +4 -0
- package/dist/remote/version.d.ts +4 -0
- package/dist/remote/wire.d.ts +53 -0
- package/dist/schemas/choice.schema.json +5 -0
- package/dist/schemas/tasks.schema.json +5 -0
- package/dist/svelte--5p79yCD.js +15 -1
- package/dist/tsx-B8rCNbgL.js +6 -1
- package/dist/types.d.ts +42 -0
- package/dist/typescript-RycA9KXf.js +6 -1
- package/dist/ui/action-icons.d.ts +11 -0
- package/dist/ui/avatar.d.ts +8 -0
- package/dist/ui/badge.d.ts +9 -0
- package/dist/ui/button.d.ts +10 -0
- package/dist/ui/collapsible.d.ts +27 -0
- package/dist/ui/dropdown.d.ts +20 -0
- package/dist/ui/hover-card.d.ts +28 -0
- package/dist/ui/overlay.d.ts +67 -0
- package/dist/ui/popover.d.ts +32 -0
- package/dist/ui/resizable.d.ts +76 -0
- package/dist/ui/scroll-area.d.ts +5 -0
- package/dist/ui/separator.d.ts +5 -0
- package/dist/ui/skeleton.d.ts +5 -0
- package/dist/ui/switch.d.ts +19 -0
- package/dist/ui/textarea.d.ts +6 -0
- package/dist/ui/tooltip.d.ts +9 -0
- package/dist/utils/cn.d.ts +2 -0
- package/dist/vue-BmIZj4XD.js +33 -1
- package/frameworks/react/index.tsx +67 -11
- package/frameworks/react/runtime.tsx +16 -2
- package/llms-full.txt +57 -12
- package/llms.txt +3 -3
- package/package.json +36 -14
- package/src/agent-tooling/archetypes.ts +46 -0
- package/src/agent-tooling/integrations/cloudflare.ts +88 -0
- package/src/agent-tooling/integrations/langgraph.ts +60 -0
- package/src/agent-tooling/integrations/mastra.ts +27 -0
- package/src/agent-tooling/integrations/mock.ts +34 -0
- package/src/agent-tooling/integrations/ollama.ts +47 -0
- package/src/agent-tooling/integrations/openrouter.ts +26 -0
- package/src/agent-tooling/integrations/pi.ts +40 -0
- package/src/agent-tooling/integrations/pydantic-ai.ts +52 -0
- package/src/agent-tooling/integrations/vercel-ai-sdk.ts +44 -0
- package/src/agent-tooling/mcp/manifest.ts +128 -0
- package/src/agent-tooling/mcp/server.ts +59 -0
- package/src/agent-tooling/mcp/stdio.ts +18 -0
- package/src/agent-tooling/mcp/tools/debug.ts +489 -0
- package/src/agent-tooling/mcp/tools/reference.ts +132 -0
- package/src/agent-tooling/mcp/tools/scaffold.ts +1759 -0
- package/src/agent-tooling/mcp/tools/theme.ts +340 -0
- package/src/agent-tooling/mcp/tools/types.ts +14 -0
- package/src/agent-tooling/registry.ts +41 -0
- package/src/agent-tooling/types.ts +30 -0
- package/src/components/chat-thread.tsx +27 -35
- package/src/components/choice-card.tsx +33 -4
- package/src/components/component-meta.json +250 -26
- package/src/components/confirm-card.tsx +15 -1
- package/src/components/dismissed-stub.tsx +80 -0
- package/src/components/form.tsx +21 -3
- package/src/components/message.tsx +151 -10
- package/src/components/response-compare-types.ts +127 -0
- package/src/components/response-compare.tsx +431 -0
- package/src/components/tasks-card.tsx +33 -3
- package/src/components/toast.tsx +398 -0
- package/src/components/use-card-resolution.ts +24 -8
- package/src/elements/autoloader.ts +89 -0
- package/src/elements/chat-types.ts +7 -0
- package/src/elements/chat-workspace.tsx +4 -2
- package/src/elements/chat.tsx +4 -2
- package/src/elements/compare.tsx +88 -0
- package/src/elements/compiled.css +1 -1
- package/src/elements/define.tsx +7 -1
- package/src/elements/element-manifest.json +182 -0
- package/src/elements/element-meta.json +191 -68
- package/src/elements/element-types.d.ts +43 -6
- package/src/elements/framework-usage.json +25 -0
- package/src/elements/message.tsx +27 -45
- package/src/elements/register-impl.ts +58 -0
- package/src/elements/register.ts +26 -51
- package/src/elements/remote.tsx +4 -0
- package/src/elements/styles.css +19 -0
- package/src/elements/toast.tsx +74 -0
- package/src/index.ts +30 -0
- package/src/primitives/card-contract.ts +14 -5
- package/src/primitives/card-recovery.ts +149 -0
- package/src/primitives/card-routing.ts +3 -0
- package/src/primitives/card-schemas/choice.schema.json +5 -0
- package/src/primitives/card-schemas/tasks.schema.json +5 -0
- package/src/primitives/message-feedback.ts +123 -0
- package/src/primitives/toast-store.ts +235 -0
- package/src/ui/tooltip.tsx +8 -2
- package/theme.css +4 -2
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { cn } from '../utils/cn';
|
|
13
13
|
import { Button } from '../ui/button';
|
|
14
14
|
import { Card } from './card';
|
|
15
|
+
import { DismissedStub } from './dismissed-stub';
|
|
15
16
|
import type { CardEnvelope, CardEvent, CardHost, CardResolution } from '../primitives/card-contract';
|
|
16
17
|
import { useCardResolution } from './use-card-resolution';
|
|
17
18
|
import { emitCardEvent } from '../primitives/card-routing';
|
|
@@ -208,7 +209,15 @@ export function ConfirmCard(props: ConfirmCardProps): JSX.Element {
|
|
|
208
209
|
});
|
|
209
210
|
};
|
|
210
211
|
|
|
211
|
-
|
|
212
|
+
// Dismiss: emit the contract `dismiss` AND optimistically flip to a `dismissed`
|
|
213
|
+
// resolution so the card collapses to its re-openable stub immediately (the host
|
|
214
|
+
// confirms/overrides via the resolution prop on the next render).
|
|
215
|
+
const onDismiss = (): void => {
|
|
216
|
+
if (res.isResolved()) return;
|
|
217
|
+
emit({ kind: 'dismiss', cardId: local.cardId });
|
|
218
|
+
res.setLocal({ kind: 'dismissed' });
|
|
219
|
+
};
|
|
220
|
+
const onReopen = (): void => emit({ kind: 'reopen', cardId: local.cardId });
|
|
212
221
|
|
|
213
222
|
let bodyRef: HTMLDivElement | undefined;
|
|
214
223
|
|
|
@@ -229,6 +238,10 @@ export function ConfirmCard(props: ConfirmCardProps): JSX.Element {
|
|
|
229
238
|
return <Card heading={local.heading} errorMessage="The card failed to render." />;
|
|
230
239
|
}}
|
|
231
240
|
>
|
|
241
|
+
<Show
|
|
242
|
+
when={!res.isDeferred()}
|
|
243
|
+
fallback={<DismissedStub type={CONFIRM_CARD_TYPE} title={local.heading} onReopen={onReopen} />}
|
|
244
|
+
>
|
|
232
245
|
<Card
|
|
233
246
|
heading={local.heading}
|
|
234
247
|
actions={
|
|
@@ -309,6 +322,7 @@ export function ConfirmCard(props: ConfirmCardProps): JSX.Element {
|
|
|
309
322
|
</Show>
|
|
310
323
|
</div>
|
|
311
324
|
</Card>
|
|
325
|
+
</Show>
|
|
312
326
|
</ErrorBoundary>
|
|
313
327
|
</Show>
|
|
314
328
|
);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type JSX, Show } from 'solid-js';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
import { Button } from '../ui/button';
|
|
4
|
+
import { Card } from './card';
|
|
5
|
+
import { RotateCcw } from 'lucide-solid';
|
|
6
|
+
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
// DismissedStub — the collapsed, re-openable read-only view a card renders once
|
|
9
|
+
// the user has dismissed it (a DEFERRED resolution, not a terminal one). It's a
|
|
10
|
+
// dense `Card` showing "<intent>: <title> — dismissed" + a ghost Reopen button.
|
|
11
|
+
// Pure/presentational: the owning card wires `onReopen` to emit `{kind:'reopen'}`.
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/** The card types that render a dismissed stub, mapped to their intent verb. */
|
|
15
|
+
export type DismissedCardType = 'confirm' | 'choice' | 'form' | 'tasks';
|
|
16
|
+
|
|
17
|
+
const INTENT_LABELS: Record<DismissedCardType, string> = {
|
|
18
|
+
confirm: 'Proposed',
|
|
19
|
+
choice: 'Choose',
|
|
20
|
+
form: 'Form',
|
|
21
|
+
tasks: 'Tasks',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Map a card `type` to its dismissed-stub intent label ("Proposed:", "Choose:",
|
|
25
|
+
* "Form:", "Tasks:"). Falls back to a capitalized type for unknown card types. */
|
|
26
|
+
export function stubIntent(type: string): string {
|
|
27
|
+
const known = INTENT_LABELS[type as DismissedCardType];
|
|
28
|
+
if (known) return `${known}:`;
|
|
29
|
+
if (type.length === 0) return 'Card:';
|
|
30
|
+
return `${type.charAt(0).toUpperCase()}${type.slice(1)}:`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DismissedStubProps {
|
|
34
|
+
/** The card type, used to derive the intent label ("Proposed:" etc.). */
|
|
35
|
+
type: string;
|
|
36
|
+
/** The card's title/heading; shown after the intent label. Falls back gracefully. */
|
|
37
|
+
title?: string;
|
|
38
|
+
/** Re-open handler. The owning card emits `{ kind:'reopen', cardId }`. */
|
|
39
|
+
onReopen: () => void;
|
|
40
|
+
/** The Reopen button label. Default 'Reopen'. */
|
|
41
|
+
reopenLabel?: string;
|
|
42
|
+
class?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* `DismissedStub` — the compact stub a card collapses to after it's been dismissed.
|
|
47
|
+
* Group-labelled for a11y; the Reopen affordance is a ghost button.
|
|
48
|
+
*/
|
|
49
|
+
export function DismissedStub(props: DismissedStubProps): JSX.Element {
|
|
50
|
+
const intent = (): string => stubIntent(props.type);
|
|
51
|
+
const title = (): string => props.title ?? 'this card';
|
|
52
|
+
const label = (): string => props.reopenLabel ?? 'Reopen';
|
|
53
|
+
const groupLabel = (): string => `${intent()} ${title()} — dismissed`;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Card dense class={props.class}>
|
|
57
|
+
<div
|
|
58
|
+
role="group"
|
|
59
|
+
aria-label={groupLabel()}
|
|
60
|
+
class="flex flex-wrap items-center gap-x-2 gap-y-1"
|
|
61
|
+
>
|
|
62
|
+
<span class="min-w-0 text-sm text-muted-foreground">
|
|
63
|
+
<span class="font-medium text-foreground">{intent()}</span>{' '}
|
|
64
|
+
<span class="text-foreground">{title()}</span>
|
|
65
|
+
<span class="text-muted-foreground"> — dismissed</span>
|
|
66
|
+
</span>
|
|
67
|
+
<Button
|
|
68
|
+
type="button"
|
|
69
|
+
variant="ghost"
|
|
70
|
+
size="sm"
|
|
71
|
+
class={cn('ml-auto gap-1.5')}
|
|
72
|
+
onClick={() => props.onReopen()}
|
|
73
|
+
>
|
|
74
|
+
<RotateCcw size={14} aria-hidden="true" />
|
|
75
|
+
<Show when={label()}>{label()}</Show>
|
|
76
|
+
</Button>
|
|
77
|
+
</div>
|
|
78
|
+
</Card>
|
|
79
|
+
);
|
|
80
|
+
}
|
package/src/components/form.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import { createStore, produce, unwrap } from 'solid-js/store';
|
|
|
14
14
|
import { cn } from '../utils/cn';
|
|
15
15
|
import { Button } from '../ui/button';
|
|
16
16
|
import { Card } from './card';
|
|
17
|
+
import { DismissedStub } from './dismissed-stub';
|
|
17
18
|
import {
|
|
18
19
|
validateAgainstSchema,
|
|
19
20
|
type JsonSchema,
|
|
@@ -437,11 +438,12 @@ export function Form(props: FormProps): JSX.Element {
|
|
|
437
438
|
else emit({ kind: 'error', cardId: local.cardId, message: state.message });
|
|
438
439
|
}));
|
|
439
440
|
|
|
440
|
-
// Surface the resolved state for host styling.
|
|
441
|
+
// Surface the resolved state for host styling. Only a `submit` resolution is the
|
|
442
|
+
// "submitted" state; a deferred `dismissed` (or `expired`) is not.
|
|
441
443
|
createEffect(() => {
|
|
442
444
|
const el = local.hostElement;
|
|
443
445
|
if (!el) return;
|
|
444
|
-
if (res.
|
|
446
|
+
if (res.resolution()?.kind === 'submit') el.setAttribute('data-kai-resolved', 'submitted');
|
|
445
447
|
else el.removeAttribute('data-kai-resolved');
|
|
446
448
|
});
|
|
447
449
|
|
|
@@ -488,6 +490,15 @@ export function Form(props: FormProps): JSX.Element {
|
|
|
488
490
|
res.setLocal({ kind: 'submit', data: out });
|
|
489
491
|
};
|
|
490
492
|
|
|
493
|
+
// Dismiss: emit `dismiss` AND optimistically flip to a `dismissed` resolution so
|
|
494
|
+
// the form collapses to its re-openable stub immediately.
|
|
495
|
+
const onDismiss = (): void => {
|
|
496
|
+
if (res.isResolved()) return;
|
|
497
|
+
emit({ kind: 'dismiss', cardId: local.cardId });
|
|
498
|
+
res.setLocal({ kind: 'dismissed' });
|
|
499
|
+
};
|
|
500
|
+
const onReopen = (): void => emit({ kind: 'reopen', cardId: local.cardId });
|
|
501
|
+
|
|
491
502
|
const actions = createMemo(() => def()['x-kai-actions'] ?? []);
|
|
492
503
|
const submitLabel = () => def()['x-kai-submitLabel'] ?? 'Submit';
|
|
493
504
|
const dismissible = () => def()['x-kai-dismissible'] === true;
|
|
@@ -509,6 +520,12 @@ export function Form(props: FormProps): JSX.Element {
|
|
|
509
520
|
return <Card heading={local.heading} errorMessage="The form failed to render." />;
|
|
510
521
|
}}
|
|
511
522
|
>
|
|
523
|
+
<Show
|
|
524
|
+
when={!res.isDeferred()}
|
|
525
|
+
fallback={
|
|
526
|
+
<DismissedStub type="form" title={local.heading ?? def().title} onReopen={onReopen} />
|
|
527
|
+
}
|
|
528
|
+
>
|
|
512
529
|
<Card
|
|
513
530
|
heading={local.heading ?? def().title}
|
|
514
531
|
description={def().description}
|
|
@@ -522,7 +539,7 @@ export function Form(props: FormProps): JSX.Element {
|
|
|
522
539
|
<Button
|
|
523
540
|
type="button"
|
|
524
541
|
variant="ghost"
|
|
525
|
-
onClick={
|
|
542
|
+
onClick={onDismiss}
|
|
526
543
|
>
|
|
527
544
|
Dismiss
|
|
528
545
|
</Button>
|
|
@@ -575,6 +592,7 @@ export function Form(props: FormProps): JSX.Element {
|
|
|
575
592
|
</form>
|
|
576
593
|
</Show>
|
|
577
594
|
</Card>
|
|
595
|
+
</Show>
|
|
578
596
|
</ErrorBoundary>
|
|
579
597
|
</Show>
|
|
580
598
|
);
|
|
@@ -5,8 +5,11 @@ import { cn } from "../utils/cn";
|
|
|
5
5
|
import { Markdown } from "./markdown";
|
|
6
6
|
import { Button } from "../ui/button";
|
|
7
7
|
import { actionIcon, BUILTIN_ACTION_LABEL } from "../ui/action-icons";
|
|
8
|
-
import type { ChatMessageAction, CustomAction } from "../elements/chat-types";
|
|
8
|
+
import type { ChatMessageAction, CustomAction, FeedbackVote } from "../elements/chat-types";
|
|
9
9
|
import { useChatConfig, textClass } from "../primitives/chat-config";
|
|
10
|
+
import { Reasoning, ReasoningTrigger, ReasoningContent } from "./reasoning";
|
|
11
|
+
import { Tool, type ToolPart } from "./tool";
|
|
12
|
+
import { Attachments, Attachment, AttachmentPreview, AttachmentInfo, type AttachmentData } from "./attachments";
|
|
10
13
|
|
|
11
14
|
// --- Message ---
|
|
12
15
|
|
|
@@ -119,6 +122,13 @@ export interface MessageActionBarProps {
|
|
|
119
122
|
reveal?: 'always' | 'hover';
|
|
120
123
|
/** Fired with the built-in name or the custom action id when a button is clicked. */
|
|
121
124
|
onAction: (id: string) => void;
|
|
125
|
+
/** The active feedback vote. When `'like'`/`'dislike'` is set, that button is
|
|
126
|
+
* marked pressed (filled) and the OTHER vote button animates out. `undefined`
|
|
127
|
+
* shows both. Pure/prop-driven so the bar survives re-renders. */
|
|
128
|
+
activeFeedback?: FeedbackVote;
|
|
129
|
+
/** When true, the `copy` button shows its emerald check icon instead of the
|
|
130
|
+
* copy glyph (cleared by the owner after ~2s). */
|
|
131
|
+
copied?: boolean;
|
|
122
132
|
class?: string;
|
|
123
133
|
}
|
|
124
134
|
|
|
@@ -130,12 +140,25 @@ function normalizeAction(a: ChatMessageAction | CustomAction) {
|
|
|
130
140
|
return { id: a.id, label: a.label, Icon: actionIcon(a.icon) };
|
|
131
141
|
}
|
|
132
142
|
|
|
143
|
+
/** Is this entry one of the two feedback votes? */
|
|
144
|
+
function feedbackVoteOf(a: ChatMessageAction | CustomAction): FeedbackVote | undefined {
|
|
145
|
+
return a === 'like' || a === 'dislike' ? a : undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
133
148
|
/**
|
|
134
149
|
* The shared message action toolbar. Renders one ghost icon button per entry —
|
|
135
150
|
* built-in names pull their label+icon from the curated registry; custom
|
|
136
151
|
* descriptors use their `label` plus `actionIcon(icon)` (label-only when the
|
|
137
152
|
* icon is unknown or absent). `reveal="hover"` makes the bar fade in on the
|
|
138
153
|
* parent `.group`'s hover.
|
|
154
|
+
*
|
|
155
|
+
* Pure/prop-driven: feedback (`activeFeedback`) and copy (`copied`) state are
|
|
156
|
+
* owned by the parent facade and passed in — the bar holds no internal signals,
|
|
157
|
+
* so it survives the new-array-per-chunk re-renders of a streaming thread. With
|
|
158
|
+
* a vote active, the chosen `like`/`dislike` button is marked `aria-pressed` +
|
|
159
|
+
* filled and the other vote button collapses its width (sliding the active thumb
|
|
160
|
+
* into its place) via a 0fr↔1fr grid transition. The
|
|
161
|
+
* `copy` button swaps to an emerald check while `copied`.
|
|
139
162
|
*/
|
|
140
163
|
function MessageActionBar(props: MessageActionBarProps) {
|
|
141
164
|
return (
|
|
@@ -150,38 +173,156 @@ function MessageActionBar(props: MessageActionBarProps) {
|
|
|
150
173
|
{(a) => {
|
|
151
174
|
const item = normalizeAction(a);
|
|
152
175
|
const tooltipText = () => (typeof a !== 'string' && a.tooltip) ? a.tooltip : item.label;
|
|
176
|
+
const vote = feedbackVoteOf(a);
|
|
177
|
+
// A vote button is the active one when it matches the resolved vote.
|
|
178
|
+
const isActiveVote = () => vote !== undefined && props.activeFeedback === vote;
|
|
179
|
+
// The COPY button reflects the copied check.
|
|
180
|
+
const isCopy = item.id === 'copy';
|
|
181
|
+
const showCheck = () => isCopy && props.copied === true;
|
|
182
|
+
|
|
153
183
|
// Factory (not a shared node): each Show branch gets its own Button so
|
|
154
184
|
// the eager DOM node is never referenced from two places.
|
|
155
185
|
const button = () => (
|
|
156
186
|
<Button
|
|
157
187
|
variant="ghost"
|
|
158
188
|
size="icon-sm"
|
|
159
|
-
class=
|
|
189
|
+
class={cn('rounded-full', isActiveVote() && 'text-primary')}
|
|
160
190
|
data-action={item.id}
|
|
161
|
-
aria-label={item.label}
|
|
191
|
+
aria-label={showCheck() ? 'Copied' : item.label}
|
|
192
|
+
aria-pressed={vote !== undefined ? isActiveVote() : undefined}
|
|
162
193
|
onClick={() => props.onAction(item.id)}
|
|
163
194
|
>
|
|
164
|
-
<Show
|
|
165
|
-
{(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
195
|
+
<Show
|
|
196
|
+
when={showCheck()}
|
|
197
|
+
fallback={
|
|
198
|
+
<Show when={item.Icon} fallback={<span class="px-1 text-xs">{item.label}</span>}>
|
|
199
|
+
{(Icon) => {
|
|
200
|
+
const I = Icon();
|
|
201
|
+
return <I class={cn('size-3.5', isActiveVote() && 'fill-current')} />;
|
|
202
|
+
}}
|
|
203
|
+
</Show>
|
|
204
|
+
}
|
|
205
|
+
>
|
|
206
|
+
<Check class="size-3.5 text-emerald-400" />
|
|
169
207
|
</Show>
|
|
170
208
|
</Button>
|
|
171
209
|
);
|
|
172
210
|
// Icon-only buttons get a tooltip; label-only buttons (text already
|
|
173
211
|
// visible) don't.
|
|
174
|
-
|
|
212
|
+
const rendered = () => (
|
|
175
213
|
<Show when={item.Icon} fallback={button()}>
|
|
176
214
|
<Tooltip content={tooltipText()}>{button()}</Tooltip>
|
|
177
215
|
</Show>
|
|
178
216
|
);
|
|
217
|
+
|
|
218
|
+
// When the OTHER vote is active, this vote button collapses — its WIDTH
|
|
219
|
+
// animates to zero via a 0fr↔1fr grid so the remaining thumb slides into
|
|
220
|
+
// its place, while it fades out; it slides + fades back on un-vote. (Kept
|
|
221
|
+
// mounted-but-collapsed, not unmounted, so the sibling can slide.) No
|
|
222
|
+
// vote active → both shown; non-vote entries always render.
|
|
223
|
+
return (
|
|
224
|
+
<Show when={vote !== undefined} fallback={rendered()}>
|
|
225
|
+
{(() => {
|
|
226
|
+
const show = () => props.activeFeedback === undefined || props.activeFeedback === vote;
|
|
227
|
+
return (
|
|
228
|
+
<span
|
|
229
|
+
data-feedback-collapsed={show() ? undefined : ''}
|
|
230
|
+
class={cn(
|
|
231
|
+
'grid transition-[grid-template-columns,opacity] duration-300 ease-out',
|
|
232
|
+
show() ? 'grid-cols-[1fr] opacity-100' : 'grid-cols-[0fr] opacity-0',
|
|
233
|
+
)}
|
|
234
|
+
>
|
|
235
|
+
<span class="min-w-0 overflow-hidden">{rendered()}</span>
|
|
236
|
+
</span>
|
|
237
|
+
);
|
|
238
|
+
})()}
|
|
239
|
+
</Show>
|
|
240
|
+
);
|
|
179
241
|
}}
|
|
180
242
|
</For>
|
|
181
243
|
</MessageActions>
|
|
182
244
|
);
|
|
183
245
|
}
|
|
184
246
|
|
|
247
|
+
// --- MessageBody ---
|
|
248
|
+
|
|
249
|
+
export interface MessageBodyProps {
|
|
250
|
+
/** The message text/markdown. */
|
|
251
|
+
content: string;
|
|
252
|
+
/** Optional collapsible reasoning block, rendered above the content. */
|
|
253
|
+
reasoning?: { text: string; label?: string };
|
|
254
|
+
/** Tool-call parts, rendered above the content. */
|
|
255
|
+
tools?: ToolPart[];
|
|
256
|
+
/** Inline attachment previews, rendered above the content. */
|
|
257
|
+
attachments?: AttachmentData[];
|
|
258
|
+
/** Whether this is a user message (right-aligned bubble) vs an assistant
|
|
259
|
+
* message (full-width transparent). */
|
|
260
|
+
isUser: boolean;
|
|
261
|
+
/** Whether the content renders as markdown. */
|
|
262
|
+
markdown: boolean;
|
|
263
|
+
/** Action-bar entries — built-in names and/or custom descriptors. When empty
|
|
264
|
+
* the bar is not rendered. */
|
|
265
|
+
actions?: (ChatMessageAction | CustomAction)[];
|
|
266
|
+
/** `'always'` (default) keeps the bar visible; `'hover'` reveals it on parent
|
|
267
|
+
* `.group` hover. */
|
|
268
|
+
actionsReveal?: 'always' | 'hover';
|
|
269
|
+
/** Fired with the built-in name or custom id when an action is clicked. */
|
|
270
|
+
onAction?: (id: string) => void;
|
|
271
|
+
/** The currently-active feedback vote, so the bar can mark like/dislike and
|
|
272
|
+
* hide the other. */
|
|
273
|
+
activeFeedback?: FeedbackVote;
|
|
274
|
+
/** When true, the copy button shows its "copied" check icon. */
|
|
275
|
+
copied?: boolean;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The shared message body: an optional reasoning block, tool calls, inline
|
|
280
|
+
* attachments, the content bubble, and the action bar — in that order. This is
|
|
281
|
+
* the single source of truth for how a message renders, consumed by `ChatThread`
|
|
282
|
+
* (the `<For>` over `messages`), the standalone `<kai-message>` facade, and (in
|
|
283
|
+
* future) `kai-compare` for each candidate. Pure/prop-driven: all interaction
|
|
284
|
+
* state (copied, feedback vote) is owned above and passed in.
|
|
285
|
+
*/
|
|
286
|
+
function MessageBody(props: MessageBodyProps) {
|
|
287
|
+
return (
|
|
288
|
+
<>
|
|
289
|
+
<Show when={props.reasoning}>
|
|
290
|
+
{(r) => (
|
|
291
|
+
<Reasoning class="mb-2 w-full">
|
|
292
|
+
<ReasoningTrigger>{r().label ?? 'Reasoning'}</ReasoningTrigger>
|
|
293
|
+
<ReasoningContent markdown>{r().text}</ReasoningContent>
|
|
294
|
+
</Reasoning>
|
|
295
|
+
)}
|
|
296
|
+
</Show>
|
|
297
|
+
<For each={props.tools ?? []}>{(tp) => <Tool toolPart={tp} class="mb-2 w-full" />}</For>
|
|
298
|
+
<Show when={props.attachments?.length}>
|
|
299
|
+
<Attachments variant="inline" class={props.isUser ? 'mb-2 justify-end' : 'mb-2'}>
|
|
300
|
+
<For each={props.attachments!}>
|
|
301
|
+
{(att) => (<Attachment data={att}><AttachmentPreview /><AttachmentInfo /></Attachment>)}
|
|
302
|
+
</For>
|
|
303
|
+
</Attachments>
|
|
304
|
+
</Show>
|
|
305
|
+
<MessageContent
|
|
306
|
+
markdown={props.markdown}
|
|
307
|
+
class={props.isUser
|
|
308
|
+
? 'bg-muted text-primary max-w-[85%] rounded-2xl px-4 py-2'
|
|
309
|
+
: 'bg-transparent p-0'}
|
|
310
|
+
>
|
|
311
|
+
{props.content}
|
|
312
|
+
</MessageContent>
|
|
313
|
+
<Show when={(props.actions?.length ?? 0) > 0}>
|
|
314
|
+
<MessageActionBar
|
|
315
|
+
actions={props.actions!}
|
|
316
|
+
reveal={props.actionsReveal === 'hover' ? 'hover' : 'always'}
|
|
317
|
+
activeFeedback={props.activeFeedback}
|
|
318
|
+
copied={props.copied}
|
|
319
|
+
onAction={(id) => props.onAction?.(id)}
|
|
320
|
+
/>
|
|
321
|
+
</Show>
|
|
322
|
+
</>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
185
326
|
// --- MessageAction ---
|
|
186
327
|
|
|
187
328
|
export interface MessageActionProps {
|
|
@@ -224,4 +365,4 @@ function MessageCopyButton(props: MessageCopyButtonProps) {
|
|
|
224
365
|
);
|
|
225
366
|
}
|
|
226
367
|
|
|
227
|
-
export { Message, MessageAvatar, MessageContent, MessageActions, MessageActionBar, MessageAction, MessageCopyButton };
|
|
368
|
+
export { Message, MessageAvatar, MessageContent, MessageActions, MessageActionBar, MessageBody, MessageAction, MessageCopyButton };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Types + pure helpers for the dual-response comparison card (`ResponseCompare` /
|
|
2
|
+
// `<kai-compare>`). A compare presents exactly TWO assistant candidates for the
|
|
3
|
+
// same prompt side-by-side; the user picks the better one (a COMMIT, not a
|
|
4
|
+
// Submit), which the consumer turns into a `(prompt, chosen, rejected)`
|
|
5
|
+
// preference pair. Kept framework-agnostic and DOM-free so it unit-tests in
|
|
6
|
+
// isolation.
|
|
7
|
+
import type { ToolPart } from './tool';
|
|
8
|
+
import type { AttachmentData } from './attachments';
|
|
9
|
+
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
// Types
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/** One assistant response in a comparison. Rendered identically to an assistant
|
|
15
|
+
* message (reasoning + tools + attachments + markdown content) via `MessageBody`. */
|
|
16
|
+
export interface CompareCandidate {
|
|
17
|
+
/** Stable id, unique within the pair; echoed back in the selection. */
|
|
18
|
+
id: string;
|
|
19
|
+
/** The response text/markdown. */
|
|
20
|
+
content: string;
|
|
21
|
+
/** Optional collapsible reasoning block. */
|
|
22
|
+
reasoning?: { text: string; label?: string };
|
|
23
|
+
/** Tool-call parts rendered above the content. */
|
|
24
|
+
tools?: ToolPart[];
|
|
25
|
+
/** Inline attachment previews rendered above the content. */
|
|
26
|
+
attachments?: AttachmentData[];
|
|
27
|
+
/** Optional short column label (e.g. "A" / "B" / "Concise"). */
|
|
28
|
+
label?: string;
|
|
29
|
+
/** Optional model name shown as a sub-label. */
|
|
30
|
+
model?: string;
|
|
31
|
+
/** When true this candidate is still streaming — its pick control is disabled
|
|
32
|
+
* and a per-column shimmer shows until it (and its sibling) settle. */
|
|
33
|
+
streaming?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The two-candidate tuple. Enforced at the type level; validated at runtime by
|
|
37
|
+
* `normalizeCandidates`. */
|
|
38
|
+
export type ComparePair = [CompareCandidate, CompareCandidate];
|
|
39
|
+
|
|
40
|
+
/** How the card collapses once the user picks. `'winner'` (default) shows just the
|
|
41
|
+
* chosen candidate's body; `'none'` keeps both columns but marks the choice. */
|
|
42
|
+
export type CompareCollapse = 'winner' | 'none';
|
|
43
|
+
|
|
44
|
+
/** The compare definition (set as the `data` JS property on `<kai-compare>`). */
|
|
45
|
+
export interface ResponseCompareData {
|
|
46
|
+
/** Optional prompt/question shown above the two columns. */
|
|
47
|
+
prompt?: string;
|
|
48
|
+
/** Exactly two candidates: [A, B]. */
|
|
49
|
+
candidates: ComparePair;
|
|
50
|
+
/** Collapse behaviour after a pick. Default `'winner'`. */
|
|
51
|
+
collapse?: CompareCollapse;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Emitted (and re-hydratable) when the user picks. The consumer pairs
|
|
55
|
+
* `(prompt, chosenId, rejectedIds)` for preference capture / RLHF. */
|
|
56
|
+
export interface CompareSelection {
|
|
57
|
+
/** The id of the chosen candidate. */
|
|
58
|
+
chosenId: string;
|
|
59
|
+
/** The id(s) of the rejected candidate(s) — for a pair, the other one. */
|
|
60
|
+
rejectedIds: string[];
|
|
61
|
+
/** Epoch ms when the pick was made. */
|
|
62
|
+
at?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
// Pure helpers (unit-tested in isolation).
|
|
67
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validate + normalize the candidate list. A compare requires EXACTLY two
|
|
71
|
+
* candidates with unique, non-empty string ids. Returns the usable pair, or an
|
|
72
|
+
* error message when the definition is unusable (rendered as an inline error /
|
|
73
|
+
* emitted as a `kai-error`). Modeled on `normalizeOptions`.
|
|
74
|
+
*/
|
|
75
|
+
export function normalizeCandidates(candidates: unknown): {
|
|
76
|
+
candidates: ComparePair | null;
|
|
77
|
+
error?: string;
|
|
78
|
+
} {
|
|
79
|
+
if (!Array.isArray(candidates)) {
|
|
80
|
+
return { candidates: null, error: "This comparison couldn't be displayed." };
|
|
81
|
+
}
|
|
82
|
+
if (candidates.length !== 2) {
|
|
83
|
+
return {
|
|
84
|
+
candidates: null,
|
|
85
|
+
error: `A comparison needs exactly two candidates (got ${candidates.length}).`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const out: CompareCandidate[] = [];
|
|
89
|
+
const seen = new Set<string>();
|
|
90
|
+
for (const c of candidates) {
|
|
91
|
+
if (!c || typeof c !== 'object') {
|
|
92
|
+
return { candidates: null, error: "This comparison couldn't be displayed." };
|
|
93
|
+
}
|
|
94
|
+
const cand = c as Partial<CompareCandidate>;
|
|
95
|
+
if (typeof cand.id !== 'string' || cand.id.length === 0) {
|
|
96
|
+
return { candidates: null, error: 'Each candidate needs a non-empty id.' };
|
|
97
|
+
}
|
|
98
|
+
if (seen.has(cand.id)) {
|
|
99
|
+
return { candidates: null, error: `Duplicate candidate id "${cand.id}".` };
|
|
100
|
+
}
|
|
101
|
+
seen.add(cand.id);
|
|
102
|
+
out.push({
|
|
103
|
+
id: cand.id,
|
|
104
|
+
content: typeof cand.content === 'string' ? cand.content : '',
|
|
105
|
+
reasoning: cand.reasoning,
|
|
106
|
+
tools: Array.isArray(cand.tools) ? cand.tools : undefined,
|
|
107
|
+
attachments: Array.isArray(cand.attachments) ? cand.attachments : undefined,
|
|
108
|
+
label: typeof cand.label === 'string' ? cand.label : undefined,
|
|
109
|
+
model: typeof cand.model === 'string' ? cand.model : undefined,
|
|
110
|
+
streaming: cand.streaming === true,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return { candidates: [out[0], out[1]] };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Build the selection payload for choosing `chosenId` out of the pair (the other
|
|
117
|
+
* candidate is the rejected one). Stamps `at` with the current time. */
|
|
118
|
+
export function buildSelection(pair: ComparePair, chosenId: string): CompareSelection {
|
|
119
|
+
const rejectedIds = pair.filter((c) => c.id !== chosenId).map((c) => c.id);
|
|
120
|
+
return { chosenId, rejectedIds, at: Date.now() };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Whether any candidate in the pair is still streaming (pick stays disabled). */
|
|
124
|
+
export function isAnyStreaming(pair: ComparePair | null): boolean {
|
|
125
|
+
if (!pair) return false;
|
|
126
|
+
return pair.some((c) => c.streaming === true);
|
|
127
|
+
}
|