@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
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type JSX,
|
|
3
|
+
type Accessor,
|
|
4
|
+
Show,
|
|
5
|
+
Index,
|
|
6
|
+
splitProps,
|
|
7
|
+
mergeProps,
|
|
8
|
+
createSignal,
|
|
9
|
+
createMemo,
|
|
10
|
+
createEffect,
|
|
11
|
+
on,
|
|
12
|
+
ErrorBoundary,
|
|
13
|
+
createUniqueId,
|
|
14
|
+
} from 'solid-js';
|
|
15
|
+
import { cn } from '../utils/cn';
|
|
16
|
+
import { Button } from '../ui/button';
|
|
17
|
+
import { Card } from './card';
|
|
18
|
+
import { MessageBody } from './message';
|
|
19
|
+
import { TextShimmer } from './text-shimmer';
|
|
20
|
+
import { Check } from 'lucide-solid';
|
|
21
|
+
import {
|
|
22
|
+
type CompareCandidate,
|
|
23
|
+
type ComparePair,
|
|
24
|
+
type CompareSelection,
|
|
25
|
+
type ResponseCompareData,
|
|
26
|
+
buildSelection,
|
|
27
|
+
isAnyStreaming,
|
|
28
|
+
normalizeCandidates,
|
|
29
|
+
} from './response-compare-types';
|
|
30
|
+
|
|
31
|
+
// Re-export the data types + pure helpers so consumers (and the `<kai-compare>`
|
|
32
|
+
// facade) can import everything from a single `response-compare` module.
|
|
33
|
+
export type {
|
|
34
|
+
CompareCandidate,
|
|
35
|
+
ComparePair,
|
|
36
|
+
CompareCollapse,
|
|
37
|
+
ResponseCompareData,
|
|
38
|
+
CompareSelection,
|
|
39
|
+
} from './response-compare-types';
|
|
40
|
+
export { normalizeCandidates, buildSelection, isAnyStreaming } from './response-compare-types';
|
|
41
|
+
|
|
42
|
+
export type CompareLayout = 'auto' | 'columns' | 'tabs';
|
|
43
|
+
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
// Resolution controller — a sibling of `useCardResolution` for `CompareSelection`,
|
|
46
|
+
// which has no `kind` field (so it can't satisfy `R extends CardResolution`). Same
|
|
47
|
+
// precedence: prop (host-driven / re-hydrated) > local optimistic flip > none. A
|
|
48
|
+
// fresh `data` identity clears the local flip, but an explicit prop keeps it
|
|
49
|
+
// resolved. `isOptimistic` is true only for a flip made THIS session (announced via
|
|
50
|
+
// role="status"; silent on re-hydrate).
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export interface ResolvedController<T> {
|
|
54
|
+
value: Accessor<T | undefined>;
|
|
55
|
+
isResolved: Accessor<boolean>;
|
|
56
|
+
isOptimistic: Accessor<boolean>;
|
|
57
|
+
setLocal: (v: T) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function useResolved<T>(opts: {
|
|
61
|
+
prop: Accessor<T | undefined>;
|
|
62
|
+
data: Accessor<unknown>;
|
|
63
|
+
}): ResolvedController<T> {
|
|
64
|
+
const [local, setLocal] = createSignal<T | undefined>(undefined);
|
|
65
|
+
// Clear the optimistic flip on a NEW data identity (deferred so mount doesn't
|
|
66
|
+
// clobber an initial prop). The prop still wins via the memo below.
|
|
67
|
+
createEffect(on(opts.data, () => setLocal(undefined), { defer: true }));
|
|
68
|
+
const value = createMemo(() => opts.prop() ?? local());
|
|
69
|
+
const isResolved = createMemo(() => value() !== undefined);
|
|
70
|
+
const isOptimistic = createMemo(() => opts.prop() === undefined && local() !== undefined);
|
|
71
|
+
return { value, isResolved, isOptimistic, setLocal };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
// The <ResponseCompare> component.
|
|
76
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
export interface ResponseCompareProps {
|
|
79
|
+
/** The compare definition (prompt + the two candidates). */
|
|
80
|
+
data?: ResponseCompareData;
|
|
81
|
+
/** Stable id correlating every emitted event. */
|
|
82
|
+
compareId?: string;
|
|
83
|
+
/** Re-hydrate / control the selection. Renders the collapsed winner. Set as a
|
|
84
|
+
* JS property: `el.selection = { chosenId, rejectedIds }`. */
|
|
85
|
+
selection?: CompareSelection;
|
|
86
|
+
/** Layout. `'columns'` = side-by-side; `'tabs'` = one candidate at a time with
|
|
87
|
+
* pills to switch; `'auto'` (default) uses a CONTAINER query — columns when the
|
|
88
|
+
* component is ≥640px wide, tabs when narrower (e.g. on a phone). */
|
|
89
|
+
layout?: CompareLayout;
|
|
90
|
+
class?: string;
|
|
91
|
+
/** Fired when the user commits a pick. */
|
|
92
|
+
onSelect?: (sel: CompareSelection) => void;
|
|
93
|
+
/** Fired once both candidates have settled (stopped streaming) and a valid
|
|
94
|
+
* definition is shown. */
|
|
95
|
+
onReady?: () => void;
|
|
96
|
+
/** Fired for an unusable definition (inline error state). */
|
|
97
|
+
onError?: (message: string) => void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* `ResponseCompare` — a dual-response comparison. Two assistant candidates for the
|
|
102
|
+
* same prompt render side-by-side (or as tabs), each via `MessageBody` so a
|
|
103
|
+
* candidate reads exactly like an assistant message (reasoning + tools +
|
|
104
|
+
* attachments + markdown). The pick is a COMMIT, not a Submit: clicking "Pick this"
|
|
105
|
+
* (or Enter on the focused column) computes `{ chosenId, rejectedIds:[other], at }`,
|
|
106
|
+
* optimistically collapses to the chosen candidate's body, and calls `onSelect` for
|
|
107
|
+
* the consumer to send a `(prompt, chosen, rejected)` preference pair. Single-shot.
|
|
108
|
+
*
|
|
109
|
+
* While EITHER candidate is `streaming`, both pick controls are disabled and the
|
|
110
|
+
* streaming column shows a `TextShimmer`; `onReady` fires once both settle. The two
|
|
111
|
+
* columns are a WAI-ARIA radiogroup with roving tabindex (Arrow keys move A↔B,
|
|
112
|
+
* Enter/Space picks). Emits `onError` for a malformed definition.
|
|
113
|
+
*/
|
|
114
|
+
export function ResponseCompare(props: ResponseCompareProps): JSX.Element {
|
|
115
|
+
const merged = mergeProps({ compareId: 'kai-compare', layout: 'auto' as CompareLayout }, props);
|
|
116
|
+
const [local] = splitProps(merged, [
|
|
117
|
+
'data',
|
|
118
|
+
'compareId',
|
|
119
|
+
'selection',
|
|
120
|
+
'layout',
|
|
121
|
+
'class',
|
|
122
|
+
'onSelect',
|
|
123
|
+
'onReady',
|
|
124
|
+
'onError',
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
const uid = createUniqueId();
|
|
128
|
+
|
|
129
|
+
const normalized = createMemo(() => normalizeCandidates(local.data?.candidates));
|
|
130
|
+
const valid = createMemo(() => normalized().candidates !== null);
|
|
131
|
+
const errorMessage = createMemo(() => normalized().error ?? '');
|
|
132
|
+
const pair = createMemo<ComparePair | null>(() => normalized().candidates);
|
|
133
|
+
|
|
134
|
+
const res = useResolved<CompareSelection>({
|
|
135
|
+
prop: () => local.selection,
|
|
136
|
+
data: () => local.data,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Both candidates settled → enable the pick + announce ready.
|
|
140
|
+
const streaming = createMemo(() => isAnyStreaming(pair()));
|
|
141
|
+
|
|
142
|
+
const [focusIndex, setFocusIndex] = createSignal(0);
|
|
143
|
+
// In tabs mode (and auto when the container is narrow) only one candidate shows
|
|
144
|
+
// at a time; `active` is which. Ignored in columns mode where both render.
|
|
145
|
+
const [active, setActive] = createSignal(0);
|
|
146
|
+
let groupRef: HTMLDivElement | undefined;
|
|
147
|
+
|
|
148
|
+
// Reset the roving tab stop + active tab whenever a NEW definition arrives.
|
|
149
|
+
createEffect(on(() => local.data, () => { setFocusIndex(0); setActive(0); }));
|
|
150
|
+
|
|
151
|
+
// ready / error lifecycle: error on an unusable definition, ready once both
|
|
152
|
+
// candidates have settled (so consumers know the pick is now live).
|
|
153
|
+
createEffect(
|
|
154
|
+
on([valid, streaming], ([ok, isStreaming]) => {
|
|
155
|
+
if (!ok) {
|
|
156
|
+
local.onError?.(errorMessage());
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (!isStreaming) local.onReady?.();
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const chosenId = createMemo(() => res.value()?.chosenId);
|
|
164
|
+
const chosenCandidate = createMemo<CompareCandidate | undefined>(() => {
|
|
165
|
+
const id = chosenId();
|
|
166
|
+
return id === undefined ? undefined : pair()?.find((c) => c.id === id);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Commit a pick: emit + optimistically flip (single-shot; inert once resolved
|
|
170
|
+
// or while still streaming).
|
|
171
|
+
const pick = (candidate: CompareCandidate): void => {
|
|
172
|
+
if (res.isResolved()) return;
|
|
173
|
+
if (streaming()) return;
|
|
174
|
+
const p = pair();
|
|
175
|
+
if (!p) return;
|
|
176
|
+
const sel = buildSelection(p, candidate.id);
|
|
177
|
+
local.onSelect?.(sel);
|
|
178
|
+
res.setLocal(sel);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const focusColumn = (index: number): void => {
|
|
182
|
+
const cols = groupRef?.querySelectorAll<HTMLElement>('[role="radio"]');
|
|
183
|
+
cols?.[index]?.focus();
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Two-item roving tabindex (A↔B). Arrows toggle the focused column; Enter/Space
|
|
187
|
+
// picks it. No-op once resolved.
|
|
188
|
+
const onGroupKeyDown = (e: KeyboardEvent): void => {
|
|
189
|
+
if (res.isResolved()) return;
|
|
190
|
+
const p = pair();
|
|
191
|
+
if (!p) return;
|
|
192
|
+
const move = (next: number) => { setFocusIndex(next); setActive(next); focusColumn(next); };
|
|
193
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
move(focusIndex() === 0 ? 1 : 0);
|
|
196
|
+
} else if (e.key === 'Home') {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
move(0);
|
|
199
|
+
} else if (e.key === 'End') {
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
move(1);
|
|
202
|
+
} else if (e.key === ' ' || e.key === 'Enter') {
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
const cand = p[focusIndex()];
|
|
205
|
+
if (cand) pick(cand);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// `auto` switches by CONTAINER width (Tailwind v4 `@container` + `@[640px]`
|
|
210
|
+
// variants); `@container/compare` lives on the OUTER wrapper (below) so the
|
|
211
|
+
// pill bar + columns both respond to it.
|
|
212
|
+
const wrapperClass = createMemo(() => {
|
|
213
|
+
const l = local.layout;
|
|
214
|
+
if (l === 'columns') return 'grid grid-cols-2 gap-3';
|
|
215
|
+
if (l === 'tabs') return 'grid grid-cols-1 gap-3';
|
|
216
|
+
return 'grid grid-cols-1 gap-3 @[640px]/compare:grid-cols-2'; // auto
|
|
217
|
+
});
|
|
218
|
+
// The tab-pill bar shows in tabs mode, and in auto only while the container is narrow.
|
|
219
|
+
const showTabs = () => local.layout === 'tabs' || local.layout === 'auto';
|
|
220
|
+
const tabBarClass = () => (local.layout === 'auto' ? '@[640px]/compare:hidden' : '');
|
|
221
|
+
// A non-active candidate is hidden in tabs (always) and auto (only while narrow);
|
|
222
|
+
// the active one — and both in columns mode — always render.
|
|
223
|
+
const columnHiddenClass = (index: number): string => {
|
|
224
|
+
const l = local.layout;
|
|
225
|
+
if (l === 'columns' || index === active()) return '';
|
|
226
|
+
return l === 'tabs' ? 'hidden' : 'hidden @[640px]/compare:block';
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const promptId = `kai-compare-prompt-${uid}`;
|
|
230
|
+
const groupLabel = (): string => local.data?.prompt ?? 'Compare two responses';
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<Show
|
|
234
|
+
when={valid()}
|
|
235
|
+
fallback={<Card errorMessage={errorMessage() || "This comparison couldn't be displayed."} />}
|
|
236
|
+
>
|
|
237
|
+
<ErrorBoundary
|
|
238
|
+
fallback={() => {
|
|
239
|
+
local.onError?.('The comparison failed to render.');
|
|
240
|
+
return <Card errorMessage="The comparison failed to render." />;
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
<div class={cn('flex flex-col gap-3', local.layout === 'auto' && '@container/compare', local.class)}>
|
|
244
|
+
<Show when={local.data?.prompt}>
|
|
245
|
+
<p id={promptId} class="text-sm text-foreground">
|
|
246
|
+
{local.data?.prompt}
|
|
247
|
+
</p>
|
|
248
|
+
</Show>
|
|
249
|
+
|
|
250
|
+
<Show
|
|
251
|
+
when={!res.isResolved()}
|
|
252
|
+
fallback={
|
|
253
|
+
<CollapsedWinner
|
|
254
|
+
candidate={chosenCandidate()}
|
|
255
|
+
optimistic={res.isOptimistic()}
|
|
256
|
+
/>
|
|
257
|
+
}
|
|
258
|
+
>
|
|
259
|
+
{/* Tab pills — switch the visible candidate in tabs / auto-narrow mode. */}
|
|
260
|
+
<Show when={showTabs()}>
|
|
261
|
+
<div role="tablist" aria-label="Switch response" class={cn('flex gap-1', tabBarClass())}>
|
|
262
|
+
<Index each={pair() ?? []}>
|
|
263
|
+
{(candidate, index) => (
|
|
264
|
+
<button
|
|
265
|
+
type="button"
|
|
266
|
+
role="tab"
|
|
267
|
+
aria-selected={active() === index}
|
|
268
|
+
class={cn(
|
|
269
|
+
'rounded-md px-3 py-1 text-xs font-medium transition-colors',
|
|
270
|
+
active() === index
|
|
271
|
+
? 'bg-muted text-foreground'
|
|
272
|
+
: 'text-muted-foreground hover:text-foreground',
|
|
273
|
+
)}
|
|
274
|
+
onClick={() => { setActive(index); setFocusIndex(index); }}
|
|
275
|
+
>
|
|
276
|
+
{candidate().label ?? `Response ${candidate().id}`}
|
|
277
|
+
</button>
|
|
278
|
+
)}
|
|
279
|
+
</Index>
|
|
280
|
+
</div>
|
|
281
|
+
</Show>
|
|
282
|
+
|
|
283
|
+
<div
|
|
284
|
+
ref={groupRef}
|
|
285
|
+
role="radiogroup"
|
|
286
|
+
aria-label={groupLabel()}
|
|
287
|
+
aria-describedby={local.data?.prompt ? promptId : undefined}
|
|
288
|
+
class={wrapperClass()}
|
|
289
|
+
onKeyDown={onGroupKeyDown}
|
|
290
|
+
>
|
|
291
|
+
<Index each={pair() ?? []}>
|
|
292
|
+
{(candidate, index) => (
|
|
293
|
+
<CompareColumn
|
|
294
|
+
candidate={candidate()}
|
|
295
|
+
tabStop={index === focusIndex()}
|
|
296
|
+
disabled={streaming()}
|
|
297
|
+
hiddenClass={() => columnHiddenClass(index)}
|
|
298
|
+
onFocus={() => setFocusIndex(index)}
|
|
299
|
+
onPick={() => {
|
|
300
|
+
setFocusIndex(index);
|
|
301
|
+
pick(candidate());
|
|
302
|
+
}}
|
|
303
|
+
/>
|
|
304
|
+
)}
|
|
305
|
+
</Index>
|
|
306
|
+
</div>
|
|
307
|
+
</Show>
|
|
308
|
+
</div>
|
|
309
|
+
</ErrorBoundary>
|
|
310
|
+
</Show>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
315
|
+
// One candidate column — a plain container with the assistant-styled body and a
|
|
316
|
+
// "Pick this" button that IS the radio (the radiogroup option). Keeping the radio
|
|
317
|
+
// on the button, not the wrapping div, avoids nesting interactive controls (the
|
|
318
|
+
// body itself can contain interactive bits like reasoning/tool toggles).
|
|
319
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
interface ColumnProps {
|
|
322
|
+
candidate: CompareCandidate;
|
|
323
|
+
tabStop: boolean;
|
|
324
|
+
disabled: boolean;
|
|
325
|
+
/** Extra (reactive) class controlling tabs/auto visibility — hidden when this
|
|
326
|
+
* is the non-active candidate in tabs / auto-narrow mode. */
|
|
327
|
+
hiddenClass?: () => string;
|
|
328
|
+
onFocus: () => void;
|
|
329
|
+
onPick: () => void;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function CompareColumn(props: ColumnProps): JSX.Element {
|
|
333
|
+
const labelText = (): string =>
|
|
334
|
+
props.candidate.label ?? props.candidate.model ?? props.candidate.id;
|
|
335
|
+
return (
|
|
336
|
+
<div
|
|
337
|
+
class={cn(
|
|
338
|
+
'flex min-w-0 flex-col gap-3 rounded-lg border border-input bg-background/40 p-3',
|
|
339
|
+
props.hiddenClass?.(),
|
|
340
|
+
)}
|
|
341
|
+
>
|
|
342
|
+
<Show when={props.candidate.label || props.candidate.model}>
|
|
343
|
+
<div class="flex items-center gap-2">
|
|
344
|
+
<Show when={props.candidate.label}>
|
|
345
|
+
<span class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
346
|
+
{props.candidate.label}
|
|
347
|
+
</span>
|
|
348
|
+
</Show>
|
|
349
|
+
<Show when={props.candidate.model}>
|
|
350
|
+
<span class="text-xs text-muted-foreground">{props.candidate.model}</span>
|
|
351
|
+
</Show>
|
|
352
|
+
</div>
|
|
353
|
+
</Show>
|
|
354
|
+
|
|
355
|
+
<div class="min-w-0 flex-1">
|
|
356
|
+
<Show
|
|
357
|
+
when={props.candidate.streaming}
|
|
358
|
+
fallback={
|
|
359
|
+
<MessageBody
|
|
360
|
+
content={props.candidate.content}
|
|
361
|
+
reasoning={props.candidate.reasoning}
|
|
362
|
+
tools={props.candidate.tools}
|
|
363
|
+
attachments={props.candidate.attachments}
|
|
364
|
+
isUser={false}
|
|
365
|
+
markdown={true}
|
|
366
|
+
/>
|
|
367
|
+
}
|
|
368
|
+
>
|
|
369
|
+
<TextShimmer class="text-sm">Generating response…</TextShimmer>
|
|
370
|
+
</Show>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
<Button
|
|
374
|
+
type="button"
|
|
375
|
+
role="radio"
|
|
376
|
+
aria-checked={false}
|
|
377
|
+
aria-label={`Pick ${labelText()}`}
|
|
378
|
+
data-candidate-id={props.candidate.id}
|
|
379
|
+
variant="outline"
|
|
380
|
+
size="sm"
|
|
381
|
+
class="mt-auto w-full"
|
|
382
|
+
disabled={props.disabled}
|
|
383
|
+
tabindex={props.tabStop ? 0 : -1}
|
|
384
|
+
onFocus={props.onFocus}
|
|
385
|
+
onClick={(e) => {
|
|
386
|
+
e.stopPropagation();
|
|
387
|
+
props.onPick();
|
|
388
|
+
}}
|
|
389
|
+
>
|
|
390
|
+
Pick this
|
|
391
|
+
</Button>
|
|
392
|
+
</div>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
397
|
+
// CollapsedWinner — the read-only view after a pick: the chosen candidate's body
|
|
398
|
+
// (reasoning + tools + attachments + markdown), with a "Selected" affordance.
|
|
399
|
+
// role="status" only for an optimistic flip made this session.
|
|
400
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
function CollapsedWinner(props: {
|
|
403
|
+
candidate: CompareCandidate | undefined;
|
|
404
|
+
optimistic: boolean;
|
|
405
|
+
}): JSX.Element {
|
|
406
|
+
return (
|
|
407
|
+
<div
|
|
408
|
+
class="flex flex-col gap-2 rounded-lg border border-input bg-accent/40 p-3"
|
|
409
|
+
role={props.optimistic ? 'status' : undefined}
|
|
410
|
+
>
|
|
411
|
+
<div class="flex items-center gap-2 text-sm font-medium text-accent-foreground">
|
|
412
|
+
<Check size={16} aria-hidden="true" />
|
|
413
|
+
<span>Selected{props.candidate?.label ? `: ${props.candidate.label}` : ''}</span>
|
|
414
|
+
</div>
|
|
415
|
+
<Show when={props.candidate} fallback={<p class="text-sm text-muted-foreground">Response selected.</p>}>
|
|
416
|
+
{(c) => (
|
|
417
|
+
<div class="min-w-0">
|
|
418
|
+
<MessageBody
|
|
419
|
+
content={c().content}
|
|
420
|
+
reasoning={c().reasoning}
|
|
421
|
+
tools={c().tools}
|
|
422
|
+
attachments={c().attachments}
|
|
423
|
+
isUser={false}
|
|
424
|
+
markdown={true}
|
|
425
|
+
/>
|
|
426
|
+
</div>
|
|
427
|
+
)}
|
|
428
|
+
</Show>
|
|
429
|
+
</div>
|
|
430
|
+
);
|
|
431
|
+
}
|
|
@@ -14,11 +14,12 @@ import {
|
|
|
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 type { CardEnvelope, CardEvent, CardHost, CardResolution } from '../primitives/card-contract';
|
|
18
19
|
import { useCardResolution } from './use-card-resolution';
|
|
19
20
|
import { emitCardEvent } from '../primitives/card-routing';
|
|
20
21
|
import { useCardHost } from '../primitives/card-host';
|
|
21
|
-
import { Check } from 'lucide-solid';
|
|
22
|
+
import { Check, X } from 'lucide-solid';
|
|
22
23
|
|
|
23
24
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
25
|
// Types (tasks.schema.json) — see src/primitives/card-schemas/tasks.schema.json
|
|
@@ -41,6 +42,7 @@ export interface TasksCardData {
|
|
|
41
42
|
allowEmpty?: boolean;
|
|
42
43
|
min?: number;
|
|
43
44
|
max?: number;
|
|
45
|
+
dismissible?: boolean; // show a close affordance that emits `dismiss`
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
export interface TasksCardResult {
|
|
@@ -251,11 +253,21 @@ export function TasksCard(props: TasksCardProps): JSX.Element {
|
|
|
251
253
|
res.setLocal({ kind: 'submit', data: result });
|
|
252
254
|
};
|
|
253
255
|
|
|
254
|
-
//
|
|
256
|
+
// Dismiss: emit `dismiss` AND optimistically flip to a `dismissed` resolution so
|
|
257
|
+
// the card collapses to its re-openable stub immediately.
|
|
258
|
+
const onDismiss = (): void => {
|
|
259
|
+
if (res.isResolved()) return;
|
|
260
|
+
emit({ kind: 'dismiss', cardId: local.cardId });
|
|
261
|
+
res.setLocal({ kind: 'dismissed' });
|
|
262
|
+
};
|
|
263
|
+
const onReopen = (): void => emit({ kind: 'reopen', cardId: local.cardId });
|
|
264
|
+
|
|
265
|
+
// Surface the resolved state for host styling. Only a `submit` resolution is the
|
|
266
|
+
// "submitted" state; a deferred `dismissed` (or `expired`) is not.
|
|
255
267
|
createEffect(() => {
|
|
256
268
|
const el = local.hostElement;
|
|
257
269
|
if (!el) return;
|
|
258
|
-
if (res.
|
|
270
|
+
if (res.resolution()?.kind === 'submit') el.setAttribute('data-kai-resolved', 'submitted');
|
|
259
271
|
else el.removeAttribute('data-kai-resolved');
|
|
260
272
|
});
|
|
261
273
|
|
|
@@ -280,11 +292,28 @@ export function TasksCard(props: TasksCardProps): JSX.Element {
|
|
|
280
292
|
return <Card heading={local.heading} errorMessage="The card failed to render." />;
|
|
281
293
|
}}
|
|
282
294
|
>
|
|
295
|
+
<Show
|
|
296
|
+
when={!res.isDeferred()}
|
|
297
|
+
fallback={
|
|
298
|
+
<DismissedStub type={TASKS_CARD_TYPE} title={local.heading ?? local.data?.heading} onReopen={onReopen} />
|
|
299
|
+
}
|
|
300
|
+
>
|
|
283
301
|
<Card
|
|
284
302
|
heading={local.heading ?? local.data?.heading}
|
|
285
303
|
actions={
|
|
286
304
|
res.isResolved() ? undefined : (
|
|
287
305
|
<div class="flex w-full flex-wrap items-center justify-between gap-2">
|
|
306
|
+
<Show when={local.data?.dismissible === true}>
|
|
307
|
+
<Button
|
|
308
|
+
type="button"
|
|
309
|
+
variant="ghost"
|
|
310
|
+
size="icon-sm"
|
|
311
|
+
aria-label="Dismiss"
|
|
312
|
+
onClick={onDismiss}
|
|
313
|
+
>
|
|
314
|
+
<X size={16} aria-hidden="true" />
|
|
315
|
+
</Button>
|
|
316
|
+
</Show>
|
|
288
317
|
<span id={countId} aria-live="polite" class="text-xs text-muted-foreground">
|
|
289
318
|
{count()} selected
|
|
290
319
|
</span>
|
|
@@ -402,6 +431,7 @@ export function TasksCard(props: TasksCardProps): JSX.Element {
|
|
|
402
431
|
</div>
|
|
403
432
|
</Show>
|
|
404
433
|
</Card>
|
|
434
|
+
</Show>
|
|
405
435
|
</ErrorBoundary>
|
|
406
436
|
</Show>
|
|
407
437
|
);
|