@kitn.ai/ui 0.14.1

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.
Files changed (370) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +394 -0
  3. package/dist/bash-InADTalH.js +1 -0
  4. package/dist/core-AYMC6_lb.js +12 -0
  5. package/dist/css-M7EaDHN_.js +1 -0
  6. package/dist/custom-elements.json +4739 -0
  7. package/dist/engine-javascript-vq0WuIJl.js +141 -0
  8. package/dist/github-dark-dimmed-DUshB20C.js +1 -0
  9. package/dist/github-light-JYsPkUQd.js +1 -0
  10. package/dist/host-embed.d.ts +33 -0
  11. package/dist/html-CPZ3oZQ7.js +1 -0
  12. package/dist/index.d.ts +2 -0
  13. package/dist/javascript-C25yR2R2.js +1 -0
  14. package/dist/json-DxJze_jm.js +1 -0
  15. package/dist/kitn-chat-provider.d.ts +2 -0
  16. package/dist/kitn-chat-provider.es.js +240 -0
  17. package/dist/kitn-chat.es.js +77 -0
  18. package/dist/llms/llms-full.txt +1018 -0
  19. package/dist/llms/llms.txt +104 -0
  20. package/dist/origin.d.ts +5 -0
  21. package/dist/provider-runtime.d.ts +27 -0
  22. package/dist/schemas/card-envelope.schema.json +14 -0
  23. package/dist/schemas/card-event.schema.json +12 -0
  24. package/dist/schemas/choice.schema.json +70 -0
  25. package/dist/schemas/confirm.schema.json +65 -0
  26. package/dist/schemas/embed.schema.json +65 -0
  27. package/dist/schemas/form.result.schema.json +7 -0
  28. package/dist/schemas/form.schema.json +33 -0
  29. package/dist/schemas/link.schema.json +56 -0
  30. package/dist/schemas/tasks.result.schema.json +16 -0
  31. package/dist/schemas/tasks.schema.json +78 -0
  32. package/dist/svelte--5p79yCD.js +1 -0
  33. package/dist/theme.tokens.css +176 -0
  34. package/dist/tsx-B8rCNbgL.js +1 -0
  35. package/dist/typescript-RycA9KXf.js +1 -0
  36. package/dist/validate.d.ts +4 -0
  37. package/dist/version.d.ts +4 -0
  38. package/dist/vue-BmIZj4XD.js +1 -0
  39. package/dist/wire.d.ts +53 -0
  40. package/frameworks/react/index.tsx +948 -0
  41. package/frameworks/react/runtime.tsx +94 -0
  42. package/llms-full.txt +1018 -0
  43. package/llms.txt +104 -0
  44. package/package.json +141 -0
  45. package/src/components/artifact.stories.tsx +138 -0
  46. package/src/components/artifact.tsx +743 -0
  47. package/src/components/attachments.stories.tsx +303 -0
  48. package/src/components/attachments.test.tsx +64 -0
  49. package/src/components/attachments.tsx +399 -0
  50. package/src/components/card-fallback.tsx +28 -0
  51. package/src/components/card-renderer.tsx +52 -0
  52. package/src/components/card.tsx +110 -0
  53. package/src/components/chain-of-thought.stories.tsx +211 -0
  54. package/src/components/chain-of-thought.tsx +139 -0
  55. package/src/components/chat-container.stories.tsx +187 -0
  56. package/src/components/chat-container.tsx +82 -0
  57. package/src/components/chat-scope-picker.tsx +47 -0
  58. package/src/components/chat-thread.test.tsx +58 -0
  59. package/src/components/chat-thread.tsx +249 -0
  60. package/src/components/checkpoint.stories.tsx +102 -0
  61. package/src/components/checkpoint.tsx +88 -0
  62. package/src/components/choice-card.tsx +569 -0
  63. package/src/components/code-block.stories.tsx +150 -0
  64. package/src/components/code-block.tsx +105 -0
  65. package/src/components/component-meta.json +3735 -0
  66. package/src/components/confirm-card.stories.tsx +74 -0
  67. package/src/components/confirm-card.tsx +315 -0
  68. package/src/components/context.stories.tsx +229 -0
  69. package/src/components/context.test.tsx +133 -0
  70. package/src/components/context.tsx +363 -0
  71. package/src/components/conversation-item.stories.tsx +125 -0
  72. package/src/components/conversation-item.tsx +16 -0
  73. package/src/components/conversation-list.stories.tsx +133 -0
  74. package/src/components/conversation-list.tsx +101 -0
  75. package/src/components/default-input.test.tsx +111 -0
  76. package/src/components/embed.tsx +196 -0
  77. package/src/components/empty.stories.tsx +434 -0
  78. package/src/components/empty.tsx +166 -0
  79. package/src/components/feedback-bar.stories.tsx +138 -0
  80. package/src/components/feedback-bar.test.tsx +103 -0
  81. package/src/components/feedback-bar.tsx +166 -0
  82. package/src/components/file-tree.stories.tsx +73 -0
  83. package/src/components/file-tree.tsx +383 -0
  84. package/src/components/file-upload.stories.tsx +156 -0
  85. package/src/components/file-upload.tsx +161 -0
  86. package/src/components/form-widgets.tsx +461 -0
  87. package/src/components/form.tsx +873 -0
  88. package/src/components/image.stories.tsx +89 -0
  89. package/src/components/image.tsx +67 -0
  90. package/src/components/link-preview.tsx +194 -0
  91. package/src/components/loader.stories.tsx +181 -0
  92. package/src/components/loader.tsx +333 -0
  93. package/src/components/markdown.stories.tsx +180 -0
  94. package/src/components/markdown.tsx +81 -0
  95. package/src/components/message-action-bar.test.tsx +128 -0
  96. package/src/components/message-narrow.stories.tsx +329 -0
  97. package/src/components/message-skills.stories.tsx +211 -0
  98. package/src/components/message-skills.tsx +36 -0
  99. package/src/components/message.stories.tsx +281 -0
  100. package/src/components/message.tsx +227 -0
  101. package/src/components/model-switcher.stories.tsx +123 -0
  102. package/src/components/model-switcher.test.tsx +65 -0
  103. package/src/components/model-switcher.tsx +87 -0
  104. package/src/components/prompt-input.stories.tsx +222 -0
  105. package/src/components/prompt-input.tsx +211 -0
  106. package/src/components/prompt-suggestion.stories.tsx +142 -0
  107. package/src/components/prompt-suggestion.tsx +115 -0
  108. package/src/components/reasoning.stories.tsx +140 -0
  109. package/src/components/reasoning.tsx +156 -0
  110. package/src/components/resizable.test.tsx +226 -0
  111. package/src/components/response-stream.tsx +103 -0
  112. package/src/components/scroll-button.stories.tsx +100 -0
  113. package/src/components/scroll-button.tsx +34 -0
  114. package/src/components/slash-command.stories.tsx +163 -0
  115. package/src/components/slash-command.tsx +232 -0
  116. package/src/components/source-list.test.tsx +152 -0
  117. package/src/components/source.stories.tsx +124 -0
  118. package/src/components/source.tsx +129 -0
  119. package/src/components/tasks-card.stories.tsx +78 -0
  120. package/src/components/tasks-card.tsx +432 -0
  121. package/src/components/text-shimmer.stories.tsx +87 -0
  122. package/src/components/text-shimmer.tsx +37 -0
  123. package/src/components/thinking-bar.stories.tsx +87 -0
  124. package/src/components/thinking-bar.tsx +50 -0
  125. package/src/components/tool.stories.tsx +153 -0
  126. package/src/components/tool.tsx +184 -0
  127. package/src/components/use-card-resolution.ts +28 -0
  128. package/src/components/voice-input.stories.tsx +83 -0
  129. package/src/components/voice-input.tsx +107 -0
  130. package/src/elements/artifact.stories.tsx +506 -0
  131. package/src/elements/artifact.tsx +137 -0
  132. package/src/elements/attachments.stories.tsx +190 -0
  133. package/src/elements/attachments.tsx +136 -0
  134. package/src/elements/card.stories.tsx +113 -0
  135. package/src/elements/card.tsx +40 -0
  136. package/src/elements/cards.stories.tsx +59 -0
  137. package/src/elements/cards.tsx +97 -0
  138. package/src/elements/catalog.stories.tsx +491 -0
  139. package/src/elements/chain-of-thought.declarative.test.tsx +153 -0
  140. package/src/elements/chain-of-thought.stories.tsx +113 -0
  141. package/src/elements/chain-of-thought.tsx +92 -0
  142. package/src/elements/chat-scope-picker.stories.tsx +73 -0
  143. package/src/elements/chat-scope-picker.tsx +36 -0
  144. package/src/elements/chat-types.ts +42 -0
  145. package/src/elements/chat-workspace.stories.tsx +288 -0
  146. package/src/elements/chat-workspace.tsx +161 -0
  147. package/src/elements/chat.stories.tsx +151 -0
  148. package/src/elements/chat.tsx +80 -0
  149. package/src/elements/checkpoint.stories.tsx +72 -0
  150. package/src/elements/checkpoint.tsx +47 -0
  151. package/src/elements/choice.stories.tsx +240 -0
  152. package/src/elements/choice.tsx +48 -0
  153. package/src/elements/code-block.stories.tsx +83 -0
  154. package/src/elements/code-block.tsx +42 -0
  155. package/src/elements/compiled.css +2 -0
  156. package/src/elements/composed-shell.stories.tsx +316 -0
  157. package/src/elements/confirm-card.stories.tsx +227 -0
  158. package/src/elements/confirm-card.tsx +51 -0
  159. package/src/elements/context-meter.stories.tsx +151 -0
  160. package/src/elements/context-meter.tsx +121 -0
  161. package/src/elements/conversation-list.declarative.test.tsx +137 -0
  162. package/src/elements/conversation-list.stories.tsx +243 -0
  163. package/src/elements/conversation-list.tsx +80 -0
  164. package/src/elements/css.ts +5 -0
  165. package/src/elements/default-input.tsx +247 -0
  166. package/src/elements/define.tsx +171 -0
  167. package/src/elements/element-meta.json +3314 -0
  168. package/src/elements/element-types.d.ts +710 -0
  169. package/src/elements/embed.stories.tsx +213 -0
  170. package/src/elements/embed.tsx +35 -0
  171. package/src/elements/empty.stories.tsx +111 -0
  172. package/src/elements/empty.tsx +29 -0
  173. package/src/elements/feedback-bar.stories.tsx +114 -0
  174. package/src/elements/feedback-bar.tsx +61 -0
  175. package/src/elements/file-tree.stories.tsx +134 -0
  176. package/src/elements/file-tree.tsx +52 -0
  177. package/src/elements/file-upload.stories.tsx +82 -0
  178. package/src/elements/file-upload.tsx +44 -0
  179. package/src/elements/form.stories.tsx +248 -0
  180. package/src/elements/form.tsx +43 -0
  181. package/src/elements/framework-usage.json +563 -0
  182. package/src/elements/image.stories.tsx +71 -0
  183. package/src/elements/image.tsx +32 -0
  184. package/src/elements/link-preview.stories.tsx +209 -0
  185. package/src/elements/link-preview.tsx +34 -0
  186. package/src/elements/loader.stories.tsx +88 -0
  187. package/src/elements/loader.tsx +25 -0
  188. package/src/elements/markdown.stories.tsx +76 -0
  189. package/src/elements/markdown.tsx +38 -0
  190. package/src/elements/message-skills.declarative.test.tsx +104 -0
  191. package/src/elements/message-skills.stories.tsx +114 -0
  192. package/src/elements/message-skills.tsx +85 -0
  193. package/src/elements/message.stories.tsx +218 -0
  194. package/src/elements/message.tsx +162 -0
  195. package/src/elements/model-switcher.declarative.test.tsx +130 -0
  196. package/src/elements/model-switcher.stories.tsx +198 -0
  197. package/src/elements/model-switcher.tsx +93 -0
  198. package/src/elements/popover.stories.tsx +80 -0
  199. package/src/elements/popover.tsx +59 -0
  200. package/src/elements/prompt-input-slash-command.test.tsx +128 -0
  201. package/src/elements/prompt-input-stoppable.test.tsx +88 -0
  202. package/src/elements/prompt-input-toolbar-actions.test.tsx +120 -0
  203. package/src/elements/prompt-input.stories.tsx +331 -0
  204. package/src/elements/prompt-input.tsx +185 -0
  205. package/src/elements/prompt-suggestions.declarative.test.tsx +164 -0
  206. package/src/elements/prompt-suggestions.stories.tsx +213 -0
  207. package/src/elements/prompt-suggestions.tsx +94 -0
  208. package/src/elements/reasoning.stories.tsx +77 -0
  209. package/src/elements/reasoning.tsx +50 -0
  210. package/src/elements/register.ts +56 -0
  211. package/src/elements/remote.stories.tsx +314 -0
  212. package/src/elements/remote.tsx +221 -0
  213. package/src/elements/resizable.d.ts +27 -0
  214. package/src/elements/resizable.stories.tsx +426 -0
  215. package/src/elements/resizable.tsx +553 -0
  216. package/src/elements/response-stream.stories.tsx +79 -0
  217. package/src/elements/response-stream.tsx +40 -0
  218. package/src/elements/scroll-button.stories.tsx +202 -0
  219. package/src/elements/scroll-button.test.tsx +148 -0
  220. package/src/elements/scroll-button.tsx +126 -0
  221. package/src/elements/source-list.stories.tsx +143 -0
  222. package/src/elements/source.stories.tsx +88 -0
  223. package/src/elements/source.tsx +120 -0
  224. package/src/elements/styles.css +199 -0
  225. package/src/elements/switch.stories.tsx +56 -0
  226. package/src/elements/switch.tsx +43 -0
  227. package/src/elements/tasks.stories.tsx +237 -0
  228. package/src/elements/tasks.tsx +46 -0
  229. package/src/elements/text-shimmer.stories.tsx +64 -0
  230. package/src/elements/text-shimmer.tsx +28 -0
  231. package/src/elements/thinking-bar.stories.tsx +72 -0
  232. package/src/elements/thinking-bar.tsx +34 -0
  233. package/src/elements/tool.stories.tsx +89 -0
  234. package/src/elements/tool.tsx +23 -0
  235. package/src/elements/voice-input.stories.tsx +88 -0
  236. package/src/elements/voice-input.tsx +41 -0
  237. package/src/index.ts +253 -0
  238. package/src/primitives/card-contract.ts +71 -0
  239. package/src/primitives/card-host.tsx +35 -0
  240. package/src/primitives/card-registry.tsx +67 -0
  241. package/src/primitives/card-resolution.ts +36 -0
  242. package/src/primitives/card-routing.ts +79 -0
  243. package/src/primitives/card-schemas/card-envelope.schema.json +14 -0
  244. package/src/primitives/card-schemas/card-event.schema.json +12 -0
  245. package/src/primitives/card-schemas/choice.schema.json +70 -0
  246. package/src/primitives/card-schemas/confirm.schema.json +65 -0
  247. package/src/primitives/card-schemas/embed.schema.json +65 -0
  248. package/src/primitives/card-schemas/form.result.schema.json +7 -0
  249. package/src/primitives/card-schemas/form.schema.json +33 -0
  250. package/src/primitives/card-schemas/link.schema.json +56 -0
  251. package/src/primitives/card-schemas/tasks.result.schema.json +16 -0
  252. package/src/primitives/card-schemas/tasks.schema.json +78 -0
  253. package/src/primitives/card-validate.ts +95 -0
  254. package/src/primitives/chat-config.tsx +76 -0
  255. package/src/primitives/controllable.test.ts +47 -0
  256. package/src/primitives/controllable.ts +21 -0
  257. package/src/primitives/embed-providers.ts +254 -0
  258. package/src/primitives/highlighter.ts +157 -0
  259. package/src/primitives/link-preview.ts +87 -0
  260. package/src/primitives/pdf-preview.ts +121 -0
  261. package/src/primitives/use-auto-resize.ts +31 -0
  262. package/src/primitives/use-resize-observer.ts +12 -0
  263. package/src/primitives/use-stick-to-bottom.ts +43 -0
  264. package/src/primitives/use-text-stream.ts +112 -0
  265. package/src/primitives/use-voice-recorder.ts +50 -0
  266. package/src/remote/host-embed.ts +345 -0
  267. package/src/remote/index.ts +2 -0
  268. package/src/remote/origin.ts +30 -0
  269. package/src/remote/provider-runtime.ts +259 -0
  270. package/src/remote/provider.ts +2 -0
  271. package/src/remote/validate.ts +22 -0
  272. package/src/remote/version.ts +12 -0
  273. package/src/remote/wire.ts +48 -0
  274. package/src/stories/chat-panel-layout.stories.tsx +145 -0
  275. package/src/stories/chat-scene.tsx +571 -0
  276. package/src/stories/checkpoint-restore.stories.tsx +256 -0
  277. package/src/stories/context-usage.stories.tsx +212 -0
  278. package/src/stories/conversation-with-reasoning.stories.tsx +182 -0
  279. package/src/stories/conversation-with-sources.stories.tsx +471 -0
  280. package/src/stories/docs/Accessibility.mdx +119 -0
  281. package/src/stories/docs/ChildElements.mdx +269 -0
  282. package/src/stories/docs/ForAIAgents.mdx +162 -0
  283. package/src/stories/docs/GettingStarted.mdx +65 -0
  284. package/src/stories/docs/Installation.mdx +83 -0
  285. package/src/stories/docs/Introduction.mdx +50 -0
  286. package/src/stories/docs/SolidJsAdvanced.mdx +17 -0
  287. package/src/stories/docs/Theming.mdx +85 -0
  288. package/src/stories/docs/element-controls.ts +121 -0
  289. package/src/stories/docs/frameworks/Angular.mdx +245 -0
  290. package/src/stories/docs/frameworks/Html.mdx +227 -0
  291. package/src/stories/docs/frameworks/Overview.mdx +47 -0
  292. package/src/stories/docs/frameworks/React.mdx +227 -0
  293. package/src/stories/docs/frameworks/Solid.mdx +173 -0
  294. package/src/stories/docs/frameworks/Svelte.mdx +194 -0
  295. package/src/stories/docs/frameworks/Vue.mdx +238 -0
  296. package/src/stories/docs/generative-ui-overview.mdx +186 -0
  297. package/src/stories/docs/recipes/SpeechToText.mdx +7 -0
  298. package/src/stories/docs/recipes/Streaming.mdx +68 -0
  299. package/src/stories/docs/recipes/TextToSpeech.mdx +35 -0
  300. package/src/stories/docs/theme-editor/canvas.tsx +32 -0
  301. package/src/stories/docs/theme-editor/inspector.tsx +66 -0
  302. package/src/stories/docs/theme-editor/presets.test.ts +32 -0
  303. package/src/stories/docs/theme-editor/presets.ts +64 -0
  304. package/src/stories/docs/theme-editor/theme-css.test.ts +19 -0
  305. package/src/stories/docs/theme-editor/theme-css.ts +15 -0
  306. package/src/stories/docs/theme-editor/theme-editor.tsx +146 -0
  307. package/src/stories/docs/theme-tokens.tsx +174 -0
  308. package/src/stories/examples/ChoosingComponents.mdx +106 -0
  309. package/src/stories/examples/sample-data.ts +79 -0
  310. package/src/stories/examples/usage/checkpoint-restore.ts +156 -0
  311. package/src/stories/examples/usage/context-usage.ts +591 -0
  312. package/src/stories/examples/usage/conversation-with-reasoning.ts +224 -0
  313. package/src/stories/examples/usage/conversation-with-sources.ts +589 -0
  314. package/src/stories/examples/usage/empty-state.ts +144 -0
  315. package/src/stories/examples/usage/full-chat-app.ts +277 -0
  316. package/src/stories/examples/usage/index.ts +63 -0
  317. package/src/stories/examples/usage/message-actions.ts +704 -0
  318. package/src/stories/examples/usage/pattern-centered-conversation.ts +446 -0
  319. package/src/stories/examples/usage/pattern-chat-panel-layout.ts +442 -0
  320. package/src/stories/examples/usage/pattern-docked-widget.ts +479 -0
  321. package/src/stories/examples/usage/prompt-input-variants.ts +1356 -0
  322. package/src/stories/examples/usage/streaming-response.ts +687 -0
  323. package/src/stories/examples/usage/types.ts +33 -0
  324. package/src/stories/full-chat.stories.tsx +18 -0
  325. package/src/stories/message-actions.stories.tsx +230 -0
  326. package/src/stories/pattern-centered-conversation.stories.tsx +93 -0
  327. package/src/stories/pattern-docked-widget.stories.tsx +93 -0
  328. package/src/stories/pattern-empty-state.stories.tsx +76 -0
  329. package/src/stories/prompt-input-variants.stories.tsx +456 -0
  330. package/src/stories/streaming-response.stories.tsx +438 -0
  331. package/src/stories/theme-editor.stories.tsx +16 -0
  332. package/src/stories/token-reference.stories.tsx +18 -0
  333. package/src/stories/typography.stories.tsx +78 -0
  334. package/src/types.ts +47 -0
  335. package/src/ui/action-icons.ts +52 -0
  336. package/src/ui/avatar.stories.tsx +103 -0
  337. package/src/ui/avatar.tsx +23 -0
  338. package/src/ui/badge.stories.tsx +86 -0
  339. package/src/ui/badge.tsx +21 -0
  340. package/src/ui/button.stories.tsx +145 -0
  341. package/src/ui/button.tsx +38 -0
  342. package/src/ui/collapsible.stories.tsx +69 -0
  343. package/src/ui/collapsible.tsx +125 -0
  344. package/src/ui/dropdown.stories.tsx +59 -0
  345. package/src/ui/dropdown.tsx +196 -0
  346. package/src/ui/hover-card.stories.tsx +77 -0
  347. package/src/ui/hover-card.test.tsx +30 -0
  348. package/src/ui/hover-card.tsx +172 -0
  349. package/src/ui/overlay.stories.tsx +115 -0
  350. package/src/ui/overlay.tsx +158 -0
  351. package/src/ui/popover.stories.tsx +81 -0
  352. package/src/ui/popover.test.tsx +99 -0
  353. package/src/ui/popover.tsx +92 -0
  354. package/src/ui/resizable.stories.tsx +236 -0
  355. package/src/ui/resizable.tsx +576 -0
  356. package/src/ui/scroll-area.stories.tsx +50 -0
  357. package/src/ui/scroll-area.tsx +15 -0
  358. package/src/ui/separator.stories.tsx +81 -0
  359. package/src/ui/separator.tsx +10 -0
  360. package/src/ui/skeleton.stories.tsx +337 -0
  361. package/src/ui/skeleton.tsx +16 -0
  362. package/src/ui/switch.stories.tsx +51 -0
  363. package/src/ui/switch.test.tsx +59 -0
  364. package/src/ui/switch.tsx +62 -0
  365. package/src/ui/textarea.stories.tsx +76 -0
  366. package/src/ui/textarea.tsx +21 -0
  367. package/src/ui/tooltip.stories.tsx +74 -0
  368. package/src/ui/tooltip.tsx +68 -0
  369. package/src/utils/cn.ts +24 -0
  370. package/theme.css +187 -0
