@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,43 @@
1
+ import { createSignal, onCleanup } from 'solid-js';
2
+
3
+ const SCROLL_THRESHOLD = 50;
4
+
5
+ export function useStickToBottom() {
6
+ const [isAtBottom, setIsAtBottom] = createSignal(true);
7
+ let containerEl: HTMLElement | undefined;
8
+ let shouldStick = true;
9
+
10
+ function checkIfAtBottom() {
11
+ if (!containerEl) return;
12
+ const { scrollTop, scrollHeight, clientHeight } = containerEl;
13
+ const atBottom = scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD;
14
+ setIsAtBottom(atBottom);
15
+ shouldStick = atBottom;
16
+ }
17
+
18
+ function scrollToBottom(behavior: ScrollBehavior = 'smooth') {
19
+ if (!containerEl) return;
20
+ containerEl.scrollTo({ top: containerEl.scrollHeight, behavior });
21
+ shouldStick = true;
22
+ setIsAtBottom(true);
23
+ }
24
+
25
+ function onNewContent() {
26
+ if (shouldStick) {
27
+ requestAnimationFrame(() => scrollToBottom('instant'));
28
+ }
29
+ }
30
+
31
+ function ref(el: HTMLElement) {
32
+ containerEl = el;
33
+ el.addEventListener('scroll', checkIfAtBottom, { passive: true });
34
+ const observer = new MutationObserver(onNewContent);
35
+ observer.observe(el, { childList: true, subtree: true, characterData: true });
36
+ onCleanup(() => {
37
+ el.removeEventListener('scroll', checkIfAtBottom);
38
+ observer.disconnect();
39
+ });
40
+ }
41
+
42
+ return { ref, isAtBottom, scrollToBottom };
43
+ }
@@ -0,0 +1,112 @@
1
+ import { createSignal, onCleanup } from 'solid-js';
2
+
3
+ export interface UseTextStreamOptions {
4
+ mode: 'typewriter' | 'fade';
5
+ speed?: number;
6
+ characterChunkSize?: number;
7
+ fadeDuration?: number;
8
+ }
9
+
10
+ export interface TextStreamSegment {
11
+ text: string;
12
+ index: number;
13
+ }
14
+
15
+ export function useTextStream(options: UseTextStreamOptions) {
16
+ const speed = options.speed ?? 20;
17
+ const chunkSize = options.characterChunkSize ?? 3;
18
+
19
+ const [displayedText, setDisplayedText] = createSignal('');
20
+ const [isComplete, setIsComplete] = createSignal(true);
21
+ const [segments, setSegments] = createSignal<TextStreamSegment[]>([]);
22
+
23
+ let fullText = '';
24
+ let charIndex = 0;
25
+ let intervalId: ReturnType<typeof setInterval> | undefined;
26
+ let isPaused = false;
27
+ let asyncIterator: AsyncIterator<string> | undefined;
28
+
29
+ function clearInterval_() {
30
+ if (intervalId !== undefined) {
31
+ clearInterval(intervalId);
32
+ intervalId = undefined;
33
+ }
34
+ }
35
+
36
+ function typewriterTick() {
37
+ if (isPaused) return;
38
+ if (charIndex >= fullText.length) {
39
+ if (!asyncIterator) {
40
+ clearInterval_();
41
+ setIsComplete(true);
42
+ }
43
+ return;
44
+ }
45
+ const end = Math.min(charIndex + chunkSize, fullText.length);
46
+ charIndex = end;
47
+ setDisplayedText(fullText.slice(0, charIndex));
48
+ }
49
+
50
+ async function consumeAsyncIterable(source: AsyncIterable<string>) {
51
+ asyncIterator = source[Symbol.asyncIterator]();
52
+ try {
53
+ while (true) {
54
+ const { value, done } = await asyncIterator.next();
55
+ if (done) break;
56
+ if (value) {
57
+ fullText += value;
58
+ setSegments((prev) => [...prev, { text: value, index: prev.length }]);
59
+ }
60
+ }
61
+ } finally {
62
+ asyncIterator = undefined;
63
+ }
64
+ }
65
+
66
+ function startStreaming(source: string | AsyncIterable<string>) {
67
+ reset();
68
+ setIsComplete(false);
69
+
70
+ if (typeof source === 'string') {
71
+ fullText = source;
72
+ if (options.mode === 'typewriter') {
73
+ intervalId = setInterval(typewriterTick, speed);
74
+ } else {
75
+ // Deliver all segments at once — CSS animation-delay handles staggered fade-in
76
+ const words = source.split(/(\s+)/).filter(Boolean);
77
+ setSegments(words.map((text, index) => ({ text, index })));
78
+ setDisplayedText(source);
79
+ // isComplete fires after animation has time to play
80
+ // (segments count * delay per segment from ResponseStream)
81
+ }
82
+ } else {
83
+ if (options.mode === 'typewriter') {
84
+ intervalId = setInterval(typewriterTick, speed);
85
+ }
86
+ consumeAsyncIterable(source).then(() => {
87
+ if (options.mode === 'fade') {
88
+ setDisplayedText(fullText);
89
+ setIsComplete(true);
90
+ }
91
+ });
92
+ }
93
+ }
94
+
95
+ function pause() { isPaused = true; }
96
+ function resume() { isPaused = false; }
97
+
98
+ function reset() {
99
+ clearInterval_();
100
+ fullText = '';
101
+ charIndex = 0;
102
+ isPaused = false;
103
+ asyncIterator = undefined;
104
+ setDisplayedText('');
105
+ setIsComplete(true);
106
+ setSegments([]);
107
+ }
108
+
109
+ onCleanup(() => clearInterval_());
110
+
111
+ return { displayedText, isComplete, segments, startStreaming, pause, resume, reset };
112
+ }
@@ -0,0 +1,50 @@
1
+ import { createSignal, onCleanup } from 'solid-js';
2
+
3
+ export interface UseVoiceRecorderOptions {
4
+ mimeType?: string;
5
+ }
6
+
7
+ export function useVoiceRecorder(options: UseVoiceRecorderOptions = {}) {
8
+ const mimeType = options.mimeType ?? 'audio/webm;codecs=opus';
9
+ const [isRecording, setIsRecording] = createSignal(false);
10
+ const [error, setError] = createSignal<string | null>(null);
11
+
12
+ let mediaRecorder: MediaRecorder | undefined;
13
+ let chunks: Blob[] = [];
14
+ let resolveBlob: ((blob: Blob) => void) | undefined;
15
+
16
+ async function start(): Promise<Blob> {
17
+ setError(null);
18
+ chunks = [];
19
+ try {
20
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
21
+ mediaRecorder = new MediaRecorder(stream, { mimeType });
22
+ mediaRecorder.ondataavailable = (e) => {
23
+ if (e.data.size > 0) chunks.push(e.data);
24
+ };
25
+ mediaRecorder.onstop = () => {
26
+ const blob = new Blob(chunks, { type: mimeType });
27
+ stream.getTracks().forEach((t) => t.stop());
28
+ setIsRecording(false);
29
+ resolveBlob?.(blob);
30
+ };
31
+ mediaRecorder.start();
32
+ setIsRecording(true);
33
+ return new Promise<Blob>((resolve) => { resolveBlob = resolve; });
34
+ } catch (err) {
35
+ setError(err instanceof Error ? err.message : 'Microphone access denied');
36
+ setIsRecording(false);
37
+ throw err;
38
+ }
39
+ }
40
+
41
+ function stop() {
42
+ if (mediaRecorder && mediaRecorder.state === 'recording') {
43
+ mediaRecorder.stop();
44
+ }
45
+ }
46
+
47
+ onCleanup(() => stop());
48
+
49
+ return { isRecording, error, start, stop };
50
+ }
@@ -0,0 +1,345 @@
1
+ // src/remote/host-embed.ts
2
+ // The host embed SDK: mountRemoteCard() + the RemoteCardHandle. Drops one sandboxed
3
+ // cross-origin iframe, runs the handshake (origin+source+nonce+version), pushes
4
+ // CardContext + CardEnvelope down, validates inbound up-frames, and routes every
5
+ // CardEvent through the SAME routeCardEvent the native transport uses. Auto-sizes the
6
+ // iframe from `resize`, intercepts `focus-edge`.
7
+ //
8
+ // Security-critical, stateful, vanilla DOM ONLY — NO SolidJS imports. See the
9
+ // iframe-transport design spec ("Host embed SDK surface" + addendum H-A/H-C/H-D/H-E/
10
+ // H-F/H-G/H-O).
11
+
12
+ import {
13
+ type CardEnvelope,
14
+ type CardContext,
15
+ type CardEvent,
16
+ type CardPolicy,
17
+ } from '../primitives/card-contract';
18
+ import { createPacker, isCardWireFrame, type WireMessage } from './wire';
19
+ import { assertCrossOrigin, redactFrame } from './origin';
20
+ import { isValidVersion } from './version';
21
+ import { hasPollutionKey, isKnownEventKind } from './validate';
22
+ import { routeCardEvent } from '../primitives/card-routing';
23
+
24
+ export interface MountRemoteCardOptions {
25
+ /** Where to mount the iframe. */
26
+ container: HTMLElement;
27
+ /** The card to render (carried down as a `render` frame). */
28
+ envelope: CardEnvelope;
29
+ /** EXACT provider origin (https). Pinned for all origin checks. */
30
+ providerOrigin: string;
31
+ /** The provider URL the iframe frames (must be same-origin as providerOrigin). */
32
+ src: string;
33
+ /** Ambient context; theme/locale/conversationId/short-lived authToken. */
34
+ context: CardContext;
35
+ /** The SAME routing policy native cards use. resize/focus-edge are handled by the SDK. */
36
+ policy?: CardPolicy;
37
+ /** Override sandbox. Default: allow-scripts allow-forms allow-same-origin (NO allow-popups — H-G). */
38
+ sandbox?: string;
39
+ /** Cap auto-height (px); frame scrolls internally beyond it. */
40
+ maxHeight?: number;
41
+ /** Handshake timeout (ms). Default 5000. */
42
+ handshakeTimeoutMs?: number;
43
+ }
44
+
45
+ export interface RemoteCardHandle {
46
+ /** Push new ambient context (theme toggle, token refresh). Shallow top-level merge;
47
+ * `theme`/`a11y` replaced wholesale. Always sends a FULL resolved CardContext. */
48
+ updateContext(context: Partial<CardContext>): void;
49
+ /** Re-render with a new/updated envelope (same id = update). */
50
+ update(envelope: CardEnvelope): void;
51
+ /** Send `teardown`, stop listeners, remove the iframe. Idempotent. */
52
+ destroy(): void;
53
+ /** Current bridge state for the host to reflect in UI. */
54
+ state(): 'connecting' | 'open' | 'error' | 'closed';
55
+ }
56
+
57
+ type BridgeState = 'connecting' | 'open' | 'error' | 'closed';
58
+
59
+ const HOST_SUPPORTED_VERSIONS = ['1'];
60
+ const DEFAULT_SANDBOX = 'allow-scripts allow-forms allow-same-origin';
61
+ const DEFAULT_HANDSHAKE_TIMEOUT_MS = 5000;
62
+
63
+ /** Provider host for the iframe title (best-effort; never throws). */
64
+ function providerHost(providerOrigin: string): string {
65
+ try { return new URL(providerOrigin).host; } catch { return providerOrigin; }
66
+ }
67
+
68
+ export function mountRemoteCard(options: MountRemoteCardOptions): RemoteCardHandle {
69
+ const {
70
+ container,
71
+ providerOrigin,
72
+ src,
73
+ policy,
74
+ maxHeight,
75
+ } = options;
76
+
77
+ // ── Cross-origin precondition (H-F): fail closed BEFORE mounting. ──
78
+ const hostOrigin = typeof window !== 'undefined' ? window.location.origin : '';
79
+ assertCrossOrigin(src, providerOrigin, hostOrigin);
80
+
81
+ const handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
82
+
83
+ // ── Bridge identity + generation (H-A). A new nonce per generation; stale frames are
84
+ // rejected by construction (generation capture + nonce equality). ──
85
+ let generation = 0;
86
+ let nonce = mintNonce();
87
+ let packer = createPacker('1', nonce); // provisional until `ready` negotiates the version.
88
+
89
+ // ── State machine (H-E). ──
90
+ let state: BridgeState = 'connecting';
91
+
92
+ // ── Latest-wins pre-OPEN buffer (H-E). Always the newest pending context/render. ──
93
+ let currentContext: CardContext = resolveContext(options.context);
94
+ let currentEnvelope: CardEnvelope = options.envelope;
95
+ let pendingContext: CardContext | null = null;
96
+ let pendingRender: CardEnvelope | null = null;
97
+
98
+ let iframe: HTMLIFrameElement | null = null;
99
+ let timer: ReturnType<typeof setTimeout> | undefined;
100
+ let listener: ((e: MessageEvent) => void) | null = null;
101
+
102
+ function warn(data: unknown): void {
103
+ try { console.warn('[kai-remote]', redactFrame(data)); } catch { /* best-effort */ }
104
+ }
105
+
106
+ function postDown(message: WireMessage): void {
107
+ const cw = iframe?.contentWindow;
108
+ if (!cw) return;
109
+ try { cw.postMessage(packer(message), providerOrigin); } catch { /* best-effort */ }
110
+ }
111
+
112
+ function clearTimer(): void {
113
+ if (timer !== undefined) { clearTimeout(timer); timer = undefined; }
114
+ }
115
+
116
+ function sendContext(ctx: CardContext): void {
117
+ if (state === 'open') postDown({ dir: 'down', kind: 'context', context: ctx });
118
+ else if (state === 'connecting') pendingContext = ctx; // latest-wins; no buffer in error/closed
119
+ }
120
+
121
+ function sendRender(envelope: CardEnvelope): void {
122
+ if (state === 'open') postDown({ dir: 'down', kind: 'render', envelope });
123
+ else if (state === 'connecting') pendingRender = envelope; // latest-wins; no buffer in error/closed
124
+ }
125
+
126
+ function flushBuffer(): void {
127
+ // Order: context → render (H-E), so the runtime themes before first paint.
128
+ if (pendingContext) { postDown({ dir: 'down', kind: 'context', context: pendingContext }); pendingContext = null; }
129
+ if (pendingRender) { postDown({ dir: 'down', kind: 'render', envelope: pendingRender }); pendingRender = null; }
130
+ }
131
+
132
+ function renderFallback(message: string): void {
133
+ // Replace the iframe with an inline accessible fallback + a Retry button.
134
+ const fallback = document.createElement('div');
135
+ fallback.setAttribute('role', 'alert');
136
+ fallback.dataset.kcRemoteFallback = '';
137
+ const msg = document.createElement('p');
138
+ msg.textContent = message;
139
+ fallback.appendChild(msg);
140
+ const retry = document.createElement('button');
141
+ retry.type = 'button';
142
+ retry.textContent = 'Retry';
143
+ retry.addEventListener('click', () => retryMount());
144
+ fallback.appendChild(retry);
145
+ if (iframe && iframe.parentNode === container) container.replaceChild(fallback, iframe);
146
+ else container.appendChild(fallback);
147
+ iframe = null;
148
+ }
149
+
150
+ function toError(message: string): void {
151
+ if (state === 'closed') return;
152
+ clearTimer();
153
+ state = 'error';
154
+ pendingContext = null;
155
+ pendingRender = null; // discard the coalesced buffer (H-O)
156
+ renderFallback(message);
157
+ }
158
+
159
+ /** Build the iframe, wire the listener BEFORE setting src (race closure), append, set src. */
160
+ function createIframe(): void {
161
+ const myGen = generation;
162
+ const el = document.createElement('iframe');
163
+ el.setAttribute('sandbox', options.sandbox ?? DEFAULT_SANDBOX);
164
+ el.setAttribute('referrerpolicy', 'no-referrer');
165
+ el.setAttribute('allow', '');
166
+ const titleBase = currentEnvelope.title ?? currentEnvelope.type;
167
+ el.setAttribute('title', `${titleBase} — provided by ${providerHost(providerOrigin)}`);
168
+ el.style.border = '0';
169
+ el.style.width = '100%';
170
+
171
+ el.addEventListener('load', () => {
172
+ if (generation !== myGen || state !== 'connecting') return;
173
+ // Provisional packer (negotiated version is set on `ready`).
174
+ packer = createPacker('1', nonce);
175
+ postDown({ dir: 'down', kind: 'hello', supportedVersions: HOST_SUPPORTED_VERSIONS });
176
+ clearTimer();
177
+ timer = setTimeout(() => {
178
+ if (generation !== myGen || state !== 'connecting') return;
179
+ toError('[timeout] The card took too long to load.');
180
+ policy?.onError?.(currentEnvelope.id, '[timeout] handshake timed out');
181
+ }, handshakeTimeoutMs);
182
+ });
183
+
184
+ // Listener BEFORE src (H — load-before-listener race closure).
185
+ listener = (event: MessageEvent) => onMessage(event, myGen);
186
+ window.addEventListener('message', listener);
187
+
188
+ iframe = el;
189
+ container.appendChild(el);
190
+ el.setAttribute('src', src); // set LAST.
191
+ }
192
+
193
+ function retryMount(): void {
194
+ // Guard: only retry from error state; a double-click (or stray call) is a no-op.
195
+ if (state !== 'error') return;
196
+ // New generation + new nonce; stale frames from the dead iframe are rejected.
197
+ generation += 1;
198
+ nonce = mintNonce();
199
+ packer = createPacker('1', nonce);
200
+ state = 'connecting';
201
+ pendingContext = null;
202
+ pendingRender = null;
203
+ clearTimer();
204
+ if (listener) { window.removeEventListener('message', listener); listener = null; }
205
+ // Defensively remove any lingering iframe before creating a fresh one.
206
+ if (iframe) { iframe.remove(); iframe = null; }
207
+ // Always re-push the latest resolved context + envelope after the new handshake.
208
+ pendingContext = currentContext;
209
+ pendingRender = currentEnvelope;
210
+ // Remove any existing fallback before re-mounting.
211
+ container.querySelectorAll('[data-kai-remote-fallback]').forEach((n) => n.remove());
212
+ createIframe();
213
+ }
214
+
215
+ function sizeIframe(height: number): void {
216
+ if (!iframe) return;
217
+ const clamped = Math.min(height, maxHeight ?? Infinity);
218
+ iframe.style.height = `${clamped}px`;
219
+ }
220
+
221
+ function moveFocusPastIframe(): void {
222
+ // v1 focus-edge handling: blur the iframe so default browser tab order moves past it.
223
+ // (A richer "next host focusable" mover is a later refinement; blur is deterministic
224
+ // and never routes through CardPolicy.)
225
+ if (iframe) { try { iframe.blur(); } catch { /* best-effort */ } }
226
+ }
227
+
228
+ function onMessage(event: MessageEvent, myGen: number): void {
229
+ // ── Stale-generation defense (H-A): ignore frames from a superseded/closed bridge. ──
230
+ if (myGen !== generation || state === 'closed' || state === 'error') return;
231
+ const data = event.data;
232
+
233
+ // ── Inbound security gates, in order (H-A/H-C/H-D): ──
234
+ // 1. origin pin 2. source-window pin 3. structural + direction guard
235
+ // 4. nonce equality (bridge-instance binding)
236
+ if (event.origin !== providerOrigin) return;
237
+ if (event.source !== iframe?.contentWindow) return;
238
+ if (!isCardWireFrame(data, 'up')) return;
239
+ if (data.nonce !== nonce) return;
240
+
241
+ // A throw past the gate must NOT kill the listener.
242
+ try {
243
+ const message = data.message;
244
+ switch (message.kind) {
245
+ case 'ready': {
246
+ if (state !== 'connecting') return;
247
+ const accepted = message.acceptedVersion;
248
+ // 5. version: accepted MUST be a host-supported version (H-C).
249
+ if (!isValidVersion(accepted) || !HOST_SUPPORTED_VERSIONS.includes(accepted)) {
250
+ toError('[version-unsupported] This card needs a different host version.');
251
+ policy?.onError?.(currentEnvelope.id, '[version-unsupported] no compatible protocol version');
252
+ return;
253
+ }
254
+ // Rebuild the packer with the negotiated version (H-C).
255
+ packer = createPacker(accepted, nonce);
256
+ clearTimer();
257
+ state = 'open';
258
+ // Seed the buffer with the current resolved context + envelope, then flush.
259
+ if (!pendingContext) pendingContext = currentContext;
260
+ if (!pendingRender) pendingRender = currentEnvelope;
261
+ flushBuffer();
262
+ return;
263
+ }
264
+ case 'event': {
265
+ if (state !== 'open') return;
266
+ const ev: CardEvent = message.event;
267
+ // cardId fence (H-E): drop in-flight events for a replaced/stale card.
268
+ if (ev.cardId !== currentEnvelope.id) return;
269
+ // resize is transport plumbing — intercept, never route (H-J / spec UP table).
270
+ if (ev.kind === 'resize') { sizeIframe(ev.height); return; }
271
+ // Schema/pollution gate before routing (H-D).
272
+ if (!isKnownEventKind(ev.kind) || hasPollutionKey(ev)) { warn(data); return; }
273
+ routeCardEvent(policy ?? {}, ev);
274
+ return;
275
+ }
276
+ case 'focus-edge': {
277
+ // a11y plumbing — intercept, never route (H-O).
278
+ moveFocusPastIframe();
279
+ return;
280
+ }
281
+ case 'fault': {
282
+ const code = message.code;
283
+ const reason = message.message;
284
+ toError(`[${code}] The card reported a problem.`);
285
+ policy?.onError?.(currentEnvelope.id, `[${code}] ${reason}`);
286
+ return;
287
+ }
288
+ default:
289
+ return;
290
+ }
291
+ } catch {
292
+ warn(data);
293
+ }
294
+ }
295
+
296
+ // ── Public handle ──────────────────────────────────────────────────────────
297
+ const handle: RemoteCardHandle = {
298
+ updateContext(partial: Partial<CardContext>): void {
299
+ if (state === 'closed') return;
300
+ currentContext = mergeContext(currentContext, partial);
301
+ sendContext(currentContext);
302
+ },
303
+ update(envelope: CardEnvelope): void {
304
+ if (state === 'closed') return;
305
+ currentEnvelope = envelope;
306
+ sendRender(envelope);
307
+ },
308
+ destroy(): void {
309
+ if (state === 'closed') return;
310
+ generation += 1; // tombstone the current generation (rejects stale inbound).
311
+ if (state === 'open') postDown({ dir: 'down', kind: 'teardown' });
312
+ state = 'closed';
313
+ clearTimer();
314
+ if (listener) { window.removeEventListener('message', listener); listener = null; }
315
+ if (iframe) { iframe.remove(); iframe = null; }
316
+ container.querySelectorAll('[data-kai-remote-fallback]').forEach((n) => n.remove());
317
+ },
318
+ state(): BridgeState {
319
+ return state;
320
+ },
321
+ };
322
+
323
+ createIframe();
324
+ return handle;
325
+ }
326
+
327
+ /** Crypto-random hex nonce (H-A): 16 bytes → 32 hex chars. */
328
+ function mintNonce(): string {
329
+ const bytes = new Uint8Array(16);
330
+ crypto.getRandomValues(bytes);
331
+ let out = '';
332
+ for (const b of bytes) out += b.toString(16).padStart(2, '0');
333
+ return out;
334
+ }
335
+
336
+ /** A shallow copy so the caller's context object isn't mutated by later merges. */
337
+ function resolveContext(ctx: CardContext): CardContext {
338
+ return { ...ctx };
339
+ }
340
+
341
+ /** Shallow top-level merge (H-E): `theme` and `a11y` are replaced wholesale, not
342
+ * deep-merged. Returns a new full resolved CardContext. */
343
+ function mergeContext(base: CardContext, partial: Partial<CardContext>): CardContext {
344
+ return { ...base, ...partial };
345
+ }
@@ -0,0 +1,2 @@
1
+ export { mountRemoteCard } from './host-embed';
2
+ export type { MountRemoteCardOptions, RemoteCardHandle } from './host-embed';
@@ -0,0 +1,30 @@
1
+ export function assertOrigin(actual: string, expected: string): boolean {
2
+ return typeof actual === 'string' && actual.length > 0 && actual === expected;
3
+ }
4
+
5
+ /** Fail-closed precondition (H-F): the iframe must be cross-origin to the host so
6
+ * `allow-same-origin` can't reach host DOM, and `src` must be on the provider origin. */
7
+ export function assertCrossOrigin(src: string, providerOrigin: string, hostOrigin: string): void {
8
+ const srcOrigin = new URL(src).origin;
9
+ if (providerOrigin === hostOrigin) throw new Error('[kai-remote] providerOrigin must be cross-origin to the host');
10
+ if (srcOrigin === hostOrigin) throw new Error('[kai-remote] src must not be same-origin as the host');
11
+ if (srcOrigin !== providerOrigin) throw new Error('[kai-remote] src origin must equal providerOrigin');
12
+ }
13
+
14
+ /** Field-positive log redaction (H-H/H-P): keep structural keys, redact secrets +
15
+ * unknown leaf values by default. nonce + authToken are always redacted. */
16
+ const SAFE_KEYS = new Set(['protocol', 'version', 'dir', 'kind', 'mode', 'locale', 'edge', 'code', 'acceptedVersion', 'supportedVersions', 'cardId', 'type', 'id', 'height', 'target']);
17
+ const ALWAYS_REDACT = new Set(['authToken', 'nonce']);
18
+ export function redactFrame(value: unknown): unknown {
19
+ if (Array.isArray(value)) return value.map(redactFrame);
20
+ if (value && typeof value === 'object') {
21
+ const out: Record<string, unknown> = {};
22
+ for (const [k, v] of Object.entries(value)) {
23
+ if (ALWAYS_REDACT.has(k)) out[k] = '[redacted]';
24
+ else if (typeof v === 'object' && v !== null) out[k] = redactFrame(v);
25
+ else out[k] = SAFE_KEYS.has(k) ? v : '[redacted]';
26
+ }
27
+ return out;
28
+ }
29
+ return value;
30
+ }