@kitn.ai/ui 0.21.0 → 0.22.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/dist/components/chat-thread.d.ts +9 -0
- package/dist/{create-tween-D3Y5qiij.js → create-tween-COnPwAQN.js} +1 -1
- package/dist/{create-tween-DGKY1FDY.js → create-tween-DmTUaXWE.js} +1 -1
- package/dist/{create-tween-paho5Rh-.js → create-tween-qs-jjvNA.js} +1 -1
- package/dist/custom-elements.json +43 -7
- package/dist/elements/agent-card.js +1 -1
- package/dist/elements/artifact.js +1 -1
- package/dist/elements/attachments.js +1 -1
- package/dist/elements/audio-visualizer.js +1 -1
- package/dist/elements/avatar.js +1 -1
- package/dist/elements/badge.js +1 -1
- package/dist/elements/button.js +1 -1
- package/dist/elements/card.js +1 -1
- package/dist/elements/cards.js +1 -1
- package/dist/elements/chain-of-thought.js +1 -1
- package/dist/elements/chat-scope-picker.js +1 -1
- package/dist/elements/chat-workspace.js +1 -1
- package/dist/elements/chat.js +1 -1
- package/dist/elements/checkpoint.js +1 -1
- package/dist/elements/choice.js +1 -1
- package/dist/elements/chunks/{Icon-C-2LvGDG.js → Icon-CHG5z5L7.js} +1 -1
- package/dist/elements/chunks/{action-icons-CaYoImEk.js → action-icons-BAtCEEgg.js} +1 -1
- package/dist/elements/chunks/arrow-left-CFPTeL1Y.js +1 -0
- package/dist/elements/chunks/{artifact-BBIscOxJ.js → artifact-fBZOE4SO.js} +1 -1
- package/dist/elements/chunks/{attachments-BB56zoji.js → attachments-DpYMe5VQ.js} +1 -1
- package/dist/elements/chunks/{audio-visualizer-BxOx4THz.js → audio-visualizer-DFwzAcuL.js} +1 -1
- package/dist/elements/chunks/{badge-SxLSZ19R.js → badge-D6zodm6E.js} +1 -1
- package/dist/elements/chunks/{button-CmkUkz6c.js → button-C42vrGvO.js} +1 -1
- package/dist/elements/chunks/{card-renderer-Db4ffkWv.js → card-renderer-DMZsSuvU.js} +1 -1
- package/dist/elements/chunks/{chat-thread-B3Ks-cKr.js → chat-thread-CNNEhol-.js} +1 -1
- package/dist/elements/chunks/check-7Q3K-iXE.js +1 -0
- package/dist/elements/chunks/chevron-down-CpNYLn7E.js +1 -0
- package/dist/elements/chunks/chevron-right-DrH85Fha.js +1 -0
- package/dist/elements/chunks/{choice-card-Q9cYYDyM.js → choice-card-YoWjpsFV.js} +1 -1
- package/dist/elements/chunks/circle-C9KRjKYv.js +1 -0
- package/dist/elements/chunks/circle-check-Dmi7Y4zw.js +1 -0
- package/dist/elements/chunks/{circle-x-D0MEfxOM.js → circle-x-QIxXda6k.js} +1 -1
- package/dist/elements/chunks/{code-block-C8VzAraE.js → code-block-CqeugReY.js} +1 -1
- package/dist/elements/chunks/{collapsible-Cvj61obE.js → collapsible-sTv-_5lF.js} +1 -1
- package/dist/elements/chunks/{composer-D86ztyL0.js → composer-ldQyeVEB.js} +1 -1
- package/dist/elements/chunks/{confirm-card-BdyDAGLw.js → confirm-card-C0RNOwfn.js} +1 -1
- package/dist/elements/chunks/context-5qgypNHC.js +1 -0
- package/dist/elements/chunks/conversation-list-DLzOacXz.js +1 -0
- package/dist/elements/chunks/{create-tween-C_A_B638.js → create-tween-V4TXZ93X.js} +1 -1
- package/dist/elements/chunks/default-input-DeeRSDHc.js +1 -0
- package/dist/elements/chunks/{define-BzPnNsXg.js → define-lE-QvQFn.js} +1 -1
- package/dist/elements/chunks/{disclosure-DVKsoc1F.js → disclosure-DZSa9jog.js} +1 -1
- package/dist/elements/chunks/{download-BjA54IMJ.js → download-B2ZmnGqW.js} +1 -1
- package/dist/elements/chunks/{dropdown-BjVVHWXH.js → dropdown-R3B7cSRE.js} +1 -1
- package/dist/elements/chunks/{ellipsis-BNedmqfi.js → ellipsis-BmxceY4l.js} +1 -1
- package/dist/elements/chunks/{embed-DQsl4XEq.js → embed-BVrtTZPo.js} +1 -1
- package/dist/elements/chunks/{external-link-Di-dw2CU.js → external-link-BbdR7YO7.js} +1 -1
- package/dist/elements/chunks/{file-text-B3sAODCV.js → file-text-D7dijo1A.js} +1 -1
- package/dist/elements/chunks/{file-tree-Dgto-bMn.js → file-tree-0upJ5jeb.js} +1 -1
- package/dist/elements/chunks/{folder-D6rMadFV.js → folder-D8z58t7B.js} +1 -1
- package/dist/elements/chunks/{form-CILxvhQ7.js → form-p5BKJNw5.js} +1 -1
- package/dist/elements/chunks/{hover-card-BtiF4XiU.js → hover-card-CN9s2md5.js} +1 -1
- package/dist/elements/chunks/{icon-DKkmze3y.js → icon-CNiKP37E.js} +1 -1
- package/dist/elements/chunks/{info-CWRjZkuy.js → info-BSnuJiHI.js} +1 -1
- package/dist/elements/chunks/{input--QJBK6BA.js → input-BauDmzxX.js} +1 -1
- package/dist/elements/chunks/{kbd-C3zuKh8R.js → kbd-BlHJaP9b.js} +1 -1
- package/dist/elements/chunks/{link-BL5JA7Q0.js → link-DEi5iUWC.js} +1 -1
- package/dist/elements/chunks/{link-preview-CU37bbXh.js → link-preview-BaHyM6W4.js} +1 -1
- package/dist/elements/chunks/{loader-XmpyxyRH.js → loader-CmJG_Fkq.js} +1 -1
- package/dist/elements/chunks/{markdown-3Gv5UZ_o.js → markdown-D2jaMpCN.js} +1 -1
- package/dist/elements/chunks/{message-X5NZbq-W.js → message-C5UfXZ2Z.js} +1 -1
- package/dist/elements/chunks/{message-CnBA2OUt.js → message-CXWLmmLn.js} +1 -1
- package/dist/elements/chunks/message-square-hXylwsy0.js +1 -0
- package/dist/elements/chunks/{minimize-2-DwyrUTcM.js → minimize-2-C9ytZFMU.js} +1 -1
- package/dist/elements/chunks/model-switcher-CivGfD7-.js +1 -0
- package/dist/elements/chunks/{overlay-Cepd-KcS.js → overlay-DJL_sbOm.js} +1 -1
- package/dist/elements/chunks/{paperclip-CdwKjrlu.js → paperclip-CFaB8naM.js} +1 -1
- package/dist/elements/chunks/{progress-bar-DMVXBD3b.js → progress-bar-Br5SksfR.js} +1 -1
- package/dist/elements/chunks/{prompt-suggestion-Di_WZE_5.js → prompt-suggestion-CMIvtAhU.js} +1 -1
- package/dist/elements/chunks/{reasoning-D0jMch5u.js → reasoning-DpZcNzLq.js} +1 -1
- package/dist/elements/chunks/{resizable-1znbL1Gv.js → resizable-Q8i3T9a_.js} +1 -1
- package/dist/elements/chunks/{rotate-cw-DVhhgx8n.js → rotate-cw-B5IHCBFV.js} +1 -1
- package/dist/elements/chunks/{scroll-area-Bs4bFS1P.js → scroll-area-eLeydBje.js} +1 -1
- package/dist/elements/chunks/{scroll-button-Bet5NLc4.js → scroll-button-DWbz0yA1.js} +1 -1
- package/dist/elements/chunks/{separator-C5Gh8qGC.js → separator-Xic78AD_.js} +1 -1
- package/dist/elements/chunks/{settings-5vmEFg0K.js → settings-B8H51pAx.js} +1 -1
- package/dist/elements/chunks/{settings-group-BWLB38kz.js → settings-group-CsG9eYLe.js} +1 -1
- package/dist/elements/chunks/{share-BZ3G_DaB.js → share-CNTx0z__.js} +1 -1
- package/dist/elements/chunks/{skeleton-DafPJQTp.js → skeleton-CLlHTcMI.js} +1 -1
- package/dist/elements/chunks/{source-D_qj_6uJ.js → source-BoVcU5ok.js} +1 -1
- package/dist/elements/chunks/{star-Cuv7j2IE.js → star-CxQVTVhI.js} +1 -1
- package/dist/elements/chunks/{store-C1StPHfW.js → store-CLtS5CUG.js} +1 -1
- package/dist/elements/chunks/{tasks-card-EyoBZfpA.js → tasks-card-CzlkLmUt.js} +1 -1
- package/dist/elements/chunks/{text-shimmer-D9gRSh34.js → text-shimmer-D5NhpIQ1.js} +1 -1
- package/dist/elements/chunks/{textarea-DuhtHfcm.js → textarea-x5Uxsmtm.js} +1 -1
- package/dist/elements/chunks/{thumbs-up-B9hwv43w.js → thumbs-up-UobcJnx9.js} +1 -1
- package/dist/elements/chunks/{toast-store-Bm3DUEDe.js → toast-store-BOfs9eXq.js} +1 -1
- package/dist/elements/chunks/{tool-CDtW7L0z.js → tool-Be6Dh_dt.js} +1 -1
- package/dist/elements/chunks/{tooltip-eB226mGK.js → tooltip-RxMxyCLF.js} +1 -1
- package/dist/elements/chunks/{triangle-alert-C1SNzTqK.js → triangle-alert-C4yxH2XN.js} +1 -1
- package/dist/elements/chunks/{use-card-resolution-YZh2MvI5.js → use-card-resolution-jva9ehH6.js} +1 -1
- package/dist/elements/chunks/{variant-aurora-Cj1F7D0l.js → variant-aurora-Dx4VRcjS.js} +1 -1
- package/dist/elements/chunks/{variant-custom-T9gpt7QE.js → variant-custom-Y1ZTWgEa.js} +1 -1
- package/dist/elements/chunks/{variant-wave-CxU9z-TE.js → variant-wave-B9gml9Cg.js} +1 -1
- package/dist/elements/chunks/{video-CK4R7Rus.js → video-CrPHsytT.js} +1 -1
- package/dist/elements/chunks/x-_-OPHept.js +1 -0
- package/dist/elements/coachmark.js +1 -1
- package/dist/elements/code-block.js +1 -1
- package/dist/elements/command.js +1 -1
- package/dist/elements/compare.js +1 -1
- package/dist/elements/composer.js +1 -1
- package/dist/elements/confirm-card.js +1 -1
- package/dist/elements/context-meter.js +1 -1
- package/dist/elements/conversation-list.js +1 -1
- package/dist/elements/default-input.d.ts +27 -0
- package/dist/elements/dialog.js +1 -1
- package/dist/elements/editable-label.js +1 -1
- package/dist/elements/embed.js +1 -1
- package/dist/elements/empty.js +1 -1
- package/dist/elements/feedback-bar.js +1 -1
- package/dist/elements/file-tree.js +1 -1
- package/dist/elements/file-upload.js +1 -1
- package/dist/elements/form.js +1 -1
- package/dist/elements/hover-card.js +1 -1
- package/dist/elements/icon.js +1 -1
- package/dist/elements/image.js +1 -1
- package/dist/elements/input.js +1 -1
- package/dist/elements/kbd.js +1 -1
- package/dist/elements/link-preview.js +1 -1
- package/dist/elements/loader.js +1 -1
- package/dist/elements/markdown.js +1 -1
- package/dist/elements/menu.js +1 -1
- package/dist/elements/message-skills.js +1 -1
- package/dist/elements/message.js +1 -1
- package/dist/elements/model-switcher.js +1 -1
- package/dist/elements/nav.js +1 -1
- package/dist/elements/notice.js +1 -1
- package/dist/elements/pane-group.js +1 -1
- package/dist/elements/pane.js +1 -1
- package/dist/elements/popover.js +1 -1
- package/dist/elements/progress-bar.js +1 -1
- package/dist/elements/prompt-dock.js +1 -1
- package/dist/elements/prompt-input.js +1 -1
- package/dist/elements/prompt-suggestions.js +1 -1
- package/dist/elements/reasoning.js +1 -1
- package/dist/elements/remote.js +1 -1
- package/dist/elements/resizable.js +1 -1
- package/dist/elements/response-stream.js +2 -2
- package/dist/elements/screen.js +1 -1
- package/dist/elements/scroll-area.js +1 -1
- package/dist/elements/scroll-button.js +1 -1
- package/dist/elements/search.js +1 -1
- package/dist/elements/segmented.js +1 -1
- package/dist/elements/separator.js +1 -1
- package/dist/elements/setting-item.js +1 -1
- package/dist/elements/settings-group.js +1 -1
- package/dist/elements/skeleton.js +1 -1
- package/dist/elements/slot-text.d.ts +51 -0
- package/dist/elements/slots.d.ts +5 -0
- package/dist/elements/source.js +1 -1
- package/dist/elements/status.js +1 -1
- package/dist/elements/switch.js +1 -1
- package/dist/elements/tabs.js +1 -1
- package/dist/elements/tasks.js +1 -1
- package/dist/elements/text-shimmer.js +1 -1
- package/dist/elements/thinking-bar.js +1 -1
- package/dist/elements/thread.js +1 -1
- package/dist/elements/toast.js +1 -1
- package/dist/elements/tool.js +1 -1
- package/dist/elements/tooltip.js +1 -1
- package/dist/elements/voice-input.js +1 -1
- package/dist/elements/voice-output.js +1 -1
- package/dist/elements.d.ts +12 -6
- package/dist/index.js +1535 -1487
- package/dist/index.server.js +2330 -2296
- package/dist/kai.es.js +1 -1
- package/dist/llms/llms-full.txt +12 -3
- package/dist/mcp.es.js +809 -75
- package/dist/react/index.d.ts +13 -3
- package/dist/react.js +14 -14
- package/dist/register-impl-AxIlqjo7.js +145 -0
- package/dist/{solid-BCJUQqwS.js → solid-BGYSJyHG.js} +4901 -4691
- package/dist/{solid-BdENBcEq.js → solid-Dt2nEwfl.js} +4406 -4320
- package/dist/solid.js +1 -1
- package/dist/solid.server.js +1 -1
- package/dist/{variant-aurora-DOb_o4pC.js → variant-aurora-B44X69AG.js} +1 -1
- package/dist/{variant-aurora-BKHHiOgL.js → variant-aurora-BRhV8Flk.js} +2 -2
- package/dist/{variant-aurora-1wMcXRUS.js → variant-aurora-Ds5ou9BH.js} +2 -2
- package/dist/{variant-custom-DWSSgmd2.js → variant-custom-B20FhtYj.js} +2 -2
- package/dist/{variant-custom-CJGB0Kbw.js → variant-custom-BvA1Schl.js} +1 -1
- package/dist/{variant-custom-Co0CkXJF.js → variant-custom-D6Rtvneb.js} +2 -2
- package/dist/{variant-wave-CEODq-9q.js → variant-wave-BUQtkryq.js} +1 -1
- package/dist/{variant-wave-C3-45cL6.js → variant-wave-BVYnqy9L.js} +2 -2
- package/dist/{variant-wave-jdXBnCUZ.js → variant-wave-DXC6_LcH.js} +2 -2
- package/dist/wire/encode.d.ts +79 -6
- package/dist/wire/files.d.ts +116 -0
- package/dist/wire/index.d.ts +3 -1
- package/dist/wire/media-types.d.ts +106 -0
- package/dist/wire.js +596 -368
- package/frameworks/react/index.tsx +9 -5
- package/llms-full.txt +12 -3
- package/package.json +2 -1
- package/src/agent-tooling/archetypes.ts +43 -12
- package/src/agent-tooling/integrations/anthropic.ts +29 -5
- package/src/agent-tooling/integrations/cloudflare.ts +5 -0
- package/src/agent-tooling/integrations/langgraph.ts +13 -0
- package/src/agent-tooling/integrations/mastra.ts +46 -5
- package/src/agent-tooling/integrations/mock.ts +4 -0
- package/src/agent-tooling/integrations/ollama.ts +7 -0
- package/src/agent-tooling/integrations/openai.ts +2 -0
- package/src/agent-tooling/integrations/openrouter.ts +2 -0
- package/src/agent-tooling/integrations/pi.ts +26 -1
- package/src/agent-tooling/integrations/pydantic-ai.ts +37 -3
- package/src/agent-tooling/integrations/vercel-ai-sdk.ts +42 -6
- package/src/agent-tooling/mcp/tools/scaffold.ts +853 -32
- package/src/agent-tooling/registry.ts +57 -0
- package/src/agent-tooling/types.ts +132 -0
- package/src/components/chat-thread.tsx +10 -1
- package/src/components/context.tsx +18 -12
- package/src/components/conversation-item.tsx +6 -0
- package/src/components/model-switcher.tsx +38 -8
- package/src/components/toast.tsx +6 -1
- package/src/elements/button.tsx +26 -3
- package/src/elements/chat.tsx +28 -2
- package/src/elements/default-input.tsx +128 -5
- package/src/elements/element-meta.json +30 -4
- package/src/elements/element-types.d.ts +12 -6
- package/src/elements/menu.tsx +30 -3
- package/src/elements/slot-text.ts +73 -0
- package/src/elements/slots.ts +10 -0
- package/src/index.ts +9 -5
- package/src/wire/encode.ts +229 -14
- package/src/wire/files.ts +342 -0
- package/src/wire/index.ts +21 -0
- package/src/wire/media-types.ts +344 -0
- package/dist/elements/chunks/arrow-left-Bb46H7w9.js +0 -1
- package/dist/elements/chunks/check-C8desc_h.js +0 -1
- package/dist/elements/chunks/chevron-down-DKtc2HNB.js +0 -1
- package/dist/elements/chunks/chevron-right-Dw4GPhwL.js +0 -1
- package/dist/elements/chunks/circle-BeokBsvB.js +0 -1
- package/dist/elements/chunks/circle-check-wJSqt859.js +0 -1
- package/dist/elements/chunks/context-WHDl-xSx.js +0 -1
- package/dist/elements/chunks/conversation-list-CdR14_Y4.js +0 -1
- package/dist/elements/chunks/default-input-CSZqUM81.js +0 -1
- package/dist/elements/chunks/message-square-CnpAm_P0.js +0 -1
- package/dist/elements/chunks/model-switcher-CclaJEHt.js +0 -1
- package/dist/elements/chunks/x-D1qx0aLd.js +0 -1
- package/dist/register-impl-bzrZ44ft.js +0 -145
|
@@ -53,6 +53,63 @@ export function listIntegrations(): Integration[] {
|
|
|
53
53
|
return integrations;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/** One heading in `create-kai`'s gateway prompt. */
|
|
57
|
+
export interface GatewayGroup {
|
|
58
|
+
id: 'no-backend' | 'bring-a-key' | 'bring-a-server';
|
|
59
|
+
title: string;
|
|
60
|
+
integrations: Integration[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The gateway prompt's three groups, DERIVED — the thing the spec described and
|
|
65
|
+
* the catalog could not answer.
|
|
66
|
+
*
|
|
67
|
+
* It is here, and not in the CLI, for the reason `deps` is not in the spec's
|
|
68
|
+
* prose: a grouping maintained next to the menu that renders it is a second copy
|
|
69
|
+
* of the catalog, and it goes stale the first time an integration lands. Adding
|
|
70
|
+
* one now moves these groups by itself, because the only inputs are declared
|
|
71
|
+
* fields.
|
|
72
|
+
*
|
|
73
|
+
* THE ORDER OF THE TESTS IS THE DESIGN, not an implementation detail:
|
|
74
|
+
*
|
|
75
|
+
* 1. No route at all -> **No backend**. Exactly `mock` today, and derived from
|
|
76
|
+
* the absence rather than from `id === 'mock'` or `category === 'mock'`: a
|
|
77
|
+
* second no-backend entry (a canned transcript player, say) should land in
|
|
78
|
+
* this group without editing this function.
|
|
79
|
+
* 2. `outOfBand !== 'none'` -> **Bring a server or runtime**, and this is
|
|
80
|
+
* checked BEFORE the key test on purpose. `pydantic-ai` needs both a python
|
|
81
|
+
* runtime and OPENAI_API_KEY; the groups are not disjoint in reality, so
|
|
82
|
+
* something has to win, and the prerequisite does — a key is useless until
|
|
83
|
+
* the service it authenticates is running. The prompt still asks for the
|
|
84
|
+
* key afterwards; only the HEADING is decided here.
|
|
85
|
+
* 3. Everything else -> **Bring a key**.
|
|
86
|
+
*
|
|
87
|
+
* WHERE THIS DIVERGES FROM THE SPEC, ON PURPOSE. The spec's third group reads
|
|
88
|
+
* "Ollama, LangGraph, Mastra, Pydantic AI, Pi". `langgraph` comes out of this
|
|
89
|
+
* function in **Bring a key** instead, because its emitted route compiles the
|
|
90
|
+
* graph in process and asks for nothing but `OPENAI_API_KEY` — see the comment on
|
|
91
|
+
* its `outOfBand`. The spec sentence was written before the field existed and is
|
|
92
|
+
* wrong about that one entry; this is the derivation, so this is the answer.
|
|
93
|
+
*/
|
|
94
|
+
export function listGatewayGroups(): GatewayGroup[] {
|
|
95
|
+
const hasRoute = (i: Integration) =>
|
|
96
|
+
i.webRoute !== undefined || Object.keys(i.routeTemplates).length > 0;
|
|
97
|
+
|
|
98
|
+
const groups: GatewayGroup[] = [
|
|
99
|
+
{ id: 'no-backend', title: 'No backend', integrations: [] },
|
|
100
|
+
{ id: 'bring-a-key', title: 'Bring a key', integrations: [] },
|
|
101
|
+
{ id: 'bring-a-server', title: 'Bring a server or runtime', integrations: [] },
|
|
102
|
+
];
|
|
103
|
+
const into = (id: GatewayGroup['id']) => groups.find((g) => g.id === id)!;
|
|
104
|
+
|
|
105
|
+
for (const integration of integrations) {
|
|
106
|
+
if (!hasRoute(integration)) into('no-backend').integrations.push(integration);
|
|
107
|
+
else if (integration.outOfBand !== 'none') into('bring-a-server').integrations.push(integration);
|
|
108
|
+
else into('bring-a-key').integrations.push(integration);
|
|
109
|
+
}
|
|
110
|
+
return groups;
|
|
111
|
+
}
|
|
112
|
+
|
|
56
113
|
export function listArchetypes(): Archetype[] {
|
|
57
114
|
return archetypes;
|
|
58
115
|
}
|
|
@@ -44,6 +44,50 @@ export const Placement = z.enum(['side', 'full-page', 'docked-widget', 'inline']
|
|
|
44
44
|
*/
|
|
45
45
|
export const KeyExposure = z.enum(['frontend-safe', 'needs-proxy']);
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* What this integration needs the developer to supply OUT OF BAND — beyond an
|
|
49
|
+
* install and a key.
|
|
50
|
+
*
|
|
51
|
+
* `create-kai` groups its gateway prompt three ways: **No backend**, **Bring a
|
|
52
|
+
* key**, **Bring a server or runtime**. The third group is defined by exactly
|
|
53
|
+
* this question, and until now the catalog could not answer it. Every derivation
|
|
54
|
+
* that reproduced the spec's split did so by accident and broke on inspection:
|
|
55
|
+
* · `category` does not separate them at all — `provider` holds `openai` and
|
|
56
|
+
* `ollama`, `framework` holds `vercel-ai-sdk` and `langgraph`.
|
|
57
|
+
* · a non-empty `routeTemplates` picks out `cloudflare`/`pi`/`pydantic-ai`,
|
|
58
|
+
* which is a fact about how a route is EXPRESSED, not about what it needs.
|
|
59
|
+
* · "declares a secret env var" puts `langgraph` and `pydantic-ai` in the key
|
|
60
|
+
* group, which is right for one and wrong for the other.
|
|
61
|
+
* So it is declared, for the reason `keyExposure`, `forwardsFromClient` and
|
|
62
|
+
* `clientToolFormat` are each declared: only the route knows.
|
|
63
|
+
*
|
|
64
|
+
* The four values, and what makes each its own case for the CLI rather than a
|
|
65
|
+
* boolean it could have been:
|
|
66
|
+
*
|
|
67
|
+
* · `'none'` ......... `npm install` plus (maybe) a key is the whole story.
|
|
68
|
+
* The upstream is a remote HTTPS endpoint, or the agent runs IN the route.
|
|
69
|
+
* `langgraph` is the one worth reading twice: it is in the spec's "server or
|
|
70
|
+
* runtime" group and does not belong there — its emitted route builds the
|
|
71
|
+
* graph in process (`createReactAgent` + `new ChatOpenAI(...)`), so there is
|
|
72
|
+
* no LangGraph server anywhere and its runNote asks only for a key.
|
|
73
|
+
* · `'local-server'` . a separate process must already be LISTENING before the
|
|
74
|
+
* app works. The CLI cannot create it and must say so: `ollama serve` on
|
|
75
|
+
* 11434, a `mastra dev` server behind `MASTRA_URL`.
|
|
76
|
+
* · `'local-binary'` . an executable must be on PATH; the route spawns it on
|
|
77
|
+
* demand rather than talking to a port. Nothing is listening beforehand, so
|
|
78
|
+
* "start the server" is the wrong instruction — `pi` is the case.
|
|
79
|
+
* · `'language-runtime'` the emitted backend runs on a runtime the JS toolchain
|
|
80
|
+
* does not provide, and the user starts it themselves. `pydantic-ai` is a
|
|
81
|
+
* FastAPI service: `deps.pip`, `uvicorn main:app`, a second port.
|
|
82
|
+
*
|
|
83
|
+
* A boolean would collapse the last three, and they are three different sentences
|
|
84
|
+
* to print next to a menu item. The refinement below REFUSES an integration that
|
|
85
|
+
* declares nothing, for the same reason `keyExposure` does: a missing value read
|
|
86
|
+
* as `'none'` is a scaffold that says "npm run dev" to someone who needs to start
|
|
87
|
+
* Ollama first, and then reports as a broken kit.
|
|
88
|
+
*/
|
|
89
|
+
export const OutOfBand = z.enum(['none', 'local-server', 'local-binary', 'language-runtime']);
|
|
90
|
+
|
|
47
91
|
/**
|
|
48
92
|
* The packages an app must install BEYOND `@kitn.ai/ui` to run this
|
|
49
93
|
* integration's route.
|
|
@@ -93,6 +137,25 @@ export const SECRET_ENV_VAR = /(?:KEY|TOKEN|SECRET|PASSWORD)$/;
|
|
|
93
137
|
*/
|
|
94
138
|
const AUTH_HEADER = /Authorization\s*:|['"]x-api-key['"]\s*:/i;
|
|
95
139
|
|
|
140
|
+
/**
|
|
141
|
+
* A route that FETCHES a loopback address — so something local has to be
|
|
142
|
+
* listening. `ollama`'s `fetch('http://localhost:11434/v1/chat/completions')` is
|
|
143
|
+
* the case this catches.
|
|
144
|
+
*
|
|
145
|
+
* Anchored at the fetch/URL call, not matched anywhere in the source, and that
|
|
146
|
+
* precision is the whole point. Two other routes mention loopback in prose:
|
|
147
|
+
* mastra's `throw new Error('MASTRA_URL is not set… e.g. http://localhost:4111')`
|
|
148
|
+
* and pi's `console.log('chat api: http://localhost:3001/api/chat')`. A bare
|
|
149
|
+
* /localhost/ would flag both — mastra for the right answer by the wrong
|
|
150
|
+
* evidence, pi for a line about its OWN listening port, which says nothing about
|
|
151
|
+
* what it depends on. A net that is right by accident is the thing this field
|
|
152
|
+
* exists to replace, so it is narrowed until it only fires on a real dependency.
|
|
153
|
+
*/
|
|
154
|
+
const LOOPBACK_FETCH = /\b(?:fetch|URL)\s*\(\s*['"`]https?:\/\/(?:localhost|127\.0\.0\.1)/;
|
|
155
|
+
|
|
156
|
+
/** A route that shells out. The program it names must exist on the host. */
|
|
157
|
+
const SPAWNS_PROCESS = /\bfrom\s+['"]node:child_process['"]|\bspawn\s*\(/;
|
|
158
|
+
|
|
96
159
|
export const IntegrationSchema = z.object({
|
|
97
160
|
id: z.string(),
|
|
98
161
|
title: z.string(),
|
|
@@ -178,6 +241,13 @@ export const IntegrationSchema = z.object({
|
|
|
178
241
|
* the one field where the reader needs to be told what is at stake.
|
|
179
242
|
*/
|
|
180
243
|
keyExposure: KeyExposure.optional(),
|
|
244
|
+
/**
|
|
245
|
+
* See {@link OutOfBand}. Optional in the TYPE only so the refinement below owns
|
|
246
|
+
* the error message — Zod skips refinements when the base object already
|
|
247
|
+
* failed, so a plain required field would report "Invalid input" and this is a
|
|
248
|
+
* field whose reader needs to be told what the four values mean.
|
|
249
|
+
*/
|
|
250
|
+
outOfBand: OutOfBand.optional(),
|
|
181
251
|
}).superRefine((integration, ctx) => {
|
|
182
252
|
// Enforced against the real catalog by `registry.test.ts`, which parses every
|
|
183
253
|
// integration through this schema. A new host that forwards tools therefore
|
|
@@ -245,6 +315,68 @@ export const IntegrationSchema = z.object({
|
|
|
245
315
|
});
|
|
246
316
|
}
|
|
247
317
|
}
|
|
318
|
+
|
|
319
|
+
// ---- outOfBand ----
|
|
320
|
+
//
|
|
321
|
+
// Same two-check shape as keyExposure, and the same asymmetry. The first
|
|
322
|
+
// refuses SILENCE. The second refuses a specific LIE, and only in the direction
|
|
323
|
+
// that costs the developer something: it can turn a `'none'` claim into an
|
|
324
|
+
// error, never the reverse. Over-declaring prints a prerequisite that was not
|
|
325
|
+
// strictly needed; under-declaring ships a scaffold that cannot possibly run
|
|
326
|
+
// and blames the kit.
|
|
327
|
+
|
|
328
|
+
if (integration.outOfBand === undefined) {
|
|
329
|
+
ctx.addIssue({
|
|
330
|
+
code: z.ZodIssueCode.custom,
|
|
331
|
+
path: ['outOfBand'],
|
|
332
|
+
message:
|
|
333
|
+
`integration '${integration.id}' declares no outOfBand. State what the developer must supply ` +
|
|
334
|
+
`BEYOND an install and a key, because that is the group create-kai's gateway prompt puts it in: ` +
|
|
335
|
+
`'none' (a remote endpoint, or the agent runs inside the route), 'local-server' (a process must ` +
|
|
336
|
+
`already be listening, e.g. 'ollama serve'), 'local-binary' (an executable on PATH that the route ` +
|
|
337
|
+
`spawns), or 'language-runtime' (the emitted backend needs a runtime the JS toolchain does not ` +
|
|
338
|
+
`provide, e.g. python + uvicorn). Read the route and the runNote to decide, not the category: ` +
|
|
339
|
+
`'provider' holds both openai and ollama. There is no default on purpose — a missing value read ` +
|
|
340
|
+
`as 'none' is a scaffold that says "npm run dev" to someone who has nothing to connect to.`,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (integration.outOfBand === 'none') {
|
|
345
|
+
const routeSources = [
|
|
346
|
+
...Object.values(integration.routeTemplates),
|
|
347
|
+
...(integration.webRoute ? [integration.webRoute] : []),
|
|
348
|
+
];
|
|
349
|
+
if (integration.language === 'python') {
|
|
350
|
+
ctx.addIssue({
|
|
351
|
+
code: z.ZodIssueCode.custom,
|
|
352
|
+
path: ['outOfBand'],
|
|
353
|
+
message:
|
|
354
|
+
`integration '${integration.id}' claims outOfBand 'none' but its route is python, so a consumer ` +
|
|
355
|
+
`whose whole toolchain is node cannot run it without an interpreter and an ASGI server they ` +
|
|
356
|
+
`start themselves. Use 'language-runtime'.`,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
if (routeSources.some((code) => SPAWNS_PROCESS.test(code))) {
|
|
360
|
+
ctx.addIssue({
|
|
361
|
+
code: z.ZodIssueCode.custom,
|
|
362
|
+
path: ['outOfBand'],
|
|
363
|
+
message:
|
|
364
|
+
`integration '${integration.id}' claims outOfBand 'none' but its route spawns a child process, ` +
|
|
365
|
+
`so the program it spawns has to be installed on the host — nothing in package.json puts it ` +
|
|
366
|
+
`there. Use 'local-binary'.`,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
if (routeSources.some((code) => LOOPBACK_FETCH.test(code))) {
|
|
370
|
+
ctx.addIssue({
|
|
371
|
+
code: z.ZodIssueCode.custom,
|
|
372
|
+
path: ['outOfBand'],
|
|
373
|
+
message:
|
|
374
|
+
`integration '${integration.id}' claims outOfBand 'none' but its route fetches a loopback ` +
|
|
375
|
+
`address, so something local has to be listening before the first message works. ` +
|
|
376
|
+
`Use 'local-server'.`,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
248
380
|
});
|
|
249
381
|
export type Integration = z.infer<typeof IntegrationSchema>;
|
|
250
382
|
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
Context, ContextTrigger, ContextContent, ContextContentHeader,
|
|
12
12
|
ContextContentBody, ContextContentFooter, ContextInputUsage, ContextOutputUsage,
|
|
13
13
|
} from './context';
|
|
14
|
-
import { DefaultPromptInput } from '../elements/default-input';
|
|
14
|
+
import { DefaultPromptInput, type RejectedAttachment } from '../elements/default-input';
|
|
15
|
+
import type { MediaTypeFilter } from '../wire/media-types';
|
|
15
16
|
import type { TriggerDef } from './composer';
|
|
16
17
|
import type { ChatMessage } from '../elements/chat-types';
|
|
17
18
|
import type { ProseSize } from '../primitives/chat-config';
|
|
@@ -112,6 +113,13 @@ export interface ChatThreadProps {
|
|
|
112
113
|
composerActions?: boolean;
|
|
113
114
|
/** INJECT: footer row below the composer (disclaimers, token meter, …). */
|
|
114
115
|
footer?: boolean;
|
|
116
|
+
/** Which attachment media types the user may stage, in HTML `accept` syntax
|
|
117
|
+
* (`'image/*,application/pdf'`). Omitted means no filter. Narrowed by what the
|
|
118
|
+
* encoders can actually send — the same string, and the same resolver, as
|
|
119
|
+
* `toOpenAIMessages(msgs, { accept })`. */
|
|
120
|
+
accept?: MediaTypeFilter;
|
|
121
|
+
/** Files the composer refused because `accept` excluded them. */
|
|
122
|
+
onAttachmentsRejected?: (rejected: RejectedAttachment[]) => void;
|
|
115
123
|
/** Show a Search (Globe) button in the input toolbar; fires a `search` event. */
|
|
116
124
|
search?: boolean;
|
|
117
125
|
/** Show a Voice (Mic) button in the input toolbar; fires a `voice` event. */
|
|
@@ -392,6 +400,7 @@ export function ChatThread(props: ChatThreadProps) {
|
|
|
392
400
|
<DefaultPromptInput
|
|
393
401
|
value={current()} placeholder={props.placeholder} loading={props.loading === true}
|
|
394
402
|
suggestions={visibleSuggestions()} attachments={attachments()}
|
|
403
|
+
accept={props.accept} onAttachmentsRejected={props.onAttachmentsRejected}
|
|
395
404
|
search={props.search === true} voice={props.voice === true}
|
|
396
405
|
triggers={props.triggers} kindIcons={props.kindIcons}
|
|
397
406
|
onValueChange={handleChange} onSubmit={handleSubmit} onSuggestionClick={handleSuggestionClick}
|
|
@@ -2,12 +2,12 @@ import { type JSX, Show, createContext, createMemo, useContext } from 'solid-js'
|
|
|
2
2
|
import { HoverCardRoot, HoverCardTrigger, HoverCardContent } from '../ui/hover-card';
|
|
3
3
|
import { cn } from '../utils/cn';
|
|
4
4
|
import { Button } from '../ui/button';
|
|
5
|
+
import { ProgressBar, type ProgressTone } from '../ui/progress-bar';
|
|
5
6
|
|
|
6
7
|
const ICON_RADIUS = 10;
|
|
7
8
|
const ICON_VIEWBOX = 24;
|
|
8
9
|
const ICON_CENTER = 12;
|
|
9
10
|
const ICON_STROKE_WIDTH = 2;
|
|
10
|
-
const PERCENT_MAX = 100;
|
|
11
11
|
|
|
12
12
|
/** Default fraction (0–1) at which the meter transitions to the warning colour. */
|
|
13
13
|
export const DEFAULT_WARN_THRESHOLD = 0.7;
|
|
@@ -206,7 +206,6 @@ export function ContextContentHeader(props: ContextContentHeaderProps) {
|
|
|
206
206
|
const displayPct = createMemo(() => fmtPercent.format(usedPercent()));
|
|
207
207
|
const used = createMemo(() => fmtCompact.format(ctx.usedTokens));
|
|
208
208
|
const total = createMemo(() => fmtCompact.format(ctx.maxTokens));
|
|
209
|
-
const barWidth = createMemo(() => `${Math.min(usedPercent() * PERCENT_MAX, PERCENT_MAX)}%`);
|
|
210
209
|
|
|
211
210
|
const severity = createMemo<ContextSeverity>(() => {
|
|
212
211
|
const pct = usedPercent();
|
|
@@ -215,11 +214,16 @@ export function ContextContentHeader(props: ContextContentHeaderProps) {
|
|
|
215
214
|
return 'ok';
|
|
216
215
|
});
|
|
217
216
|
|
|
218
|
-
|
|
217
|
+
// Severity used to pick a bare hue class and nothing else, so the meter was a
|
|
218
|
+
// pair of styled divs: no role, no value, invisible to assistive tech. Map it to
|
|
219
|
+
// the shared ProgressBar's semantic `tone` instead and let that primitive supply
|
|
220
|
+
// `role="progressbar"` + aria-valuenow/min/max, rather than hand-rolling those
|
|
221
|
+
// onto the divs.
|
|
222
|
+
const tone = createMemo<ProgressTone>(() => {
|
|
219
223
|
const s = severity();
|
|
220
|
-
if (s === 'danger') return '
|
|
221
|
-
if (s === 'warn') return '
|
|
222
|
-
return '
|
|
224
|
+
if (s === 'danger') return 'error';
|
|
225
|
+
if (s === 'warn') return 'warning';
|
|
226
|
+
return 'primary';
|
|
223
227
|
});
|
|
224
228
|
|
|
225
229
|
return (
|
|
@@ -232,12 +236,14 @@ export function ContextContentHeader(props: ContextContentHeaderProps) {
|
|
|
232
236
|
</p>
|
|
233
237
|
</div>
|
|
234
238
|
<div class="space-y-2">
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
239
|
+
{/* Values are the raw token counts, so a screen reader announces
|
|
240
|
+
"190,000 of 200,000" rather than a bare percentage. */}
|
|
241
|
+
<ProgressBar
|
|
242
|
+
value={ctx.usedTokens}
|
|
243
|
+
max={ctx.maxTokens}
|
|
244
|
+
tone={tone()}
|
|
245
|
+
aria-label="Context usage"
|
|
246
|
+
/>
|
|
241
247
|
</div>
|
|
242
248
|
</Show>
|
|
243
249
|
</div>
|
|
@@ -43,6 +43,12 @@ export function ConversationItem(props: ConversationItemProps) {
|
|
|
43
43
|
return (
|
|
44
44
|
<button
|
|
45
45
|
data-conversation-id={local.conversation.id}
|
|
46
|
+
// `isActive` drives the selected LOOK below; it has to reach assistive tech
|
|
47
|
+
// too, or the active conversation is visible only to sighted users. `true`
|
|
48
|
+
// rather than `page` because this selects a conversation within the app, it
|
|
49
|
+
// is not page navigation — same call as ui/agent-card.tsx and ui/pane-group.tsx
|
|
50
|
+
// (ui/nav.tsx uses `page` because its items really are nav links).
|
|
51
|
+
aria-current={local.isActive ? 'true' : undefined}
|
|
46
52
|
onClick={() => local.onSelect(local.conversation.id)}
|
|
47
53
|
class={cn(
|
|
48
54
|
'w-full rounded-lg text-left transition-colors',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { splitProps, For, Show, createSignal } from 'solid-js';
|
|
2
2
|
import { cn } from '../utils/cn';
|
|
3
|
-
import { Dropdown, DropdownTrigger, DropdownContent,
|
|
3
|
+
import { Dropdown, DropdownTrigger, DropdownContent, DropdownRadioItem, type DropdownController } from '../ui/dropdown';
|
|
4
4
|
import { Button } from '../ui/button';
|
|
5
5
|
import type { ModelOption } from '../types';
|
|
6
6
|
|
|
@@ -23,15 +23,37 @@ const Chevron = (props: { class?: string }) => (
|
|
|
23
23
|
</svg>
|
|
24
24
|
);
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* One selectable model.
|
|
28
|
+
*
|
|
29
|
+
* A single-select list of models IS a radio group, so this routes through
|
|
30
|
+
* `DropdownRadioItem` (`role="menuitemradio"` + `aria-checked`) rather than
|
|
31
|
+
* hand-rolling those attributes onto a plain `DropdownItem`. Being the current
|
|
32
|
+
* model used to render as `font-medium` and nothing else — invisible to a screen
|
|
33
|
+
* reader. `kai-menu` already renders its radio items this way.
|
|
34
|
+
*
|
|
35
|
+
* `DropdownRadioItem` deliberately does NOT close the menu on activation (it is
|
|
36
|
+
* built for groups the consumer keeps mutating), so `onSelect` closes it here to
|
|
37
|
+
* preserve the pick-and-close behaviour `DropdownItem` used to provide.
|
|
38
|
+
*/
|
|
39
|
+
function ModelRow(props: {
|
|
40
|
+
model: ModelOption;
|
|
41
|
+
currentModelId: string;
|
|
42
|
+
onModelChange: (id: string) => void;
|
|
43
|
+
onClose: () => void;
|
|
44
|
+
}) {
|
|
27
45
|
const subtitle = () => props.model.description ?? props.model.provider;
|
|
46
|
+
const checked = () => props.model.id === props.currentModelId;
|
|
28
47
|
return (
|
|
29
|
-
<
|
|
48
|
+
<DropdownRadioItem
|
|
49
|
+
checked={checked()}
|
|
50
|
+
onSelect={() => { props.onModelChange(props.model.id); props.onClose(); }}
|
|
51
|
+
>
|
|
30
52
|
<div class="flex flex-col">
|
|
31
|
-
<span class={cn('text-body',
|
|
53
|
+
<span class={cn('text-body', checked() && 'font-medium text-foreground')}>{props.model.name}</span>
|
|
32
54
|
<Show when={subtitle()}><span class="text-caption text-muted-foreground">{subtitle()}</span></Show>
|
|
33
55
|
</div>
|
|
34
|
-
</
|
|
56
|
+
</DropdownRadioItem>
|
|
35
57
|
);
|
|
36
58
|
}
|
|
37
59
|
|
|
@@ -39,6 +61,14 @@ export function ModelSwitcher(props: ModelSwitcherProps) {
|
|
|
39
61
|
const [local] = splitProps(props, ['models', 'currentModelId', 'onModelChange', 'class', 'defaultOpen', 'disabled', 'controllerRef']);
|
|
40
62
|
const currentModel = () => local.models.find((m) => m.id === local.currentModelId);
|
|
41
63
|
|
|
64
|
+
// The rows are DropdownRadioItems, which stay open by design, so the switcher
|
|
65
|
+
// needs its own handle on the dropdown to close after a pick. Tapping
|
|
66
|
+
// `controllerRef` keeps that inside this component — the dropdown's own context
|
|
67
|
+
// is module-private — and the consumer's `controllerRef` is still called, so
|
|
68
|
+
// this is invisible from the outside.
|
|
69
|
+
let controller: DropdownController | undefined;
|
|
70
|
+
const closeMenu = () => controller?.setOpen(false);
|
|
71
|
+
|
|
42
72
|
// Ungrouped models list first; grouped models collect under their group name,
|
|
43
73
|
// preserving first-seen group order.
|
|
44
74
|
const ungrouped = () => local.models.filter((m) => !m.group);
|
|
@@ -58,7 +88,7 @@ export function ModelSwitcher(props: ModelSwitcherProps) {
|
|
|
58
88
|
<Dropdown
|
|
59
89
|
defaultOpen={local.defaultOpen}
|
|
60
90
|
disabled={local.disabled}
|
|
61
|
-
controllerRef={local.controllerRef}
|
|
91
|
+
controllerRef={(api) => { controller = api; local.controllerRef?.(api); }}
|
|
62
92
|
>
|
|
63
93
|
<DropdownTrigger as={(triggerProps: any) => (
|
|
64
94
|
<Button variant="ghost" size="sm" class={cn('gap-1 text-meta text-muted-foreground', local.class)} {...triggerProps}>
|
|
@@ -68,7 +98,7 @@ export function ModelSwitcher(props: ModelSwitcherProps) {
|
|
|
68
98
|
)} />
|
|
69
99
|
<DropdownContent>
|
|
70
100
|
<For each={ungrouped()}>
|
|
71
|
-
{(model) => <ModelRow model={model} currentModelId={local.currentModelId} onModelChange={local.onModelChange} />}
|
|
101
|
+
{(model) => <ModelRow model={model} currentModelId={local.currentModelId} onModelChange={local.onModelChange} onClose={closeMenu} />}
|
|
72
102
|
</For>
|
|
73
103
|
<For each={groups()}>
|
|
74
104
|
{(group) => {
|
|
@@ -87,7 +117,7 @@ export function ModelSwitcher(props: ModelSwitcherProps) {
|
|
|
87
117
|
<Show when={open()}>
|
|
88
118
|
<div class="pl-2">
|
|
89
119
|
<For each={group.models}>
|
|
90
|
-
{(model) => <ModelRow model={model} currentModelId={local.currentModelId} onModelChange={local.onModelChange} />}
|
|
120
|
+
{(model) => <ModelRow model={model} currentModelId={local.currentModelId} onModelChange={local.onModelChange} onClose={closeMenu} />}
|
|
91
121
|
</For>
|
|
92
122
|
</div>
|
|
93
123
|
</Show>
|
package/src/components/toast.tsx
CHANGED
|
@@ -214,7 +214,12 @@ export function Toast(props: ToastProps) {
|
|
|
214
214
|
<Show when={presence.present()}>
|
|
215
215
|
<div
|
|
216
216
|
ref={presence.setRef}
|
|
217
|
-
|
|
217
|
+
// `variant` already picks the error ICON; it has to pick the ROLE too, or
|
|
218
|
+
// a failure is announced with the same (polite, easily missed) urgency as
|
|
219
|
+
// a success. `alert` is implicitly assertive and interrupts, which is what
|
|
220
|
+
// the region's `aria-live="polite"` alone will not do for the toast that
|
|
221
|
+
// actually matters. Same expression as ui/notice.tsx.
|
|
222
|
+
role={variant() === 'error' ? 'alert' : 'status'}
|
|
218
223
|
data-appearance={appearance()}
|
|
219
224
|
data-inverse={inverse() ? '' : undefined}
|
|
220
225
|
data-closed={presence.state() === 'closed' ? '' : undefined}
|
package/src/elements/button.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { Show } from 'solid-js';
|
|
|
2
2
|
import { Button } from '../ui/button';
|
|
3
3
|
import { renderIcon } from '../ui/icon';
|
|
4
4
|
import { defineWebComponent } from './define';
|
|
5
|
+
import { createSlotText } from './slot-text';
|
|
5
6
|
|
|
6
7
|
interface Props extends Record<string, unknown> {
|
|
7
8
|
/** Visual style. `default` (filled), `subtle` (muted text, hover tint, the
|
|
@@ -17,7 +18,12 @@ interface Props extends Record<string, unknown> {
|
|
|
17
18
|
/** Trailing icon, after the label (e.g. `"chevron-down"` for a menu affordance). */
|
|
18
19
|
iconTrailing?: string;
|
|
19
20
|
/** Accessible name. REQUIRED for icon-only buttons (no visible text); ignored
|
|
20
|
-
* when you slot visible text, which already names the button.
|
|
21
|
+
* when you slot visible text, which already names the button. An `aria-label`
|
|
22
|
+
* on top of visible text REPLACES that name rather than adding to it, so a
|
|
23
|
+
* button reading "Save" that answers to "Submit" is unusable by speech input
|
|
24
|
+
* (WCAG 2.5.3, Label in Name). The visible text wins. An `icon` / `icon-sm`
|
|
25
|
+
* size hides the slot, which makes the button icon-only whatever you slotted,
|
|
26
|
+
* so `label` is what names it there. */
|
|
21
27
|
label?: string;
|
|
22
28
|
/** Disable the button (non-interactive, dimmed). */
|
|
23
29
|
disabled?: boolean;
|
|
@@ -71,6 +77,23 @@ defineWebComponent<Props, Events>('kai-button', {
|
|
|
71
77
|
// `label` prop still names it for assistive tech).
|
|
72
78
|
const iconOnly = () => props.size === 'icon' || props.size === 'icon-sm';
|
|
73
79
|
|
|
80
|
+
// Slotted text names the button by itself (name-from-contents reaches through
|
|
81
|
+
// the slot), so `label` must NOT be emitted on top of it: aria-label REPLACES
|
|
82
|
+
// the computed name, and a name that disagrees with the visible text locks
|
|
83
|
+
// speech-input users out of the control (WCAG 2.5.3). This is what the `label`
|
|
84
|
+
// doc has always promised, and what `kai-checkpoint` already does.
|
|
85
|
+
//
|
|
86
|
+
// `flatten` so a consumer who wraps <kai-button> in their own component and
|
|
87
|
+
// forwards a <slot> still counts as having visible text. Safe here because the
|
|
88
|
+
// default slot has no fallback content — see createSlotText.
|
|
89
|
+
const slotted = createSlotText({ flatten: true });
|
|
90
|
+
|
|
91
|
+
// The name to emit, or nothing. An `icon`/`icon-sm` size does not render the
|
|
92
|
+
// default slot at all, so whatever was slotted is INVISIBLE and `label` is the
|
|
93
|
+
// only name available — dropping it there would leave icon buttons nameless,
|
|
94
|
+
// which is the worse bug.
|
|
95
|
+
const ariaLabel = () => (!iconOnly() && slotted.hasText() ? undefined : props.label);
|
|
96
|
+
|
|
74
97
|
// ── Imperative API (instance methods on the host) ──────────────────────────
|
|
75
98
|
// The real <button> lives in the shadow root, so native focus()/blur()/click()
|
|
76
99
|
// on the host hit the wrapper, not the control. Forward to the inner button.
|
|
@@ -104,7 +127,7 @@ defineWebComponent<Props, Events>('kai-button', {
|
|
|
104
127
|
full={flag('full') || !iconOnly()}
|
|
105
128
|
type={props.type ?? 'button'}
|
|
106
129
|
disabled={flag('disabled')}
|
|
107
|
-
aria-label={
|
|
130
|
+
aria-label={ariaLabel()}
|
|
108
131
|
onClick={() => dispatch('kai-click')}
|
|
109
132
|
>
|
|
110
133
|
{/* Leading icon: a slotted SVG (slot="icon") wins — drop in any icon
|
|
@@ -115,7 +138,7 @@ defineWebComponent<Props, Events>('kai-button', {
|
|
|
115
138
|
</Show>
|
|
116
139
|
</slot>
|
|
117
140
|
<Show when={!iconOnly()}>
|
|
118
|
-
<slot />
|
|
141
|
+
<slot ref={slotted.ref} />
|
|
119
142
|
</Show>
|
|
120
143
|
<Show when={props.iconTrailing && !iconOnly()}>
|
|
121
144
|
{renderIcon(props.iconTrailing, { class: 'size-4 shrink-0 opacity-60', imgClass: 'size-4 shrink-0', spanClass: 'inline-flex size-4 shrink-0 items-center justify-center', ariaHidden: true })}
|
package/src/elements/chat.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { ChatThread, type ChatThreadProps, type ChatThreadContextUsage, type Cha
|
|
|
5
5
|
import { cardComponentsFromTags } from './message';
|
|
6
6
|
import { createMessagesGuard } from './validate-messages';
|
|
7
7
|
import type { AttachmentData } from '../components/attachments';
|
|
8
|
+
import type { RejectedAttachment } from './default-input';
|
|
8
9
|
import type { ChatMessage } from './chat-types';
|
|
9
10
|
import type { TriggerDef } from '../components/composer';
|
|
10
11
|
import type { ComposerDoc } from '../primitives/composer-model';
|
|
@@ -13,7 +14,25 @@ import type { ModelOption } from '../types';
|
|
|
13
14
|
|
|
14
15
|
type Props = Omit<ChatThreadProps,
|
|
15
16
|
'class' | 'onValueChange' | 'onSubmit' | 'onAttachmentsChange' | 'onSuggestionClick' | 'onModelChange'
|
|
16
|
-
| 'onMessageAction' | 'onSearch' | 'onVoice' | 'controllerRef' | 'cardTypes' | 'cardSchemas' | 'messages'
|
|
17
|
+
| 'onMessageAction' | 'onSearch' | 'onVoice' | 'controllerRef' | 'cardTypes' | 'cardSchemas' | 'messages'
|
|
18
|
+
| 'accept' | 'onAttachmentsRejected'> & Record<string, unknown> & {
|
|
19
|
+
/** Which attachment media types the user may stage, in HTML `accept` syntax:
|
|
20
|
+
* `<kai-chat accept="image/*,application/pdf">`. A plain string, so unlike
|
|
21
|
+
* `messages` it DOES work as an attribute. Omitted means no filter.
|
|
22
|
+
*
|
|
23
|
+
* MEDIA TYPES ONLY -- exact (`image/png`) or subtype wildcard (`text/*`).
|
|
24
|
+
* HTML allows a file extension here and this does not: `accept=".py"` THROWS
|
|
25
|
+
* with the entry named, rather than silently resolving to a picker that
|
|
26
|
+
* accepts nothing.
|
|
27
|
+
*
|
|
28
|
+
* It can only NARROW what the kit can already encode: `accept="image/*"`
|
|
29
|
+
* resolves to the four image formats both APIs take, not to every image type
|
|
30
|
+
* the OS offers. Pass the SAME string to `toOpenAIMessages(msgs, { accept })`
|
|
31
|
+
* and the picker and the wire cannot disagree -- both resolve it through
|
|
32
|
+
* `resolveMediaPolicy` against one declaration. That declaration is readable
|
|
33
|
+
* as `encodableMediaTypes()` from `@kitn.ai/ui/wire`, if you would rather
|
|
34
|
+
* build your own picker than use this prop. */
|
|
35
|
+
accept?: string;
|
|
17
36
|
/** The full message thread to render, newest last. Each entry carries its
|
|
18
37
|
* role, ordered `parts`, and optional actions/avatar/feedback. Set as a JS
|
|
19
38
|
* property (`el.messages = [...]`); a NEW array reference per streaming
|
|
@@ -54,6 +73,11 @@ interface Events {
|
|
|
54
73
|
/** The staged attachments changed (file added or removed). Carries the full
|
|
55
74
|
* current list so a consumer can react in real time. */
|
|
56
75
|
'kai-attachments-change': { attachments: AttachmentData[] };
|
|
76
|
+
/** One or more picked files were refused because `accept` excluded them. The
|
|
77
|
+
* element renders NO message of its own: it reports the facts (name, media
|
|
78
|
+
* type, whether the kit could have sent it) and what the user should see is
|
|
79
|
+
* the application's call. Only ever fires when `accept` is set. */
|
|
80
|
+
'kai-attachments-rejected': { rejected: RejectedAttachment[] };
|
|
57
81
|
/** A suggestion chip was clicked (only in `suggestion-mode="fill"`). */
|
|
58
82
|
'kai-suggestion-click': { value: string };
|
|
59
83
|
/** An action button on a message was clicked. `action` is the built-in name or
|
|
@@ -74,7 +98,7 @@ defineWebComponent<Props, Events>('kai-chat', {
|
|
|
74
98
|
codeTheme: 'github-dark-dimmed', codeHighlight: true, chatTitle: undefined,
|
|
75
99
|
models: undefined, currentModel: undefined, context: undefined, scrollButton: true,
|
|
76
100
|
search: false, voice: false, triggers: undefined, kindIcons: undefined,
|
|
77
|
-
actionsReveal: 'always', cardTypes: undefined, cardSchemas: undefined,
|
|
101
|
+
actionsReveal: 'always', cardTypes: undefined, cardSchemas: undefined, accept: undefined,
|
|
78
102
|
}, (props, { dispatch, flag, element, expose }) => {
|
|
79
103
|
// `messages` is an untyped boundary: a consumer can hand it anything at
|
|
80
104
|
// runtime (a pre-0.20.0 `{ id, role, content }` array, in particular). Skip
|
|
@@ -145,7 +169,9 @@ defineWebComponent<Props, Events>('kai-chat', {
|
|
|
145
169
|
cardSchemas={props.cardSchemas as Record<string, object> | undefined}
|
|
146
170
|
onValueChange={(value) => dispatch('kai-value-change', { value })}
|
|
147
171
|
onSubmit={(detail) => dispatch('kai-submit', detail)}
|
|
172
|
+
accept={props.accept as string | undefined}
|
|
148
173
|
onAttachmentsChange={(attachments) => dispatch('kai-attachments-change', { attachments })}
|
|
174
|
+
onAttachmentsRejected={(rejected) => dispatch('kai-attachments-rejected', { rejected })}
|
|
149
175
|
onSuggestionClick={(value) => dispatch('kai-suggestion-click', { value })}
|
|
150
176
|
onModelChange={(modelId) => dispatch('kai-model-change', { modelId })}
|
|
151
177
|
onMessageAction={(detail) => dispatch('kai-message-action', detail)}
|