@@ -0,0 +1,1356 @@
1
+ import type { ExampleUsage, StoryUsage } from './types';
2
+
3
+ /** Basic Input — a text box with a send button that enables once you type. */
4
+ const basic: StoryUsage = {
5
+ intro:
6
+ 'A complete prompt box from one element. `<kai-prompt-input>` renders the textarea and send button; bind `value`, handle `valuechange` on every keystroke, and `submit` (Enter or the send button) gives you `{ value, attachments }`. (The live demo composes the SolidJS `PromptInput` primitives.)',
7
+ snippets: {
8
+ html: `<!-- Register the elements once (CDN or bundler) -->
9
+ <script type="module">
10
+ import 'https://cdn.jsdelivr.net/npm/@kitn.ai/ui/dist/kitn-chat.es.js';
11
+ </script>
12
+
13
+ <kai-prompt-input id="prompt" placeholder="Ask anything..."></kai-prompt-input>
14
+
15
+ <script type="module">
16
+ const prompt = document.getElementById('prompt');
17
+ let value = '';
18
+ prompt.addEventListener('kai-value-change', (e) => {
19
+ value = e.detail.value; // track on every keystroke
20
+ });
21
+ prompt.addEventListener('kai-submit', (e) => {
22
+ const { value, attachments } = e.detail;
23
+ console.log(value, attachments);
24
+ });
25
+ </script>`,
26
+
27
+ react: `import { useState } from 'react';
28
+ import { PromptInput } from '@kitn.ai/ui/react';
29
+
30
+ export function Prompt() {
31
+ const [value, setValue] = useState('');
32
+
33
+ return (
34
+ <PromptInput
35
+ placeholder="Ask anything..."
36
+ onValueChange={(e) => setValue(e.detail.value)}
37
+ onSubmit={(e) => {
38
+ const { value, attachments } = e.detail;
39
+ console.log(value, attachments);
40
+ }}
41
+ />
42
+ );
43
+ }`,
44
+
45
+ vue: `<script setup>
46
+ import '@kitn.ai/ui/elements'; // register once (e.g. in main.ts)
47
+ import { ref } from 'vue';
48
+
49
+ const value = ref('');
50
+ function onValueChange(e) {
51
+ value.value = e.detail.value; // track on every keystroke
52
+ }
53
+ function onSubmit(e) {
54
+ const { value, attachments } = e.detail;
55
+ console.log(value, attachments);
56
+ }
57
+ </script>
58
+
59
+ <template>
60
+ <kai-prompt-input
61
+ placeholder="Ask anything..."
62
+ @kai-value-change="onValueChange"
63
+ @kai-submit="onSubmit"
64
+ />
65
+ </template>`,
66
+
67
+ svelte: `<script>
68
+ import '@kitn.ai/ui/elements'; // register once
69
+
70
+ let value = '';
71
+ function onValueChange(e) {
72
+ value = e.detail.value; // track on every keystroke
73
+ }
74
+ function onSubmit(e) {
75
+ const { value, attachments } = e.detail;
76
+ console.log(value, attachments);
77
+ }
78
+ </script>
79
+
80
+ <kai-prompt-input
81
+ placeholder="Ask anything..."
82
+ on:kai-value-change={onValueChange}
83
+ on:kai-submit={onSubmit}
84
+ />`,
85
+
86
+ angular: `// main.ts: import '@kitn.ai/ui/elements' before bootstrapApplication,
87
+ // and add CUSTOM_ELEMENTS_SCHEMA to the component/module.
88
+ import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
89
+
90
+ @Component({
91
+ selector: 'app-prompt',
92
+ standalone: true,
93
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
94
+ template: \`
95
+ <kai-prompt-input
96
+ placeholder="Ask anything..."
97
+ (kai-value-change)="onValueChange($event)"
98
+ (kai-submit)="onSubmit($event)"
99
+ ></kai-prompt-input>
100
+ \`,
101
+ })
102
+ export class PromptComponent {
103
+ value = '';
104
+ onValueChange(e: CustomEvent<{ value: string }>) {
105
+ this.value = e.detail.value; // track on every keystroke
106
+ }
107
+ onSubmit(e: CustomEvent<{ value: string; attachments: unknown[] }>) {
108
+ const { value, attachments } = e.detail;
109
+ console.log(value, attachments);
110
+ }
111
+ }`,
112
+
113
+ solid: `import { createSignal } from 'solid-js';
114
+ import { PromptInput, PromptInputTextarea, PromptInputActions, Button } from '@kitn.ai/ui';
115
+ import { ArrowUp } from 'lucide-solid';
116
+
117
+ export function Prompt() {
118
+ const [value, setValue] = createSignal('');
119
+ return (
120
+ <PromptInput value={value()} onValueChange={setValue} onSubmit={() => setValue('')}>
121
+ <PromptInputTextarea placeholder="Ask anything..." />
122
+ <PromptInputActions class="justify-end">
123
+ <Button variant="default" size="icon-sm" class="rounded-full" disabled={!value()} aria-label="Send message">
124
+ <ArrowUp class="size-4" />
125
+ </Button>
126
+ </PromptInputActions>
127
+ </PromptInput>
128
+ );
129
+ }`,
130
+ },
131
+ };
132
+
133
+ /** With Suggestion Chips — starter prompts above the input. */
134
+ const suggestions: StoryUsage = {
135
+ intro:
136
+ 'Show starter prompts above the input. Pass a `suggestions` array (as a PROPERTY) and pick `suggestionMode` — `"submit"` (default) sends the prompt immediately, `"fill"` just drops it into the box and fires `kai-suggestion-click`. (The demo groups its chips with the SolidJS `PromptSuggestion` primitive, which the element renders as one flat row.)',
137
+ snippets: {
138
+ html: `<script type="module">
139
+ import 'https://cdn.jsdelivr.net/npm/@kitn.ai/ui/dist/kitn-chat.es.js';
140
+ </script>
141
+
142
+ <kai-prompt-input
143
+ id="prompt"
144
+ placeholder="Ask about this document..."
145
+ suggestion-mode="fill"
146
+ ></kai-prompt-input>
147
+
148
+ <script type="module">
149
+ const prompt = document.getElementById('prompt');
150
+ // Arrays must be set as a PROPERTY (attributes only take strings).
151
+ prompt.suggestions = [
152
+ 'Summarize this document',
153
+ 'What are the key takeaways?',
154
+ 'Create an outline',
155
+ ];
156
+ prompt.addEventListener('kai-suggestion-click', (e) => {
157
+ console.log(e.detail.value); // fires when suggestion-mode="fill"
158
+ });
159
+ prompt.addEventListener('kai-submit', (e) => console.log(e.detail.value));
160
+ </script>`,
161
+
162
+ react: `import { PromptInput } from '@kitn.ai/ui/react';
163
+
164
+ const SUGGESTIONS = [
165
+ 'Summarize this document',
166
+ 'What are the key takeaways?',
167
+ 'Create an outline',
168
+ ];
169
+
170
+ export function Prompt() {
171
+ return (
172
+ <PromptInput
173
+ placeholder="Ask about this document..."
174
+ // Arrays are passed straight through as a property.
175
+ suggestions={SUGGESTIONS}
176
+ suggestionMode="fill"
177
+ onSuggestionClick={(e) => console.log(e.detail.value)}
178
+ onSubmit={(e) => console.log(e.detail.value)}
179
+ />
180
+ );
181
+ }`,
182
+
183
+ vue: `<script setup>
184
+ import '@kitn.ai/ui/elements';
185
+
186
+ const suggestions = [
187
+ 'Summarize this document',
188
+ 'What are the key takeaways?',
189
+ 'Create an outline',
190
+ ];
191
+ function onSuggestionClick(e) { console.log(e.detail.value); }
192
+ function onSubmit(e) { console.log(e.detail.value); }
193
+ </script>
194
+
195
+ <template>
196
+ <!-- .prop binds the array as a property -->
197
+ <kai-prompt-input
198
+ placeholder="Ask about this document..."
199
+ :suggestions.prop="suggestions"
200
+ suggestion-mode="fill"
201
+ @kai-suggestion-click="onSuggestionClick"
202
+ @kai-submit="onSubmit"
203
+ />
204
+ </template>`,
205
+
206
+ svelte: `<script>
207
+ import '@kitn.ai/ui/elements';
208
+
209
+ let el;
210
+ const suggestions = [
211
+ 'Summarize this document',
212
+ 'What are the key takeaways?',
213
+ 'Create an outline',
214
+ ];
215
+ // Arrays must be set as a property (attributes only take strings).
216
+ $: if (el) el.suggestions = suggestions;
217
+
218
+ function onSuggestionClick(e) { console.log(e.detail.value); }
219
+ function onSubmit(e) { console.log(e.detail.value); }
220
+ </script>
221
+
222
+ <kai-prompt-input
223
+ bind:this={el}
224
+ placeholder="Ask about this document..."
225
+ suggestion-mode="fill"
226
+ on:kai-suggestion-click={onSuggestionClick}
227
+ on:kai-submit={onSubmit}
228
+ />`,
229
+
230
+ angular: `import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
231
+
232
+ @Component({
233
+ selector: 'app-prompt',
234
+ standalone: true,
235
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
236
+ template: \`
237
+ <kai-prompt-input
238
+ placeholder="Ask about this document..."
239
+ [suggestions]="suggestions"
240
+ suggestion-mode="fill"
241
+ (kai-suggestion-click)="onSuggestionClick($event)"
242
+ (kai-submit)="onSubmit($event)"
243
+ ></kai-prompt-input>
244
+ \`,
245
+ })
246
+ export class PromptComponent {
247
+ // [suggestions] binds the array as a property.
248
+ suggestions = [
249
+ 'Summarize this document',
250
+ 'What are the key takeaways?',
251
+ 'Create an outline',
252
+ ];
253
+ onSuggestionClick(e: CustomEvent<{ value: string }>) { console.log(e.detail.value); }
254
+ onSubmit(e: CustomEvent<{ value: string }>) { console.log(e.detail.value); }
255
+ }`,
256
+
257
+ solid: `import { createSignal, For } from 'solid-js';
258
+ import { PromptInput, PromptInputTextarea, PromptInputActions, PromptSuggestion, Button } from '@kitn.ai/ui';
259
+ import { ArrowUp } from 'lucide-solid';
260
+
261
+ const GROUPS = [
262
+ { label: 'Get started', items: ['Summarize this document', 'What are the key takeaways?', 'Create an outline'] },
263
+ { label: 'Go deeper', items: ['Compare with similar approaches', 'What are the tradeoffs?', 'Find contradictions'] },
264
+ ];
265
+
266
+ export function Prompt() {
267
+ const [value, setValue] = createSignal('');
268
+ return (
269
+ <div class="space-y-4">
270
+ <For each={GROUPS}>
271
+ {(group) => (
272
+ <div class="space-y-2">
273
+ <span class="text-xs font-medium text-muted-foreground uppercase tracking-wider">{group.label}</span>
274
+ <div class="flex flex-wrap gap-2">
275
+ <For each={group.items}>
276
+ {(item) => <PromptSuggestion onClick={() => setValue(item)}>{item}</PromptSuggestion>}
277
+ </For>
278
+ </div>
279
+ </div>
280
+ )}
281
+ </For>
282
+ <PromptInput value={value()} onValueChange={setValue} onSubmit={() => setValue('')}>
283
+ <PromptInputTextarea placeholder="Ask about this document..." />
284
+ <PromptInputActions class="justify-end">
285
+ <Button variant="default" size="icon-sm" class="rounded-full" disabled={!value()} aria-label="Send message">
286
+ <ArrowUp class="size-4" />
287
+ </Button>
288
+ </PromptInputActions>
289
+ </PromptInput>
290
+ </div>
291
+ );
292
+ }`,
293
+ },
294
+ };
295
+
296
+ /** With Action Buttons — toolbar buttons beside the input. */
297
+ const actionButtons: StoryUsage = {
298
+ intro:
299
+ 'Add toolbar buttons beside the input. `<kai-prompt-input>` has built-in Search and Voice buttons — enable `search` and `voice`, then handle the `search` / `voice` events; attaching files is built in (the paperclip, emitted on `submit` as `attachments`). For extra custom buttons, place `<kai-action id icon tooltip>` children inside `<kai-prompt-input>` — the element reads them as invisible data carriers and renders a ghost icon button per entry in the left toolbar; clicking fires a `kai-toolbar-action` CustomEvent with `detail.action` = the action id. This is the same `<kai-action>` descriptor element that `<kai-message>` uses (composition symmetry). The Solid tab shows a custom Sparkles button composed directly with the `PromptInput` primitives (the full-control equivalent).',
300
+ snippets: {
301
+ html: `<script type="module">
302
+ import 'https://cdn.jsdelivr.net/npm/@kitn.ai/ui/dist/kitn-chat.es.js';
303
+ </script>
304
+
305
+ <!-- Built-in buttons: search (Globe) and voice (Mic). -->
306
+ <!-- Custom toolbar buttons: compose <kai-action> children. -->
307
+ <kai-prompt-input id="prompt" placeholder="Message..." search voice>
308
+ <kai-action id="attach" icon="paperclip" tooltip="Attach"></kai-action>
309
+ <kai-action id="sparkles" icon="star" tooltip="AI suggestions"></kai-action>
310
+ </kai-prompt-input>
311
+
312
+ <script type="module">
313
+ const prompt = document.getElementById('prompt');
314
+ prompt.addEventListener('kai-search', () => console.log('search clicked'));
315
+ prompt.addEventListener('kai-voice', () => console.log('voice clicked'));
316
+ // kai-toolbar-action fires when any <kai-action> toolbar button is clicked.
317
+ prompt.addEventListener('kai-toolbar-action', (e) => console.log('toolbar action:', e.detail.action));
318
+ prompt.addEventListener('kai-submit', (e) => {
319
+ const { value, attachments } = e.detail; // attachments from the paperclip
320
+ console.log(value, attachments);
321
+ });
322
+ </script>`,
323
+
324
+ react: `import { PromptInput } from '@kitn.ai/ui/react';
325
+
326
+ export function Prompt() {
327
+ return (
328
+ <PromptInput
329
+ placeholder="Message..."
330
+ search
331
+ voice
332
+ onSearch={() => console.log('search clicked')}
333
+ onVoice={() => console.log('voice clicked')}
334
+ onSubmit={(e) => {
335
+ const { value, attachments } = e.detail; // attachments from the paperclip
336
+ console.log(value, attachments);
337
+ }}
338
+ />
339
+ );
340
+ }`,
341
+
342
+ vue: `<script setup>
343
+ import '@kitn.ai/ui/elements';
344
+ function onSearch() { console.log('search clicked'); }
345
+ function onVoice() { console.log('voice clicked'); }
346
+ function onSubmit(e) {
347
+ const { value, attachments } = e.detail; // attachments from the paperclip
348
+ console.log(value, attachments);
349
+ }
350
+ </script>
351
+
352
+ <template>
353
+ <kai-prompt-input
354
+ placeholder="Message..."
355
+ search
356
+ voice
357
+ @kai-search="onSearch"
358
+ @kai-voice="onVoice"
359
+ @kai-submit="onSubmit"
360
+ />
361
+ </template>`,
362
+
363
+ svelte: `<script>
364
+ import '@kitn.ai/ui/elements';
365
+ function onSearch() { console.log('search clicked'); }
366
+ function onVoice() { console.log('voice clicked'); }
367
+ function onSubmit(e) {
368
+ const { value, attachments } = e.detail; // attachments from the paperclip
369
+ console.log(value, attachments);
370
+ }
371
+ </script>
372
+
373
+ <kai-prompt-input
374
+ placeholder="Message..."
375
+ search
376
+ voice
377
+ on:kai-search={onSearch}
378
+ on:kai-voice={onVoice}
379
+ on:kai-submit={onSubmit}
380
+ />`,
381
+
382
+ angular: `import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
383
+
384
+ @Component({
385
+ selector: 'app-prompt',
386
+ standalone: true,
387
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
388
+ template: \`
389
+ <kai-prompt-input
390
+ placeholder="Message..."
391
+ [search]="true"
392
+ [voice]="true"
393
+ (kai-search)="onSearch()"
394
+ (kai-voice)="onVoice()"
395
+ (kai-submit)="onSubmit($event)"
396
+ ></kai-prompt-input>
397
+ \`,
398
+ })
399
+ export class PromptComponent {
400
+ onSearch() { console.log('search clicked'); }
401
+ onVoice() { console.log('voice clicked'); }
402
+ onSubmit(e: CustomEvent<{ value: string; attachments: unknown[] }>) {
403
+ const { value, attachments } = e.detail; // attachments from the paperclip
404
+ console.log(value, attachments);
405
+ }
406
+ }`,
407
+
408
+ solid: `import { createSignal } from 'solid-js';
409
+ import { PromptInput, PromptInputTextarea, PromptInputActions, Button } from '@kitn.ai/ui';
410
+ import { ArrowUp, Paperclip, Globe, Mic, Sparkles } from 'lucide-solid';
411
+
412
+ export function Prompt() {
413
+ const [value, setValue] = createSignal('');
414
+ return (
415
+ <PromptInput value={value()} onValueChange={setValue} onSubmit={() => setValue('')}>
416
+ <PromptInputTextarea placeholder="Message..." />
417
+ <PromptInputActions class="justify-between">
418
+ <div class="flex items-center gap-1">
419
+ <Button variant="ghost" size="icon-sm" aria-label="Attach file"><Paperclip class="size-4 text-muted-foreground" /></Button>
420
+ <Button variant="ghost" size="icon-sm" aria-label="Search the web"><Globe class="size-4 text-muted-foreground" /></Button>
421
+ <Button variant="ghost" size="icon-sm" aria-label="Voice input"><Mic class="size-4 text-muted-foreground" /></Button>
422
+ {/* Sparkles: a custom button composed directly. For the element, use
423
+ <kai-action id="sparkles" icon="star" tooltip="AI suggestions"> instead. */}
424
+ <Button variant="ghost" size="icon-sm" aria-label="AI suggestions"><Sparkles class="size-4 text-muted-foreground" /></Button>
425
+ </div>
426
+ <Button variant="default" size="icon-sm" class="rounded-full" disabled={!value()} aria-label="Send message">
427
+ <ArrowUp class="size-4" />
428
+ </Button>
429
+ </PromptInputActions>
430
+ </PromptInput>
431
+ );
432
+ }`,
433
+ },
434
+ };
435
+
436
+ /** Streaming / Loading State — disabled while a reply streams in. */
437
+ const streaming: StoryUsage = {
438
+ intro:
439
+ 'Block input while a reply streams. Set `loading` to show the streaming state and stop accepting submits, and `disabled` to make the box fully non-interactive. Add `stoppable` to get a built-in Stop button that fires `kai-stop` — listen for that event and call `controller.abort()` on your fetch/SSE. (The demo composes the SolidJS `PromptInput` + `Loader` primitives to show the typing/dots indicators and a stop button.)',
440
+ snippets: {
441
+ html: `<script type="module">
442
+ import 'https://cdn.jsdelivr.net/npm/@kitn.ai/ui/dist/kitn-chat.es.js';
443
+ </script>
444
+
445
+ <kai-prompt-input id="prompt" placeholder="Generating response..." loading disabled></kai-prompt-input>
446
+
447
+ <script type="module">
448
+ const prompt = document.getElementById('prompt');
449
+ // When the reply finishes, clear the flags to re-enable.
450
+ function onDone() {
451
+ prompt.loading = false;
452
+ prompt.disabled = false;
453
+ }
454
+ </script>`,
455
+
456
+ react: `import { PromptInput } from '@kitn.ai/ui/react';
457
+
458
+ export function Prompt({ isStreaming }: { isStreaming: boolean }) {
459
+ return (
460
+ <PromptInput
461
+ placeholder={isStreaming ? 'Generating response...' : 'Ask anything...'}
462
+ loading={isStreaming}
463
+ disabled={isStreaming}
464
+ />
465
+ );
466
+ }`,
467
+
468
+ vue: `<script setup>
469
+ import '@kitn.ai/ui/elements';
470
+ import { ref } from 'vue';
471
+ const isStreaming = ref(true);
472
+ </script>
473
+
474
+ <template>
475
+ <kai-prompt-input
476
+ placeholder="Generating response..."
477
+ :loading="isStreaming"
478
+ :disabled="isStreaming"
479
+ />
480
+ </template>`,
481
+
482
+ svelte: `<script>
483
+ import '@kitn.ai/ui/elements';
484
+ let isStreaming = true;
485
+ </script>
486
+
487
+ <kai-prompt-input
488
+ placeholder="Generating response..."
489
+ loading={isStreaming}
490
+ disabled={isStreaming}
491
+ />`,
492
+
493
+ angular: `import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
494
+
495
+ @Component({
496
+ selector: 'app-prompt',
497
+ standalone: true,
498
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
499
+ template: \`
500
+ <kai-prompt-input
501
+ placeholder="Generating response..."
502
+ [loading]="isStreaming"
503
+ [disabled]="isStreaming"
504
+ ></kai-prompt-input>
505
+ \`,
506
+ })
507
+ export class PromptComponent {
508
+ isStreaming = true;
509
+ }`,
510
+
511
+ solid: `import { PromptInput, PromptInputTextarea, PromptInputActions, Loader, Button } from '@kitn.ai/ui';
512
+ import { Square } from 'lucide-solid';
513
+
514
+ export function Prompt() {
515
+ return (
516
+ <PromptInput disabled isLoading>
517
+ <PromptInputTextarea placeholder="Generating response..." />
518
+ <PromptInputActions class="justify-between">
519
+ <div class="flex items-center gap-2">
520
+ <Loader variant="typing" size="sm" />
521
+ <span class="text-xs text-foreground">Generating...</span>
522
+ </div>
523
+ <Button variant="outline" size="icon-sm" class="rounded-full" aria-label="Stop">
524
+ <Square class="size-3" />
525
+ </Button>
526
+ </PromptInputActions>
527
+ </PromptInput>
528
+ );
529
+ }`,
530
+ },
531
+ };
532
+
533
+ /** With Model Selector — a model picker alongside the input.
534
+ *
535
+ * INSIGHT: `ModelSwitcher` only renders when `models.length > 1`. Passing a
536
+ * single-item array hides it entirely — so the render path for free/pro tiers
537
+ * (single model) is already handled without conditional code.
538
+ */
539
+ const modelSelector: StoryUsage = {
540
+ intro:
541
+ 'Put a model picker beside the input. `<kai-prompt-input>` doesn\'t expose a model-switcher prop — pair it with the standalone `<kai-model-switcher>` element (bind `models` and `currentModel`, handle `modelchange`) and lay them out side by side. (The demo composes the SolidJS `PromptInput` + `ModelSwitcher` primitives in the actions row.)',
542
+ snippets: {
543
+ html: `<script type="module">
544
+ import 'https://cdn.jsdelivr.net/npm/@kitn.ai/ui/dist/kitn-chat.es.js';
545
+ </script>
546
+
547
+ <div style="display:flex; flex-direction:column; gap:0.5rem">
548
+ <kai-model-switcher id="models"></kai-model-switcher>
549
+ <kai-prompt-input id="prompt" placeholder="Ask anything..."></kai-prompt-input>
550
+ </div>
551
+
552
+ <script type="module">
553
+ const models = document.getElementById('models');
554
+ // Arrays must be set as a PROPERTY.
555
+ models.models = [
556
+ { id: 'claude-4', name: 'Claude 4 Opus', provider: 'Anthropic' },
557
+ { id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' },
558
+ { id: 'gemini-2', name: 'Gemini 2.5 Pro', provider: 'Google' },
559
+ ];
560
+ models.currentModel = 'claude-4';
561
+ models.addEventListener('kai-model-change', (e) => console.log(e.detail));
562
+
563
+ const prompt = document.getElementById('prompt');
564
+ prompt.addEventListener('kai-submit', (e) => console.log(e.detail.value));
565
+ </script>`,
566
+
567
+ react: `import { useState } from 'react';
568
+ import { PromptInput, ModelSwitcher } from '@kitn.ai/ui/react';
569
+
570
+ const MODELS = [
571
+ { id: 'claude-4', name: 'Claude 4 Opus', provider: 'Anthropic' },
572
+ { id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' },
573
+ { id: 'gemini-2', name: 'Gemini 2.5 Pro', provider: 'Google' },
574
+ ];
575
+
576
+ export function Prompt() {
577
+ const [modelId, setModelId] = useState('claude-4');
578
+ return (
579
+ <div className="flex flex-col gap-2">
580
+ <ModelSwitcher models={MODELS} currentModel={modelId} onModelChange={(e) => setModelId(e.detail.modelId)} />
581
+ <PromptInput placeholder="Ask anything..." onSubmit={(e) => console.log(e.detail.value)} />
582
+ </div>
583
+ );
584
+ }`,
585
+
586
+ vue: `<script setup>
587
+ import '@kitn.ai/ui/elements';
588
+ import { ref } from 'vue';
589
+
590
+ const modelId = ref('claude-4');
591
+ const models = [
592
+ { id: 'claude-4', name: 'Claude 4 Opus', provider: 'Anthropic' },
593
+ { id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' },
594
+ { id: 'gemini-2', name: 'Gemini 2.5 Pro', provider: 'Google' },
595
+ ];
596
+ function onModelChange(e) { modelId.value = e.detail.modelId; }
597
+ </script>
598
+
599
+ <template>
600
+ <div style="display:flex; flex-direction:column; gap:0.5rem">
601
+ <kai-model-switcher :models.prop="models" :current-model="modelId" @kai-model-change="onModelChange" />
602
+ <kai-prompt-input placeholder="Ask anything..." @kai-submit="(e) => console.log(e.detail.value)" />
603
+ </div>
604
+ </template>`,
605
+
606
+ svelte: `<script>
607
+ import '@kitn.ai/ui/elements';
608
+
609
+ let el;
610
+ let modelId = 'claude-4';
611
+ const models = [
612
+ { id: 'claude-4', name: 'Claude 4 Opus', provider: 'Anthropic' },
613
+ { id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' },
614
+ { id: 'gemini-2', name: 'Gemini 2.5 Pro', provider: 'Google' },
615
+ ];
616
+ // Arrays must be set as a property.
617
+ $: if (el) el.models = models;
618
+ function onModelChange(e) { modelId = e.detail.modelId; }
619
+ </script>
620
+
621
+ <div style="display:flex; flex-direction:column; gap:0.5rem">
622
+ <kai-model-switcher bind:this={el} current-model={modelId} on:kai-model-change={onModelChange} />
623
+ <kai-prompt-input placeholder="Ask anything..." on:kai-submit={(e) => console.log(e.detail.value)} />
624
+ </div>`,
625
+
626
+ angular: `import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
627
+
628
+ @Component({
629
+ selector: 'app-prompt',
630
+ standalone: true,
631
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
632
+ template: \`
633
+ <div style="display:flex; flex-direction:column; gap:0.5rem">
634
+ <kai-model-switcher [models]="models" [currentModel]="modelId" (kai-model-change)="onModelChange($event)"></kai-model-switcher>
635
+ <kai-prompt-input placeholder="Ask anything..." (kai-submit)="onSubmit($event)"></kai-prompt-input>
636
+ </div>
637
+ \`,
638
+ })
639
+ export class PromptComponent {
640
+ modelId = 'claude-4';
641
+ // [models] binds the array as a property.
642
+ models = [
643
+ { id: 'claude-4', name: 'Claude 4 Opus', provider: 'Anthropic' },
644
+ { id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' },
645
+ { id: 'gemini-2', name: 'Gemini 2.5 Pro', provider: 'Google' },
646
+ ];
647
+ onModelChange(e: CustomEvent<{ modelId: string }>) { this.modelId = e.detail.modelId; }
648
+ onSubmit(e: CustomEvent<{ value: string }>) { console.log(e.detail.value); }
649
+ }`,
650
+
651
+ solid: `import { createSignal } from 'solid-js';
652
+ import { PromptInput, PromptInputTextarea, PromptInputActions, ModelSwitcher, Button } from '@kitn.ai/ui';
653
+ import type { ModelOption } from '@kitn.ai/ui';
654
+ import { ArrowUp, Paperclip } from 'lucide-solid';
655
+
656
+ const MODELS: ModelOption[] = [
657
+ { id: 'claude-4', name: 'Claude 4 Opus', provider: 'Anthropic' },
658
+ { id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' },
659
+ { id: 'gemini-2', name: 'Gemini 2.5 Pro', provider: 'Google' },
660
+ ];
661
+
662
+ export function Prompt() {
663
+ const [value, setValue] = createSignal('');
664
+ const [modelId, setModelId] = createSignal('claude-4');
665
+ return (
666
+ <PromptInput value={value()} onValueChange={setValue} onSubmit={() => setValue('')}>
667
+ <PromptInputTextarea placeholder="Ask anything..." />
668
+ <PromptInputActions class="justify-between">
669
+ <ModelSwitcher models={MODELS} currentModelId={modelId()} onModelChange={setModelId} />
670
+ <div class="flex items-center gap-1">
671
+ <Button variant="ghost" size="icon-sm" aria-label="Attach file"><Paperclip class="size-4 text-muted-foreground" /></Button>
672
+ <Button variant="default" size="icon-sm" class="rounded-full" disabled={!value()} aria-label="Send message">
673
+ <ArrowUp class="size-4" />
674
+ </Button>
675
+ </div>
676
+ </PromptInputActions>
677
+ </PromptInput>
678
+ );
679
+ }`,
680
+ },
681
+ };
682
+
683
+ /** With File Attachments — staged files rendered above the textarea. */
684
+ const withFileAttachments: StoryUsage = {
685
+ intro:
686
+ 'The `kai-prompt-input` element has a built-in paperclip: clicking it opens a file picker, previews appear above the textarea (removable chips), and `kai-submit` always carries `{ value, attachments: AttachmentData[] }` — even when the array is empty. To pre-populate staged files, set `prompt.attachments = [...]` as a JS **property** after mount; the element then manages its own attachment state from there. The Solid demo wires the `Attachments`/`Attachment`/`AttachmentPreview`/`AttachmentInfo`/`AttachmentRemove` primitives manually for full control — use the element if you want the paperclip UX for free.',
687
+ snippets: {
688
+ html: `<script type="module">
689
+ import 'https://cdn.jsdelivr.net/npm/@kitn.ai/ui/dist/kitn-chat.es.js';
690
+ </script>
691
+
692
+ <!-- The element renders the paperclip, file picker, and removable chips automatically. -->
693
+ <kai-prompt-input id="prompt" placeholder="Describe or ask about the attached files..."></kai-prompt-input>
694
+
695
+ <script type="module">
696
+ const prompt = document.getElementById('prompt');
697
+ // Pre-populate staged files (must be a JS property, not an attribute).
698
+ prompt.attachments = [
699
+ { id: 'a1', type: 'file', filename: 'architecture.pdf', mediaType: 'application/pdf' },
700
+ { id: 'a2', type: 'file', filename: 'screenshot.png', mediaType: 'image/png' },
701
+ ];
702
+ // kai-submit always includes the current staged attachments.
703
+ prompt.addEventListener('kai-submit', (e) => {
704
+ const { value, attachments } = e.detail;
705
+ // attachments is always AttachmentData[] — empty array when no files staged.
706
+ console.log(value, attachments);
707
+ });
708
+ </script>`,
709
+
710
+ react: `import { useRef } from 'react';
711
+ import { PromptInput } from '@kitn.ai/ui/react';
712
+
713
+ const SEED = [
714
+ { id: 'a1', type: 'file', filename: 'architecture.pdf', mediaType: 'application/pdf' },
715
+ { id: 'a2', type: 'file', filename: 'screenshot.png', mediaType: 'image/png' },
716
+ ];
717
+
718
+ export function Prompt() {
719
+ const ref = useRef(null);
720
+
721
+ // Set the attachments property after mount.
722
+ // useEffect(() => { if (ref.current) ref.current.attachments = SEED; }, []);
723
+
724
+ return (
725
+ <PromptInput
726
+ ref={ref}
727
+ placeholder="Describe or ask about the attached files..."
728
+ onSubmit={(e) => {
729
+ // attachments is always AttachmentData[] — may be empty
730
+ const { value, attachments } = e.detail;
731
+ console.log(value, attachments);
732
+ }}
733
+ />
734
+ );
735
+ }`,
736
+
737
+ vue: `<script setup>
738
+ import '@kitn.ai/ui/elements';
739
+ import { ref, onMounted } from 'vue';
740
+
741
+ const promptEl = ref(null);
742
+
743
+ // Set the attachments property after mount.
744
+ onMounted(() => {
745
+ if (promptEl.value) {
746
+ promptEl.value.attachments = [
747
+ { id: 'a1', type: 'file', filename: 'architecture.pdf', mediaType: 'application/pdf' },
748
+ ];
749
+ }
750
+ });
751
+
752
+ function onSubmit(e) {
753
+ const { value, attachments } = e.detail; // attachments always present
754
+ console.log(value, attachments);
755
+ }
756
+ </script>
757
+
758
+ <template>
759
+ <kai-prompt-input
760
+ ref="promptEl"
761
+ placeholder="Describe or ask about the attached files..."
762
+ @kai-submit="onSubmit"
763
+ />
764
+ </template>`,
765
+
766
+ svelte: `<script>
767
+ import '@kitn.ai/ui/elements';
768
+ import { onMount } from 'svelte';
769
+
770
+ let promptEl;
771
+ // Set the attachments property after mount.
772
+ onMount(() => {
773
+ promptEl.attachments = [
774
+ { id: 'a1', type: 'file', filename: 'architecture.pdf', mediaType: 'application/pdf' },
775
+ ];
776
+ });
777
+
778
+ function onSubmit(e) {
779
+ const { value, attachments } = e.detail; // attachments always present
780
+ console.log(value, attachments);
781
+ }
782
+ </script>
783
+
784
+ <kai-prompt-input
785
+ bind:this={promptEl}
786
+ placeholder="Describe or ask about the attached files..."
787
+ on:kai-submit={onSubmit}
788
+ />`,
789
+
790
+ angular: `import { Component, CUSTOM_ELEMENTS_SCHEMA, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
791
+
792
+ @Component({
793
+ selector: 'app-prompt',
794
+ standalone: true,
795
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
796
+ template: \`
797
+ <kai-prompt-input
798
+ #prompt
799
+ placeholder="Describe or ask about the attached files..."
800
+ (kai-submit)="onSubmit($event)"
801
+ ></kai-prompt-input>
802
+ \`,
803
+ })
804
+ export class PromptComponent implements AfterViewInit {
805
+ @ViewChild('prompt') promptRef!: ElementRef;
806
+
807
+ // Set attachments as a property after mount.
808
+ ngAfterViewInit() {
809
+ this.promptRef.nativeElement.attachments = [
810
+ { id: 'a1', type: 'file', filename: 'architecture.pdf', mediaType: 'application/pdf' },
811
+ ];
812
+ }
813
+
814
+ onSubmit(e: CustomEvent<{ value: string; attachments: unknown[] }>) {
815
+ const { value, attachments } = e.detail; // attachments always present
816
+ console.log(value, attachments);
817
+ }
818
+ }`,
819
+
820
+ solid: `import { createSignal, For, Show } from 'solid-js';
821
+ import {
822
+ PromptInput, PromptInputTextarea, PromptInputActions, Button,
823
+ Attachments, Attachment, AttachmentPreview, AttachmentInfo, AttachmentRemove,
824
+ } from '@kitn.ai/ui';
825
+ import type { AttachmentData } from '@kitn.ai/ui';
826
+ import { ArrowUp, Paperclip } from 'lucide-solid';
827
+
828
+ // The Solid PromptInput primitives don't wire the paperclip for you — that's
829
+ // done by DefaultPromptInput (used internally by the kai-prompt-input element).
830
+ // Compose the Attachments primitives manually when you need full control.
831
+ export function Prompt() {
832
+ const [value, setValue] = createSignal('');
833
+ const [attachments, setAttachments] = createSignal<AttachmentData[]>([
834
+ { id: 'a1', type: 'file', filename: 'architecture.pdf', mediaType: 'application/pdf' },
835
+ { id: 'a2', type: 'file', filename: 'screenshot.png', mediaType: 'image/png' },
836
+ ]);
837
+ let fileInput: HTMLInputElement | undefined;
838
+
839
+ const addFiles = (files: FileList | null) => {
840
+ if (!files?.length) return;
841
+ setAttachments((prev) => [
842
+ ...prev,
843
+ ...Array.from(files).map((f) => ({
844
+ id: crypto.randomUUID(),
845
+ type: 'file' as const,
846
+ filename: f.name,
847
+ mediaType: f.type || undefined,
848
+ url: f.type.startsWith('image/') ? URL.createObjectURL(f) : undefined,
849
+ })),
850
+ ]);
851
+ };
852
+
853
+ const removeAttachment = (id: string) =>
854
+ setAttachments((prev) => prev.filter((a) => a.id !== id));
855
+
856
+ const handleSubmit = () => {
857
+ // kai-submit emits { value, attachments } — mirror that shape here
858
+ console.log('submit', { value: value(), attachments: attachments() });
859
+ setValue('');
860
+ setAttachments([]);
861
+ };
862
+
863
+ return (
864
+ <>
865
+ <input
866
+ ref={fileInput}
867
+ type="file"
868
+ multiple
869
+ class="hidden"
870
+ onChange={(e) => { addFiles(e.currentTarget.files); e.currentTarget.value = ''; }}
871
+ />
872
+ <PromptInput value={value()} onValueChange={setValue} onSubmit={handleSubmit}>
873
+ <Show when={attachments().length > 0}>
874
+ <div class="px-3 pt-3">
875
+ <Attachments variant="inline">
876
+ <For each={attachments()}>
877
+ {(att) => (
878
+ <Attachment data={att} onRemove={() => removeAttachment(att.id)}>
879
+ <AttachmentPreview />
880
+ <AttachmentInfo />
881
+ <AttachmentRemove />
882
+ </Attachment>
883
+ )}
884
+ </For>
885
+ </Attachments>
886
+ </div>
887
+ </Show>
888
+ <PromptInputTextarea placeholder="Describe or ask about the attached files..." class="pt-3 pl-4" />
889
+ <PromptInputActions class="justify-between">
890
+ <Button variant="ghost" size="icon-sm" aria-label="Attach file" onClick={() => fileInput?.click()}>
891
+ <Paperclip class="size-4 text-muted-foreground" />
892
+ </Button>
893
+ {/* send enabled when there's text OR staged attachments */}
894
+ <Button
895
+ variant="default"
896
+ size="icon-sm"
897
+ class="rounded-full"
898
+ disabled={!value() && attachments().length === 0}
899
+ aria-label="Send message"
900
+ >
901
+ <ArrowUp class="size-4" />
902
+ </Button>
903
+ </PromptInputActions>
904
+ </PromptInput>
905
+ </>
906
+ );
907
+ }`,
908
+ },
909
+ };
910
+
911
+ /** Full Example — everything combined: model switcher, grouped suggestions,
912
+ * streaming state with a Stop button, and a send button that enables on input.
913
+ *
914
+ * GOTCHAS compiled from the source:
915
+ * - Submit payload: `kai-submit` always emits `{ value: string; attachments: AttachmentData[] }`.
916
+ * Even when no files are staged, `attachments` is an empty array — never `undefined`.
917
+ * - Enter vs Shift+Enter: `PromptInputTextarea` intercepts `Enter` (no Shift) and calls
918
+ * `onSubmit`; `Shift+Enter` inserts a newline. This is wired at the primitive level
919
+ * (`handleKeyDown` in prompt-input.tsx line 147), not configurable.
920
+ * - `loading` vs `disabled` on the element: `loading` alone blocks submit but keeps the
921
+ * box visually interactive; `disabled` makes it fully non-interactive (opacity + no focus).
922
+ * Use both together for the streaming state.
923
+ * - `isLoading` on the Solid primitive vs `loading` on the element: the Solid
924
+ * `PromptInput` prop is `isLoading`; the web component attribute is `loading` (kebab).
925
+ * - Stop button: add `stoppable` to `kai-prompt-input` and listen for `kai-stop` — the element
926
+ * fires it when the Stop button is clicked. Call `controller.abort()` in your handler to
927
+ * cancel the fetch/SSE. When composing Solid primitives, wire the Square button yourself
928
+ * (see FullExample). The element does the toggling for you; the consumer still owns the abort.
929
+ * - `ModelSwitcher` only renders when `models.length > 1` — a single-model list hides it.
930
+ * - `suggestions` is a JS property, not an attribute — arrays must be set on the element
931
+ * reference (not via an HTML attribute string). In the web-component tab note the
932
+ * `.prop` binding for Vue, `$:` for Svelte, `[prop]` for Angular.
933
+ * - `suggestionMode="submit"` (default) immediately dispatches `kai-submit` when a chip
934
+ * is clicked; `suggestionMode="fill"` drops the text into the box and fires
935
+ * `kai-suggestion-click` instead (so the user can edit before sending).
936
+ */
937
+ const fullExample: StoryUsage = {
938
+ intro:
939
+ 'Everything combined: model switcher, grouped suggestion chips, streaming state (with a Stop button), and a send button that enables once you type. Simulates the idle → streaming → idle loop you\'d wire to a real fetch/SSE call. Key gotchas: `kai-submit` always emits `{ value, attachments }` (attachments may be empty); Enter submits, Shift+Enter newlines; `loading` blocks submit while `disabled` kills focus too — use both while streaming; add `stoppable` to enable the built-in Stop button — it fires `kai-stop` when clicked; call `controller.abort()` in your handler to cancel the stream. When composing Solid primitives (the `PromptInput` + `PromptInputActions` pattern), wire the Square button yourself as shown in the Full Example story.',
940
+ snippets: {
941
+ html: `<script type="module">
942
+ import 'https://cdn.jsdelivr.net/npm/@kitn.ai/ui/dist/kitn-chat.es.js';
943
+ </script>
944
+
945
+ <div style="max-width:42rem; padding:1rem; display:flex; flex-direction:column; gap:1rem">
946
+ <kai-model-switcher id="models"></kai-model-switcher>
947
+ <kai-prompt-input id="prompt" placeholder="Ask anything..." suggestion-mode="fill"></kai-prompt-input>
948
+ <button id="stop" style="display:none">Stop</button>
949
+ </div>
950
+
951
+ <script type="module">
952
+ const models = document.getElementById('models');
953
+ const prompt = document.getElementById('prompt');
954
+ const stopBtn = document.getElementById('stop');
955
+ let controller;
956
+
957
+ // Arrays must be set as JS properties, not attributes.
958
+ models.models = [
959
+ { id: 'claude-4-opus', name: 'Claude 4 Opus', provider: 'Anthropic' },
960
+ { id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' },
961
+ ];
962
+ models.currentModel = 'claude-4-opus';
963
+ models.addEventListener('kai-model-change', (e) => { models.currentModel = e.detail.modelId; });
964
+
965
+ prompt.suggestions = ['Summarize this document', 'What are the key takeaways?'];
966
+ // suggestion-mode="fill" lets the user edit before submitting.
967
+ prompt.addEventListener('kai-suggestion-click', (e) => {
968
+ // User clicked a chip: value is now in the box; wait for them to hit Enter.
969
+ });
970
+
971
+ prompt.addEventListener('kai-submit', async (e) => {
972
+ const { value, attachments } = e.detail; // attachments always present (may be [])
973
+ prompt.loading = true;
974
+ prompt.disabled = true;
975
+ stopBtn.style.display = 'inline';
976
+
977
+ controller = new AbortController();
978
+ try {
979
+ // Replace with your real streaming fetch:
980
+ await fetch('/api/chat', { method: 'POST', body: JSON.stringify({ value }), signal: controller.signal });
981
+ } catch (err) {
982
+ if (err.name !== 'AbortError') throw err;
983
+ } finally {
984
+ prompt.loading = false;
985
+ prompt.disabled = false;
986
+ stopBtn.style.display = 'none';
987
+ }
988
+ });
989
+
990
+ // With stoppable set, kai-stop fires when the built-in Stop button is clicked.
991
+ // prompt.addEventListener('kai-stop', () => controller?.abort());
992
+ // OR compose your own stop button outside the element:
993
+ stopBtn.addEventListener('click', () => controller?.abort());
994
+ </script>`,
995
+
996
+ react: `import { useState, useRef } from 'react';
997
+ import { PromptInput, ModelSwitcher } from '@kitn.ai/ui/react';
998
+
999
+ const MODELS = [
1000
+ { id: 'claude-4-opus', name: 'Claude 4 Opus', provider: 'Anthropic' },
1001
+ { id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' },
1002
+ ];
1003
+ const SUGGESTIONS = ['Summarize this document', 'What are the key takeaways?'];
1004
+
1005
+ export function Prompt() {
1006
+ const [modelId, setModelId] = useState('claude-4-opus');
1007
+ const [streaming, setStreaming] = useState(false);
1008
+ const controllerRef = useRef<AbortController | null>(null);
1009
+
1010
+ const handleSubmit = async (e: CustomEvent<{ value: string; attachments: unknown[] }>) => {
1011
+ // attachments always present, may be empty array
1012
+ const { value, attachments } = e.detail;
1013
+ setStreaming(true);
1014
+ controllerRef.current = new AbortController();
1015
+ try {
1016
+ await fetch('/api/chat', {
1017
+ method: 'POST',
1018
+ body: JSON.stringify({ value, modelId, attachments }),
1019
+ signal: controllerRef.current.signal,
1020
+ });
1021
+ } catch (err: unknown) {
1022
+ if ((err as Error).name !== 'AbortError') throw err;
1023
+ } finally {
1024
+ setStreaming(false);
1025
+ }
1026
+ };
1027
+
1028
+ return (
1029
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', maxWidth: '42rem' }}>
1030
+ <ModelSwitcher
1031
+ models={MODELS}
1032
+ currentModel={modelId}
1033
+ onModelChange={(e) => setModelId(e.detail.modelId)}
1034
+ />
1035
+ {/* suggestions passed as array property; suggestionMode="fill" lets user edit first */}
1036
+ <PromptInput
1037
+ suggestions={SUGGESTIONS}
1038
+ suggestionMode="fill"
1039
+ loading={streaming}
1040
+ disabled={streaming}
1041
+ placeholder={streaming ? 'Generating...' : 'Ask anything...'}
1042
+ onSubmit={handleSubmit}
1043
+ />
1044
+ {streaming && (
1045
+ {/* With stoppable + onStop, kai-prompt-input renders the Stop button for you.
1046
+ Here we compose it manually for illustration. */}
1047
+ <button onClick={() => controllerRef.current?.abort()}>Stop</button>
1048
+ )}
1049
+ </div>
1050
+ );
1051
+ }`,
1052
+
1053
+ vue: `<script setup>
1054
+ import '@kitn.ai/ui/elements';
1055
+ import { ref } from 'vue';
1056
+
1057
+ const MODELS = [
1058
+ { id: 'claude-4-opus', name: 'Claude 4 Opus', provider: 'Anthropic' },
1059
+ { id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' },
1060
+ ];
1061
+ const SUGGESTIONS = ['Summarize this document', 'What are the key takeaways?'];
1062
+
1063
+ const modelId = ref('claude-4-opus');
1064
+ const streaming = ref(false);
1065
+ let controller = null;
1066
+
1067
+ function onModelChange(e) { modelId.value = e.detail.modelId; }
1068
+
1069
+ async function onSubmit(e) {
1070
+ const { value, attachments } = e.detail; // attachments always present
1071
+ streaming.value = true;
1072
+ controller = new AbortController();
1073
+ try {
1074
+ await fetch('/api/chat', {
1075
+ method: 'POST',
1076
+ body: JSON.stringify({ value, modelId: modelId.value, attachments }),
1077
+ signal: controller.signal,
1078
+ });
1079
+ } catch (err) {
1080
+ if (err.name !== 'AbortError') throw err;
1081
+ } finally {
1082
+ streaming.value = false;
1083
+ }
1084
+ }
1085
+
1086
+ function stop() { controller?.abort(); }
1087
+ </script>
1088
+
1089
+ <template>
1090
+ <div style="display:flex; flex-direction:column; gap:1rem; max-width:42rem">
1091
+ <kai-model-switcher
1092
+ :models.prop="MODELS"
1093
+ :current-model="modelId"
1094
+ @kai-model-change="onModelChange"
1095
+ />
1096
+ <!-- suggestion-mode="fill" lets the user edit before sending -->
1097
+ <kai-prompt-input
1098
+ :suggestions.prop="SUGGESTIONS"
1099
+ suggestion-mode="fill"
1100
+ :loading="streaming"
1101
+ :disabled="streaming"
1102
+ :placeholder="streaming ? 'Generating...' : 'Ask anything...'"
1103
+ @kai-submit="onSubmit"
1104
+ />
1105
+ <!-- Add stoppable + listen for kai-stop to use the built-in Stop button.
1106
+ Here we compose it manually outside the element for illustration. -->
1107
+ <button v-if="streaming" @click="stop">Stop</button>
1108
+ </div>
1109
+ </template>`,
1110
+
1111
+ svelte: `<script>
1112
+ import '@kitn.ai/ui/elements';
1113
+
1114
+ const MODELS = [
1115
+ { id: 'claude-4-opus', name: 'Claude 4 Opus', provider: 'Anthropic' },
1116
+ { id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' },
1117
+ ];
1118
+ const SUGGESTIONS = ['Summarize this document', 'What are the key takeaways?'];
1119
+
1120
+ let modelEl;
1121
+ let modelId = 'claude-4-opus';
1122
+ let streaming = false;
1123
+ let controller;
1124
+
1125
+ // Arrays must be set as properties (not attributes).
1126
+ $: if (modelEl) { modelEl.models = MODELS; modelEl.currentModel = modelId; }
1127
+
1128
+ function onModelChange(e) { modelId = e.detail.modelId; }
1129
+
1130
+ async function onSubmit(e) {
1131
+ const { value, attachments } = e.detail; // attachments always present
1132
+ streaming = true;
1133
+ controller = new AbortController();
1134
+ try {
1135
+ await fetch('/api/chat', {
1136
+ method: 'POST',
1137
+ body: JSON.stringify({ value, modelId, attachments }),
1138
+ signal: controller.signal,
1139
+ });
1140
+ } catch (err) {
1141
+ if (err.name !== 'AbortError') throw err;
1142
+ } finally {
1143
+ streaming = false;
1144
+ }
1145
+ }
1146
+
1147
+ function stop() { controller?.abort(); }
1148
+ </script>
1149
+
1150
+ <div style="display:flex; flex-direction:column; gap:1rem; max-width:42rem">
1151
+ <kai-model-switcher bind:this={modelEl} on:kai-model-change={onModelChange} />
1152
+ <!-- suggestion-mode="fill" lets the user edit before sending -->
1153
+ <kai-prompt-input
1154
+ suggestions={SUGGESTIONS}
1155
+ suggestion-mode="fill"
1156
+ loading={streaming}
1157
+ disabled={streaming}
1158
+ placeholder={streaming ? 'Generating...' : 'Ask anything...'}
1159
+ on:kai-submit={onSubmit}
1160
+ />
1161
+ <!-- Add stoppable + listen for kai-stop to use the built-in Stop button.
1162
+ Here we compose it manually outside the element for illustration. -->
1163
+ {#if streaming}
1164
+ <button on:click={stop}>Stop</button>
1165
+ {/if}
1166
+ </div>`,
1167
+
1168
+ angular: `import { Component, CUSTOM_ELEMENTS_SCHEMA, signal, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
1169
+
1170
+ const MODELS = [
1171
+ { id: 'claude-4-opus', name: 'Claude 4 Opus', provider: 'Anthropic' },
1172
+ { id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' },
1173
+ ];
1174
+ const SUGGESTIONS = ['Summarize this document', 'What are the key takeaways?'];
1175
+
1176
+ @Component({
1177
+ selector: 'app-prompt',
1178
+ standalone: true,
1179
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
1180
+ template: \`
1181
+ <div style="display:flex; flex-direction:column; gap:1rem; max-width:42rem">
1182
+ <kai-model-switcher
1183
+ #models
1184
+ [models]="MODELS"
1185
+ [currentModel]="modelId()"
1186
+ (kai-model-change)="onModelChange($event)"
1187
+ ></kai-model-switcher>
1188
+ <!-- suggestion-mode="fill" lets the user edit before sending -->
1189
+ <kai-prompt-input
1190
+ #prompt
1191
+ [suggestions]="SUGGESTIONS"
1192
+ suggestion-mode="fill"
1193
+ [loading]="streaming()"
1194
+ [disabled]="streaming()"
1195
+ [placeholder]="streaming() ? 'Generating...' : 'Ask anything...'"
1196
+ (kai-submit)="onSubmit($event)"
1197
+ ></kai-prompt-input>
1198
+ <!-- Stop is NOT built in — compose it yourself -->
1199
+ @if (streaming()) {
1200
+ <button (click)="stop()">Stop</button>
1201
+ }
1202
+ </div>
1203
+ \`,
1204
+ })
1205
+ export class PromptComponent {
1206
+ MODELS = MODELS;
1207
+ SUGGESTIONS = SUGGESTIONS;
1208
+ modelId = signal('claude-4-opus');
1209
+ streaming = signal(false);
1210
+ private controller: AbortController | null = null;
1211
+
1212
+ onModelChange(e: CustomEvent<{ modelId: string }>) { this.modelId.set(e.detail.modelId); }
1213
+
1214
+ async onSubmit(e: CustomEvent<{ value: string; attachments: unknown[] }>) {
1215
+ const { value, attachments } = e.detail; // attachments always present
1216
+ this.streaming.set(true);
1217
+ this.controller = new AbortController();
1218
+ try {
1219
+ await fetch('/api/chat', {
1220
+ method: 'POST',
1221
+ body: JSON.stringify({ value, modelId: this.modelId(), attachments }),
1222
+ signal: this.controller.signal,
1223
+ });
1224
+ } catch (err: unknown) {
1225
+ if ((err as Error).name !== 'AbortError') throw err;
1226
+ } finally {
1227
+ this.streaming.set(false);
1228
+ }
1229
+ }
1230
+
1231
+ stop() { this.controller?.abort(); }
1232
+ }`,
1233
+
1234
+ solid: `import { createSignal, For, Show } from 'solid-js';
1235
+ import { PromptInput, PromptInputTextarea, PromptInputActions, PromptSuggestion, ModelSwitcher, Loader, Button } from '@kitn.ai/ui';
1236
+ import type { ModelOption } from '@kitn.ai/ui';
1237
+ import { ArrowUp, Square } from 'lucide-solid';
1238
+
1239
+ const MODELS: ModelOption[] = [
1240
+ { id: 'claude-4-opus', name: 'Claude 4 Opus', provider: 'Anthropic' },
1241
+ { id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' },
1242
+ ];
1243
+ const SUGGESTION_GROUPS = [
1244
+ { label: 'Get started', items: ['Summarize this document', 'What are the key takeaways?'] },
1245
+ { label: 'Go deeper', items: ['Compare with similar approaches', 'Find contradictions'] },
1246
+ ];
1247
+
1248
+ export function Prompt() {
1249
+ const [value, setValue] = createSignal('');
1250
+ const [modelId, setModelId] = createSignal('claude-4-opus');
1251
+ const [streaming, setStreaming] = createSignal(false);
1252
+ let controller: AbortController | undefined;
1253
+
1254
+ const handleSubmit = async () => {
1255
+ if (!value().trim()) return;
1256
+ setStreaming(true);
1257
+ setValue('');
1258
+ controller = new AbortController();
1259
+ try {
1260
+ // Replace with your real SSE / streaming fetch.
1261
+ await fetch('/api/chat', {
1262
+ method: 'POST',
1263
+ body: JSON.stringify({ value: value(), modelId: modelId() }),
1264
+ signal: controller.signal,
1265
+ });
1266
+ } catch (err: unknown) {
1267
+ if ((err as Error).name !== 'AbortError') throw err;
1268
+ } finally {
1269
+ setStreaming(false);
1270
+ }
1271
+ };
1272
+
1273
+ const handleStop = () => controller?.abort();
1274
+
1275
+ return (
1276
+ <div class="flex flex-col gap-4 max-w-2xl">
1277
+ <Show when={!streaming()}>
1278
+ <For each={SUGGESTION_GROUPS}>
1279
+ {(group) => (
1280
+ <div class="space-y-2">
1281
+ <span class="text-xs font-medium text-muted-foreground uppercase tracking-wider">{group.label}</span>
1282
+ <div class="flex flex-wrap gap-2">
1283
+ <For each={group.items}>
1284
+ {/* suggestionMode="fill" (default "submit") — here we fill manually */}
1285
+ {(item) => <PromptSuggestion onClick={() => setValue(item)}>{item}</PromptSuggestion>}
1286
+ </For>
1287
+ </div>
1288
+ </div>
1289
+ )}
1290
+ </For>
1291
+ </Show>
1292
+ <PromptInput
1293
+ value={value()}
1294
+ onValueChange={setValue}
1295
+ onSubmit={handleSubmit}
1296
+ disabled={streaming()}
1297
+ isLoading={streaming()} // Note: Solid prop is isLoading; element attribute is loading
1298
+ >
1299
+ <PromptInputTextarea placeholder={streaming() ? 'Generating...' : 'Ask anything...'} />
1300
+ <PromptInputActions class="justify-between">
1301
+ <Show
1302
+ when={streaming()}
1303
+ fallback={
1304
+ // ModelSwitcher renders nothing when models.length <= 1
1305
+ <ModelSwitcher models={MODELS} currentModelId={modelId()} onModelChange={setModelId} />
1306
+ }
1307
+ >
1308
+ <div class="flex items-center gap-2">
1309
+ <Loader variant="typing" size="sm" />
1310
+ <span class="text-xs text-foreground">Generating…</span>
1311
+ </div>
1312
+ </Show>
1313
+ <Show
1314
+ when={streaming()}
1315
+ fallback={
1316
+ // Enter (no Shift) also submits — the send button is a redundant affordance
1317
+ <Button variant="default" size="icon-sm" class="rounded-full" disabled={!value()} aria-label="Send message" onClick={handleSubmit}>
1318
+ <ArrowUp class="size-4" />
1319
+ </Button>
1320
+ }
1321
+ >
1322
+ {/* Solid primitive: wire the Stop button yourself inside PromptInputActions.
1323
+ With the kai-prompt-input element, add stoppable and listen for kai-stop instead. */}
1324
+ <Button variant="outline" size="icon-sm" class="rounded-full" aria-label="Stop generation" onClick={handleStop}>
1325
+ <Square class="size-3" />
1326
+ </Button>
1327
+ </Show>
1328
+ </PromptInputActions>
1329
+ </PromptInput>
1330
+ </div>
1331
+ );
1332
+ }`,
1333
+ },
1334
+ };
1335
+
1336
+ /**
1337
+ * Example: Prompt Input Variants — a complete prompt box (text, suggestions,
1338
+ * action buttons, streaming, model selector, file attachments) built from the
1339
+ * `kai-prompt-input` element. Per-story: the Usage tab shows the snippet for
1340
+ * the story you're on; the example-level fields below are the fallback.
1341
+ */
1342
+ const promptInputVariants: ExampleUsage = {
1343
+ title: 'Examples/Prompt Input Variants',
1344
+ ...basic, // example-level fallback = the headline "Basic Input"
1345
+ stories: {
1346
+ 'Basic Input': basic,
1347
+ 'With Suggestion Chips': suggestions,
1348
+ 'With Action Buttons': actionButtons,
1349
+ 'Streaming / Loading State': streaming,
1350
+ 'With Model Selector': modelSelector,
1351
+ 'With File Attachments': withFileAttachments,
1352
+ 'Full Example': fullExample,
1353
+ },
1354
+ };
1355
+
1356
+ export default promptInputVariants;