@kitn.ai/ui 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/chat-thread.d.ts +9 -0
- package/dist/{create-tween-D3Y5qiij.js → create-tween-COnPwAQN.js} +1 -1
- package/dist/{create-tween-DGKY1FDY.js → create-tween-DmTUaXWE.js} +1 -1
- package/dist/{create-tween-paho5Rh-.js → create-tween-qs-jjvNA.js} +1 -1
- package/dist/custom-elements.json +43 -7
- package/dist/elements/agent-card.js +1 -1
- package/dist/elements/artifact.js +1 -1
- package/dist/elements/attachments.js +1 -1
- package/dist/elements/audio-visualizer.js +1 -1
- package/dist/elements/avatar.js +1 -1
- package/dist/elements/badge.js +1 -1
- package/dist/elements/button.js +1 -1
- package/dist/elements/card.js +1 -1
- package/dist/elements/cards.js +1 -1
- package/dist/elements/chain-of-thought.js +1 -1
- package/dist/elements/chat-scope-picker.js +1 -1
- package/dist/elements/chat-workspace.js +1 -1
- package/dist/elements/chat.js +1 -1
- package/dist/elements/checkpoint.js +1 -1
- package/dist/elements/choice.js +1 -1
- package/dist/elements/chunks/{Icon-C-2LvGDG.js → Icon-CHG5z5L7.js} +1 -1
- package/dist/elements/chunks/{action-icons-CaYoImEk.js → action-icons-BAtCEEgg.js} +1 -1
- package/dist/elements/chunks/arrow-left-CFPTeL1Y.js +1 -0
- package/dist/elements/chunks/{artifact-BBIscOxJ.js → artifact-fBZOE4SO.js} +1 -1
- package/dist/elements/chunks/{attachments-BB56zoji.js → attachments-DpYMe5VQ.js} +1 -1
- package/dist/elements/chunks/{audio-visualizer-BxOx4THz.js → audio-visualizer-DFwzAcuL.js} +1 -1
- package/dist/elements/chunks/{badge-SxLSZ19R.js → badge-D6zodm6E.js} +1 -1
- package/dist/elements/chunks/{button-CmkUkz6c.js → button-C42vrGvO.js} +1 -1
- package/dist/elements/chunks/{card-renderer-Db4ffkWv.js → card-renderer-DMZsSuvU.js} +1 -1
- package/dist/elements/chunks/{chat-thread-B3Ks-cKr.js → chat-thread-CNNEhol-.js} +1 -1
- package/dist/elements/chunks/check-7Q3K-iXE.js +1 -0
- package/dist/elements/chunks/chevron-down-CpNYLn7E.js +1 -0
- package/dist/elements/chunks/chevron-right-DrH85Fha.js +1 -0
- package/dist/elements/chunks/{choice-card-Q9cYYDyM.js → choice-card-YoWjpsFV.js} +1 -1
- package/dist/elements/chunks/circle-C9KRjKYv.js +1 -0
- package/dist/elements/chunks/circle-check-Dmi7Y4zw.js +1 -0
- package/dist/elements/chunks/{circle-x-D0MEfxOM.js → circle-x-QIxXda6k.js} +1 -1
- package/dist/elements/chunks/{code-block-C8VzAraE.js → code-block-CqeugReY.js} +1 -1
- package/dist/elements/chunks/{collapsible-Cvj61obE.js → collapsible-sTv-_5lF.js} +1 -1
- package/dist/elements/chunks/{composer-D86ztyL0.js → composer-ldQyeVEB.js} +1 -1
- package/dist/elements/chunks/{confirm-card-BdyDAGLw.js → confirm-card-C0RNOwfn.js} +1 -1
- package/dist/elements/chunks/context-5qgypNHC.js +1 -0
- package/dist/elements/chunks/conversation-list-DLzOacXz.js +1 -0
- package/dist/elements/chunks/{create-tween-C_A_B638.js → create-tween-V4TXZ93X.js} +1 -1
- package/dist/elements/chunks/default-input-DeeRSDHc.js +1 -0
- package/dist/elements/chunks/{define-BzPnNsXg.js → define-lE-QvQFn.js} +1 -1
- package/dist/elements/chunks/{disclosure-DVKsoc1F.js → disclosure-DZSa9jog.js} +1 -1
- package/dist/elements/chunks/{download-BjA54IMJ.js → download-B2ZmnGqW.js} +1 -1
- package/dist/elements/chunks/{dropdown-BjVVHWXH.js → dropdown-R3B7cSRE.js} +1 -1
- package/dist/elements/chunks/{ellipsis-BNedmqfi.js → ellipsis-BmxceY4l.js} +1 -1
- package/dist/elements/chunks/{embed-DQsl4XEq.js → embed-BVrtTZPo.js} +1 -1
- package/dist/elements/chunks/{external-link-Di-dw2CU.js → external-link-BbdR7YO7.js} +1 -1
- package/dist/elements/chunks/{file-text-B3sAODCV.js → file-text-D7dijo1A.js} +1 -1
- package/dist/elements/chunks/{file-tree-Dgto-bMn.js → file-tree-0upJ5jeb.js} +1 -1
- package/dist/elements/chunks/{folder-D6rMadFV.js → folder-D8z58t7B.js} +1 -1
- package/dist/elements/chunks/{form-CILxvhQ7.js → form-p5BKJNw5.js} +1 -1
- package/dist/elements/chunks/{hover-card-BtiF4XiU.js → hover-card-CN9s2md5.js} +1 -1
- package/dist/elements/chunks/{icon-DKkmze3y.js → icon-CNiKP37E.js} +1 -1
- package/dist/elements/chunks/{info-CWRjZkuy.js → info-BSnuJiHI.js} +1 -1
- package/dist/elements/chunks/{input--QJBK6BA.js → input-BauDmzxX.js} +1 -1
- package/dist/elements/chunks/{kbd-C3zuKh8R.js → kbd-BlHJaP9b.js} +1 -1
- package/dist/elements/chunks/{link-BL5JA7Q0.js → link-DEi5iUWC.js} +1 -1
- package/dist/elements/chunks/{link-preview-CU37bbXh.js → link-preview-BaHyM6W4.js} +1 -1
- package/dist/elements/chunks/{loader-XmpyxyRH.js → loader-CmJG_Fkq.js} +1 -1
- package/dist/elements/chunks/{markdown-3Gv5UZ_o.js → markdown-D2jaMpCN.js} +1 -1
- package/dist/elements/chunks/{message-X5NZbq-W.js → message-C5UfXZ2Z.js} +1 -1
- package/dist/elements/chunks/{message-CnBA2OUt.js → message-CXWLmmLn.js} +1 -1
- package/dist/elements/chunks/message-square-hXylwsy0.js +1 -0
- package/dist/elements/chunks/{minimize-2-DwyrUTcM.js → minimize-2-C9ytZFMU.js} +1 -1
- package/dist/elements/chunks/model-switcher-CivGfD7-.js +1 -0
- package/dist/elements/chunks/{overlay-Cepd-KcS.js → overlay-DJL_sbOm.js} +1 -1
- package/dist/elements/chunks/{paperclip-CdwKjrlu.js → paperclip-CFaB8naM.js} +1 -1
- package/dist/elements/chunks/{progress-bar-DMVXBD3b.js → progress-bar-Br5SksfR.js} +1 -1
- package/dist/elements/chunks/{prompt-suggestion-Di_WZE_5.js → prompt-suggestion-CMIvtAhU.js} +1 -1
- package/dist/elements/chunks/{reasoning-D0jMch5u.js → reasoning-DpZcNzLq.js} +1 -1
- package/dist/elements/chunks/{resizable-1znbL1Gv.js → resizable-Q8i3T9a_.js} +1 -1
- package/dist/elements/chunks/{rotate-cw-DVhhgx8n.js → rotate-cw-B5IHCBFV.js} +1 -1
- package/dist/elements/chunks/{scroll-area-Bs4bFS1P.js → scroll-area-eLeydBje.js} +1 -1
- package/dist/elements/chunks/{scroll-button-Bet5NLc4.js → scroll-button-DWbz0yA1.js} +1 -1
- package/dist/elements/chunks/{separator-C5Gh8qGC.js → separator-Xic78AD_.js} +1 -1
- package/dist/elements/chunks/{settings-5vmEFg0K.js → settings-B8H51pAx.js} +1 -1
- package/dist/elements/chunks/{settings-group-BWLB38kz.js → settings-group-CsG9eYLe.js} +1 -1
- package/dist/elements/chunks/{share-BZ3G_DaB.js → share-CNTx0z__.js} +1 -1
- package/dist/elements/chunks/{skeleton-DafPJQTp.js → skeleton-CLlHTcMI.js} +1 -1
- package/dist/elements/chunks/{source-D_qj_6uJ.js → source-BoVcU5ok.js} +1 -1
- package/dist/elements/chunks/{star-Cuv7j2IE.js → star-CxQVTVhI.js} +1 -1
- package/dist/elements/chunks/{store-C1StPHfW.js → store-CLtS5CUG.js} +1 -1
- package/dist/elements/chunks/{tasks-card-EyoBZfpA.js → tasks-card-CzlkLmUt.js} +1 -1
- package/dist/elements/chunks/{text-shimmer-D9gRSh34.js → text-shimmer-D5NhpIQ1.js} +1 -1
- package/dist/elements/chunks/{textarea-DuhtHfcm.js → textarea-x5Uxsmtm.js} +1 -1
- package/dist/elements/chunks/{thumbs-up-B9hwv43w.js → thumbs-up-UobcJnx9.js} +1 -1
- package/dist/elements/chunks/{toast-store-Bm3DUEDe.js → toast-store-BOfs9eXq.js} +1 -1
- package/dist/elements/chunks/{tool-CDtW7L0z.js → tool-Be6Dh_dt.js} +1 -1
- package/dist/elements/chunks/{tooltip-eB226mGK.js → tooltip-RxMxyCLF.js} +1 -1
- package/dist/elements/chunks/{triangle-alert-C1SNzTqK.js → triangle-alert-C4yxH2XN.js} +1 -1
- package/dist/elements/chunks/{use-card-resolution-YZh2MvI5.js → use-card-resolution-jva9ehH6.js} +1 -1
- package/dist/elements/chunks/{variant-aurora-Cj1F7D0l.js → variant-aurora-Dx4VRcjS.js} +1 -1
- package/dist/elements/chunks/{variant-custom-T9gpt7QE.js → variant-custom-Y1ZTWgEa.js} +1 -1
- package/dist/elements/chunks/{variant-wave-CxU9z-TE.js → variant-wave-B9gml9Cg.js} +1 -1
- package/dist/elements/chunks/{video-CK4R7Rus.js → video-CrPHsytT.js} +1 -1
- package/dist/elements/chunks/x-_-OPHept.js +1 -0
- package/dist/elements/coachmark.js +1 -1
- package/dist/elements/code-block.js +1 -1
- package/dist/elements/command.js +1 -1
- package/dist/elements/compare.js +1 -1
- package/dist/elements/composer.js +1 -1
- package/dist/elements/confirm-card.js +1 -1
- package/dist/elements/context-meter.js +1 -1
- package/dist/elements/conversation-list.js +1 -1
- package/dist/elements/default-input.d.ts +27 -0
- package/dist/elements/dialog.js +1 -1
- package/dist/elements/editable-label.js +1 -1
- package/dist/elements/embed.js +1 -1
- package/dist/elements/empty.js +1 -1
- package/dist/elements/feedback-bar.js +1 -1
- package/dist/elements/file-tree.js +1 -1
- package/dist/elements/file-upload.js +1 -1
- package/dist/elements/form.js +1 -1
- package/dist/elements/hover-card.js +1 -1
- package/dist/elements/icon.js +1 -1
- package/dist/elements/image.js +1 -1
- package/dist/elements/input.js +1 -1
- package/dist/elements/kbd.js +1 -1
- package/dist/elements/link-preview.js +1 -1
- package/dist/elements/loader.js +1 -1
- package/dist/elements/markdown.js +1 -1
- package/dist/elements/menu.js +1 -1
- package/dist/elements/message-skills.js +1 -1
- package/dist/elements/message.js +1 -1
- package/dist/elements/model-switcher.js +1 -1
- package/dist/elements/nav.js +1 -1
- package/dist/elements/notice.js +1 -1
- package/dist/elements/pane-group.js +1 -1
- package/dist/elements/pane.js +1 -1
- package/dist/elements/popover.js +1 -1
- package/dist/elements/progress-bar.js +1 -1
- package/dist/elements/prompt-dock.js +1 -1
- package/dist/elements/prompt-input.js +1 -1
- package/dist/elements/prompt-suggestions.js +1 -1
- package/dist/elements/reasoning.js +1 -1
- package/dist/elements/remote.js +1 -1
- package/dist/elements/resizable.js +1 -1
- package/dist/elements/response-stream.js +2 -2
- package/dist/elements/screen.js +1 -1
- package/dist/elements/scroll-area.js +1 -1
- package/dist/elements/scroll-button.js +1 -1
- package/dist/elements/search.js +1 -1
- package/dist/elements/segmented.js +1 -1
- package/dist/elements/separator.js +1 -1
- package/dist/elements/setting-item.js +1 -1
- package/dist/elements/settings-group.js +1 -1
- package/dist/elements/skeleton.js +1 -1
- package/dist/elements/slot-text.d.ts +51 -0
- package/dist/elements/slots.d.ts +5 -0
- package/dist/elements/source.js +1 -1
- package/dist/elements/status.js +1 -1
- package/dist/elements/switch.js +1 -1
- package/dist/elements/tabs.js +1 -1
- package/dist/elements/tasks.js +1 -1
- package/dist/elements/text-shimmer.js +1 -1
- package/dist/elements/thinking-bar.js +1 -1
- package/dist/elements/thread.js +1 -1
- package/dist/elements/toast.js +1 -1
- package/dist/elements/tool.js +1 -1
- package/dist/elements/tooltip.js +1 -1
- package/dist/elements/voice-input.js +1 -1
- package/dist/elements/voice-output.js +1 -1
- package/dist/elements.d.ts +12 -6
- package/dist/index.js +1535 -1487
- package/dist/index.server.js +2330 -2296
- package/dist/kai.es.js +1 -1
- package/dist/llms/llms-full.txt +12 -3
- package/dist/mcp.es.js +809 -75
- package/dist/react/index.d.ts +13 -3
- package/dist/react.js +14 -14
- package/dist/register-impl-AxIlqjo7.js +145 -0
- package/dist/{solid-BCJUQqwS.js → solid-BGYSJyHG.js} +4901 -4691
- package/dist/{solid-BdENBcEq.js → solid-Dt2nEwfl.js} +4406 -4320
- package/dist/solid.js +1 -1
- package/dist/solid.server.js +1 -1
- package/dist/{variant-aurora-DOb_o4pC.js → variant-aurora-B44X69AG.js} +1 -1
- package/dist/{variant-aurora-BKHHiOgL.js → variant-aurora-BRhV8Flk.js} +2 -2
- package/dist/{variant-aurora-1wMcXRUS.js → variant-aurora-Ds5ou9BH.js} +2 -2
- package/dist/{variant-custom-DWSSgmd2.js → variant-custom-B20FhtYj.js} +2 -2
- package/dist/{variant-custom-CJGB0Kbw.js → variant-custom-BvA1Schl.js} +1 -1
- package/dist/{variant-custom-Co0CkXJF.js → variant-custom-D6Rtvneb.js} +2 -2
- package/dist/{variant-wave-CEODq-9q.js → variant-wave-BUQtkryq.js} +1 -1
- package/dist/{variant-wave-C3-45cL6.js → variant-wave-BVYnqy9L.js} +2 -2
- package/dist/{variant-wave-jdXBnCUZ.js → variant-wave-DXC6_LcH.js} +2 -2
- package/dist/wire/encode.d.ts +79 -6
- package/dist/wire/files.d.ts +116 -0
- package/dist/wire/index.d.ts +3 -1
- package/dist/wire/media-types.d.ts +106 -0
- package/dist/wire.js +596 -368
- package/frameworks/react/index.tsx +9 -5
- package/llms-full.txt +12 -3
- package/package.json +2 -1
- package/src/agent-tooling/archetypes.ts +43 -12
- package/src/agent-tooling/integrations/anthropic.ts +29 -5
- package/src/agent-tooling/integrations/cloudflare.ts +5 -0
- package/src/agent-tooling/integrations/langgraph.ts +13 -0
- package/src/agent-tooling/integrations/mastra.ts +46 -5
- package/src/agent-tooling/integrations/mock.ts +4 -0
- package/src/agent-tooling/integrations/ollama.ts +7 -0
- package/src/agent-tooling/integrations/openai.ts +2 -0
- package/src/agent-tooling/integrations/openrouter.ts +2 -0
- package/src/agent-tooling/integrations/pi.ts +26 -1
- package/src/agent-tooling/integrations/pydantic-ai.ts +37 -3
- package/src/agent-tooling/integrations/vercel-ai-sdk.ts +42 -6
- package/src/agent-tooling/mcp/tools/scaffold.ts +853 -32
- package/src/agent-tooling/registry.ts +57 -0
- package/src/agent-tooling/types.ts +132 -0
- package/src/components/chat-thread.tsx +10 -1
- package/src/components/context.tsx +18 -12
- package/src/components/conversation-item.tsx +6 -0
- package/src/components/model-switcher.tsx +38 -8
- package/src/components/toast.tsx +6 -1
- package/src/elements/button.tsx +26 -3
- package/src/elements/chat.tsx +28 -2
- package/src/elements/default-input.tsx +128 -5
- package/src/elements/element-meta.json +30 -4
- package/src/elements/element-types.d.ts +12 -6
- package/src/elements/menu.tsx +30 -3
- package/src/elements/slot-text.ts +73 -0
- package/src/elements/slots.ts +10 -0
- package/src/index.ts +9 -5
- package/src/wire/encode.ts +229 -14
- package/src/wire/files.ts +342 -0
- package/src/wire/index.ts +21 -0
- package/src/wire/media-types.ts +344 -0
- package/dist/elements/chunks/arrow-left-Bb46H7w9.js +0 -1
- package/dist/elements/chunks/check-C8desc_h.js +0 -1
- package/dist/elements/chunks/chevron-down-DKtc2HNB.js +0 -1
- package/dist/elements/chunks/chevron-right-Dw4GPhwL.js +0 -1
- package/dist/elements/chunks/circle-BeokBsvB.js +0 -1
- package/dist/elements/chunks/circle-check-wJSqt859.js +0 -1
- package/dist/elements/chunks/context-WHDl-xSx.js +0 -1
- package/dist/elements/chunks/conversation-list-CdR14_Y4.js +0 -1
- package/dist/elements/chunks/default-input-CSZqUM81.js +0 -1
- package/dist/elements/chunks/message-square-CnpAm_P0.js +0 -1
- package/dist/elements/chunks/model-switcher-CclaJEHt.js +0 -1
- package/dist/elements/chunks/x-D1qx0aLd.js +0 -1
- package/dist/register-impl-bzrZ44ft.js +0 -145
|
@@ -310,6 +310,20 @@ function realStreamBody(opts: {
|
|
|
310
310
|
valueSource?: string;
|
|
311
311
|
/** lines emitted right after the value is read and guarded */
|
|
312
312
|
afterValue?: string[];
|
|
313
|
+
/**
|
|
314
|
+
* The attachments capability: an EXPRESSION producing this turn's staged
|
|
315
|
+
* attachments, which become `file` parts ahead of the message's text part.
|
|
316
|
+
*
|
|
317
|
+
* An expression rather than a flag because every framework holds the list
|
|
318
|
+
* somewhere different (a module-scope `let`, a signal, a rune), and it is
|
|
319
|
+
* evaluated exactly once here — in `afterValue` each renderer has already
|
|
320
|
+
* captured it into a local, because the same lines then CLEAR the staging list
|
|
321
|
+
* and a lazily-read accessor would come back empty.
|
|
322
|
+
*
|
|
323
|
+
* It sits in `parts` before the text so the thread renders the files above the
|
|
324
|
+
* message body, which is where a composer's own attachment chips sit.
|
|
325
|
+
*/
|
|
326
|
+
filesExpr?: string;
|
|
313
327
|
/**
|
|
314
328
|
* The `mock` integration. Swaps ONLY the source of the stream — the canned
|
|
315
329
|
* responder instead of `fetch('/api/chat')` — and leaves every other line
|
|
@@ -319,14 +333,23 @@ function realStreamBody(opts: {
|
|
|
319
333
|
}): string {
|
|
320
334
|
const {
|
|
321
335
|
pad, read, commitSet, setterAdapter, setLoading, bodyPayload, strictRoles = false, toolLoop, thread,
|
|
322
|
-
cards = false, valueSource = 'e.detail.value', afterValue = [], mock = false,
|
|
336
|
+
cards = false, valueSource = 'e.detail.value', afterValue = [], mock = false, filesExpr,
|
|
323
337
|
} = opts;
|
|
324
338
|
const asConst = strictRoles ? ' as const' : '';
|
|
325
339
|
// Under strict TS an un-annotated array literal widens the part's `type` to
|
|
326
340
|
// `string`, so the later commit fails TS2322. Plain-JS contexts (html) have no
|
|
327
341
|
// type to annotate with.
|
|
328
342
|
const historyType = strictRoles ? ': ChatMessage[]' : '';
|
|
329
|
-
|
|
343
|
+
// `type: 'file' as const` even though every call site here is contextually
|
|
344
|
+
// typed by a ChatMessage[] target: the spread's element type is computed from
|
|
345
|
+
// the callback's own return, BEFORE the surrounding array literal's contextual
|
|
346
|
+
// type reaches it, so without the assertion `type` widens to string and the
|
|
347
|
+
// whole assignment fails TS2322.
|
|
348
|
+
const fileParts = filesExpr
|
|
349
|
+
? `...${filesExpr}.map((attachment) => ({ type: 'file'${asConst}, attachment })), `
|
|
350
|
+
: '';
|
|
351
|
+
const userMessage =
|
|
352
|
+
`{ id: crypto.randomUUID(), role: 'user'${asConst}, parts: [${fileParts}{ type: 'text', text: value }] }`;
|
|
330
353
|
|
|
331
354
|
const open = toolLoop
|
|
332
355
|
? thread.open({ pad, userMessage, typed: strictRoles })
|
|
@@ -631,7 +654,7 @@ function assertCardToolFormat(integration: Integration): 'openai' | 'anthropic'
|
|
|
631
654
|
*
|
|
632
655
|
* KEYED ON A COMPONENTS LIST, not an archetype id, for the same reason
|
|
633
656
|
* `renderSurface` is. A guard that took an archetype id could only ever check the
|
|
634
|
-
*
|
|
657
|
+
* seven presets, so the surfaces the feature multi-select makes reachable — the ones
|
|
635
658
|
* with no preset — would emit cards with nothing asserting they did. The archetype
|
|
636
659
|
* axis was also the narrower one: `getArchetype` returns `undefined` for anything
|
|
637
660
|
* not in the catalog, so the guard could not have been pointed at a new surface
|
|
@@ -691,13 +714,29 @@ function wireImportLines(opts: {
|
|
|
691
714
|
cardTools?: boolean;
|
|
692
715
|
/** the `mock` integration → import the shared responder, not a fetch encoder */
|
|
693
716
|
mock?: boolean;
|
|
717
|
+
/**
|
|
718
|
+
* The attachments capability → `type AttachmentData`.
|
|
719
|
+
*
|
|
720
|
+
* The kit exports it from `@kitn.ai/ui/state` precisely because it is the type
|
|
721
|
+
* BOTH ends of this feature hold: `<kai-attachments>.items` and the
|
|
722
|
+
* `{ type: 'file', attachment }` MessagePart. Naming it is better than deriving
|
|
723
|
+
* a structural equivalent per framework, which is what the first cut did — a
|
|
724
|
+
* `NonNullable<AttachmentsProps['items']>[number]` is the same type spelled so
|
|
725
|
+
* that only React can spell it.
|
|
726
|
+
*
|
|
727
|
+
* Independent of `typed` on purpose: `solid` takes `ChatMessage` from
|
|
728
|
+
* `@kitn.ai/ui` alongside its components (so `typed` is false there) and still
|
|
729
|
+
* needs this one from the state entry.
|
|
730
|
+
*/
|
|
731
|
+
attachments?: boolean;
|
|
694
732
|
}): string[] {
|
|
695
|
-
const { pad = '', typed, toolLoop = false, setMessagesType = false, cards = false, cardTools: emitsCardTools = false, mock = false } = opts;
|
|
733
|
+
const { pad = '', typed, toolLoop = false, setMessagesType = false, cards = false, cardTools: emitsCardTools = false, mock = false, attachments = false } = opts;
|
|
696
734
|
const stateNames = [
|
|
697
735
|
'createAssistantStream',
|
|
698
736
|
// The mock's canned reply comes from the kit, not from a copy pasted into
|
|
699
737
|
// this file. One implementation, shared with create-kai and the starters.
|
|
700
738
|
...(mock ? ['createMockResponder'] : []),
|
|
739
|
+
...(attachments ? ['type AttachmentData'] : []),
|
|
701
740
|
...(typed ? ['type ChatMessage'] : []),
|
|
702
741
|
...(typed && setMessagesType ? ['type SetMessages'] : []),
|
|
703
742
|
].join(', ');
|
|
@@ -1014,6 +1053,210 @@ function isWorkspace(components: readonly string[]): boolean {
|
|
|
1014
1053
|
return components.includes('kai-resizable') && components.includes('kai-artifact');
|
|
1015
1054
|
}
|
|
1016
1055
|
|
|
1056
|
+
// ── attachments ───────────────────────────────────────────────────────────────
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* The attachment staging pair. Structural in the same sense the workspace tags
|
|
1060
|
+
* are: neither is emitted as a bare sibling with nothing wired to it, because
|
|
1061
|
+
* neither does anything on its own.
|
|
1062
|
+
*
|
|
1063
|
+
* `<kai-file-upload>` is a dropzone that only EMITS (`kai-files-added`, carrying
|
|
1064
|
+
* `File[]`); it holds no list. `<kai-attachments>` only RENDERS what its `items`
|
|
1065
|
+
* property is set to, and an unset `items` draws the empty state. So the two
|
|
1066
|
+
* bare tags the generic companion fall-through used to emit were a dropzone
|
|
1067
|
+
* whose files went nowhere and a list that could never fill.
|
|
1068
|
+
*/
|
|
1069
|
+
const ATTACHMENT_TAGS = new Set(['kai-file-upload', 'kai-attachments']);
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* True when the surface stages attachments — BOTH tags, for the same reason
|
|
1073
|
+
* `isWorkspace` needs both.
|
|
1074
|
+
*
|
|
1075
|
+
* They are one capability group in `archetypes.ts`, so in practice a request
|
|
1076
|
+
* carries both or neither. Requiring both here is what keeps a hand-built
|
|
1077
|
+
* `components: ['kai-chat', 'kai-attachments']` from reaching the wiring below
|
|
1078
|
+
* and emitting a `kai-remove` listener for a list nothing can add to.
|
|
1079
|
+
*/
|
|
1080
|
+
function hasAttachments(components: readonly string[]): boolean {
|
|
1081
|
+
return components.includes('kai-file-upload') && components.includes('kai-attachments');
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* What the scaffolder INTENDS to emit for one components list, so a guard can
|
|
1086
|
+
* check the seven renderers against the decision instead of restating it.
|
|
1087
|
+
*
|
|
1088
|
+
* The sibling of `cardEmitPlan`, and exported for the same reason:
|
|
1089
|
+
* `attachmentStagingCheck` in scripts/verify-scaffold-compiles.mjs would
|
|
1090
|
+
* otherwise re-derive "both attachment tags are present" itself, and a guard that
|
|
1091
|
+
* re-derives its own expectation stops being a check of anything the day the two
|
|
1092
|
+
* derivations disagree — it just agrees with itself.
|
|
1093
|
+
*
|
|
1094
|
+
* Unlike `cardEmitPlan` this takes no integration, and that is a real difference
|
|
1095
|
+
* rather than an omission: staging a file needs no model, no tools array and no
|
|
1096
|
+
* route, so `mock` emits exactly what `openai` does. What the wire then makes of
|
|
1097
|
+
* the files is decided by the encoders and by the media-type declaration behind
|
|
1098
|
+
* them, which is the same answer for every integration (see
|
|
1099
|
+
* `ATTACHMENT_WIRE_NOTE`).
|
|
1100
|
+
*/
|
|
1101
|
+
export function attachmentEmitPlan(components: readonly string[]): { staging: boolean } {
|
|
1102
|
+
return { staging: hasAttachments(components) };
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* What the emitted attachment surface does, and — the part that matters — what
|
|
1107
|
+
* it does not.
|
|
1108
|
+
*
|
|
1109
|
+
* It stages AND it sends. `toOpenAIMessages` / `toAnthropicMessages` encode
|
|
1110
|
+
* `file` parts into the content blocks each API takes, so a staged file reaches
|
|
1111
|
+
* the model. That is worth stating because it was false for a long time and the
|
|
1112
|
+
* failure was silent: before #186 an attachment-only turn encoded to nothing at
|
|
1113
|
+
* all and was dropped from the request, so a scaffold could put a paperclip on
|
|
1114
|
+
* screen, accept a PDF, render it in the thread, and send the model a message
|
|
1115
|
+
* that never mentioned it.
|
|
1116
|
+
*
|
|
1117
|
+
* WHAT IT STILL DOES NOT DO. Both remaining limits are facts about the APIs, not
|
|
1118
|
+
* gaps in this scaffold, which is why the emitted note can state them plainly:
|
|
1119
|
+
* · Neither API has an arbitrary-file content block, so a media type outside
|
|
1120
|
+
* the kit's set — a `.zip` is the one people try — has no representation on
|
|
1121
|
+
* either wire and throws at encode time rather than going missing.
|
|
1122
|
+
* · A REMOTE text file is refused. Text has to ride as text CONTENT (no wire
|
|
1123
|
+
* has a URL form for it), so inlining one would mean fetching it, and
|
|
1124
|
+
* `wire/` does no I/O by design. Stage a `data:` URI instead.
|
|
1125
|
+
* A file the browser could not name is NOT on that list any more: it is settled
|
|
1126
|
+
* by decoding its bytes, so the `.rs` and `.toml` files a coding chat is full of
|
|
1127
|
+
* go through even though Chrome hands them over with no media type at all.
|
|
1128
|
+
*
|
|
1129
|
+
* ★ THE EMITTED NOTE MUST NOT NAME MEDIA TYPES, and that is the whole reason it
|
|
1130
|
+
* reads the way it does. It lands in a user's repo, where it outlives any
|
|
1131
|
+
* limitation it describes and where nothing can ever check it — a second copy of
|
|
1132
|
+
* the capability list, at the worst possible layer. It points at
|
|
1133
|
+
* `encodableMediaTypes()` instead, which is public precisely so that nobody has
|
|
1134
|
+
* to hardcode the set. A guard in `scaffold.test.ts` fails if a media type
|
|
1135
|
+
* creeps back into it.
|
|
1136
|
+
*
|
|
1137
|
+
* This very comment has been wrong before, in the way that matters most: #186
|
|
1138
|
+
* corrected the emitted string and left the explanation above it teaching the
|
|
1139
|
+
* reverse, so the scaffolder's own documentation said files never reach the
|
|
1140
|
+
* model while the code it emitted said they do. Anything written here that a
|
|
1141
|
+
* script cannot re-derive is a claim with a shelf life.
|
|
1142
|
+
*/
|
|
1143
|
+
export const ATTACHMENT_WIRE_NOTE = [
|
|
1144
|
+
'// The staged files ride along on the message as `file` parts, so they RENDER',
|
|
1145
|
+
'// in the thread AND reach the model: toOpenAIMessages / toAnthropicMessages',
|
|
1146
|
+
'// encode each one into the content block its API takes.',
|
|
1147
|
+
'// Which is why toAttachment below stages a `data:` URI rather than a',
|
|
1148
|
+
'// URL.createObjectURL blob: an object URL previews fine and resolves ONLY',
|
|
1149
|
+
'// inside this tab, so the encoder rejects it rather than send an address the',
|
|
1150
|
+
'// provider cannot fetch.',
|
|
1151
|
+
'// What can be sent is not a list worth copying into a comment -- it moves, and',
|
|
1152
|
+
'// a copy cannot. `encodableMediaTypes()` from "@kitn.ai/ui/wire" IS the set,',
|
|
1153
|
+
'// and `resolveMediaPolicy().decide(mediaType)` answers for one file. Anything',
|
|
1154
|
+
'// outside it throws at encode time instead of going missing from the request.',
|
|
1155
|
+
'// A file the browser could not name (a .rs, a .toml) is settled by decoding',
|
|
1156
|
+
'// its bytes, never by its extension.',
|
|
1157
|
+
];
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* The `File` -> `AttachmentData` conversion, emitted as a real function.
|
|
1161
|
+
*
|
|
1162
|
+
* `<kai-file-upload>` hands over `File` objects and `<kai-attachments>` renders
|
|
1163
|
+
* `AttachmentData`, so something has to bridge them and it may as well be code
|
|
1164
|
+
* the consumer can edit. The object URL is what makes an image preview a real
|
|
1165
|
+
* thumbnail rather than a generic icon; it is deliberately not revoked here,
|
|
1166
|
+
* and the comment says why rather than leaving a silent leak.
|
|
1167
|
+
*
|
|
1168
|
+
* The parameter type is derived from the property it feeds
|
|
1169
|
+
* (`KaiAttachmentsElement['items']` / the wrapper's `items` prop) at each call
|
|
1170
|
+
* site rather than importing `AttachmentData`, for the reason the html target
|
|
1171
|
+
* already derives its message type from `KaiChatElement['messages']`: a type
|
|
1172
|
+
* read off the assignment target cannot drift out of step with it.
|
|
1173
|
+
*/
|
|
1174
|
+
function fileToAttachmentLines(pad: string, typeName: string): string[] {
|
|
1175
|
+
return [
|
|
1176
|
+
`${pad}/** <kai-file-upload> hands over File objects; <kai-attachments> renders these. */`,
|
|
1177
|
+
`${pad}async function toAttachment(file: File): Promise<${typeName}> {`,
|
|
1178
|
+
`${pad} // A data: URI, NOT URL.createObjectURL. Both preview identically, but an`,
|
|
1179
|
+
`${pad} // object URL resolves only inside this tab, so it can never reach a model —`,
|
|
1180
|
+
`${pad} // the encoder rejects one rather than send an address the provider cannot`,
|
|
1181
|
+
`${pad} // fetch. Reading is async, which is why this function is.`,
|
|
1182
|
+
`${pad} const url = await new Promise<string>((resolve, reject) => {`,
|
|
1183
|
+
`${pad} const reader = new FileReader();`,
|
|
1184
|
+
`${pad} reader.onload = () => resolve(String(reader.result));`,
|
|
1185
|
+
`${pad} reader.onerror = () => reject(reader.error ?? new Error('Could not read ' + file.name));`,
|
|
1186
|
+
`${pad} reader.readAsDataURL(file);`,
|
|
1187
|
+
`${pad} });`,
|
|
1188
|
+
`${pad} return {`,
|
|
1189
|
+
`${pad} id: crypto.randomUUID(),`,
|
|
1190
|
+
`${pad} type: 'file',`,
|
|
1191
|
+
`${pad} filename: file.name,`,
|
|
1192
|
+
`${pad} // '' for an extensionless file, and an empty mediaType would render as a`,
|
|
1193
|
+
`${pad} // blank detail line rather than falling back to the generic icon.`,
|
|
1194
|
+
`${pad} mediaType: file.type || undefined,`,
|
|
1195
|
+
`${pad} url,`,
|
|
1196
|
+
`${pad} };`,
|
|
1197
|
+
`${pad}}`,
|
|
1198
|
+
];
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/**
|
|
1202
|
+
* Why the submit handler merges TWO sources of staged files.
|
|
1203
|
+
*
|
|
1204
|
+
* `<kai-chat>` renders its own paperclip unconditionally — `ChatThread` always
|
|
1205
|
+
* passes `onAttachmentsChange` to the composer, and there is no prop on the
|
|
1206
|
+
* element that turns it off — so every scaffold ALREADY has an attach button,
|
|
1207
|
+
* and every scaffold before this one dropped whatever was attached through it on
|
|
1208
|
+
* the floor: `kai-submit` carries `{ value, attachments }` and every emitted
|
|
1209
|
+
* handler read only `.value`.
|
|
1210
|
+
*
|
|
1211
|
+
* So reading only the dropzone would leave that bug in place on the one surface
|
|
1212
|
+
* that is supposed to be about attachments. Both are folded in.
|
|
1213
|
+
*/
|
|
1214
|
+
const ATTACHMENT_MERGE_NOTE = [
|
|
1215
|
+
"// <kai-chat> renders its OWN paperclip (there is no prop that hides it), and its",
|
|
1216
|
+
'// staged files arrive on this event as detail.attachments. The dropzone above is',
|
|
1217
|
+
'// the second source. Both are folded in — reading either one alone silently',
|
|
1218
|
+
'// drops files the user really attached.',
|
|
1219
|
+
];
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* The lines every framework emits at the TOP of a submit, before the message is
|
|
1223
|
+
* built: capture this turn's attachments into `files`, then empty the staging
|
|
1224
|
+
* list.
|
|
1225
|
+
*
|
|
1226
|
+
* They go through `afterValue` rather than into the message expression because
|
|
1227
|
+
* the order is load-bearing and easy to get backwards — `filesExpr` is spliced
|
|
1228
|
+
* into `parts` AFTER these run, so the capture has to be into a local. Reading
|
|
1229
|
+
* the staging list lazily from `parts` would read it post-clear and attach
|
|
1230
|
+
* nothing, which is precisely the silent failure this whole capability is being
|
|
1231
|
+
* added to stop.
|
|
1232
|
+
*
|
|
1233
|
+
* `fromSubmitEvent` is false for exactly one target: `solid` renders the SolidJS
|
|
1234
|
+
* `PromptInput` primitives directly rather than `<kai-chat>`, so there is no
|
|
1235
|
+
* `kai-submit` event and no second source to merge — its dropzone is the only
|
|
1236
|
+
* one. Merging a `detail` that does not exist there would be a runtime TypeError
|
|
1237
|
+
* on every submit.
|
|
1238
|
+
*/
|
|
1239
|
+
function attachmentTurnLines(opts: {
|
|
1240
|
+
/** expression reading the dropzone's staged list */
|
|
1241
|
+
stagedExpr: string;
|
|
1242
|
+
/** statement(s) that empty the staging list (and re-render it) */
|
|
1243
|
+
clear: string[];
|
|
1244
|
+
/** the framework submits through `kai-submit`, so `e.detail.attachments` exists */
|
|
1245
|
+
fromSubmitEvent: boolean;
|
|
1246
|
+
}): string[] {
|
|
1247
|
+
const { stagedExpr, clear, fromSubmitEvent } = opts;
|
|
1248
|
+
return [
|
|
1249
|
+
...(fromSubmitEvent ? ATTACHMENT_MERGE_NOTE : [
|
|
1250
|
+
'// This target renders PromptInput directly rather than <kai-chat>, so the',
|
|
1251
|
+
'// dropzone is the only source of staged files — there is no kai-submit detail.',
|
|
1252
|
+
]),
|
|
1253
|
+
fromSubmitEvent
|
|
1254
|
+
? `const files = [...${stagedExpr}, ...e.detail.attachments];`
|
|
1255
|
+
: `const files = [...${stagedExpr}];`,
|
|
1256
|
+
...clear,
|
|
1257
|
+
];
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1017
1260
|
/**
|
|
1018
1261
|
* A sample assistant message showing embedded tool + reasoning.
|
|
1019
1262
|
*
|
|
@@ -1136,15 +1379,37 @@ interface RenderCtx {
|
|
|
1136
1379
|
*
|
|
1137
1380
|
* SCAF-14: workspace structural types (kai-resizable, kai-artifact) are emitted
|
|
1138
1381
|
* as a properly composed split layout — chat in one pane, artifact in the other.
|
|
1382
|
+
*
|
|
1383
|
+
* The attachment pair (kai-file-upload, kai-attachments) is filtered out of the
|
|
1384
|
+
* companion loop for the same reason the workspace pair is: it gets a composed
|
|
1385
|
+
* block of its own, above the chat, with ids the emitted module wires together.
|
|
1139
1386
|
*/
|
|
1140
1387
|
function componentTags(components: readonly string[], chatFill: string): string {
|
|
1141
1388
|
const companionTags = components.filter(
|
|
1142
|
-
(t) =>
|
|
1389
|
+
(t) =>
|
|
1390
|
+
t !== 'kai-chat' &&
|
|
1391
|
+
!MESSAGE_EMBEDDED_TAGS.has(t) &&
|
|
1392
|
+
!WORKSPACE_STRUCTURAL_TAGS.has(t) &&
|
|
1393
|
+
!(hasAttachments(components) && ATTACHMENT_TAGS.has(t)),
|
|
1143
1394
|
);
|
|
1144
1395
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
1145
1396
|
const hasStandaloneCompanions = companionTags.length > 0;
|
|
1146
1397
|
|
|
1147
1398
|
const lines: string[] = [];
|
|
1399
|
+
// The staging strip, ABOVE the thread: drop files in, see them as removable
|
|
1400
|
+
// chips, and they ride out on the next message. `flex: 0 0 auto` keeps it out
|
|
1401
|
+
// of the chat's `flex: 1` claim on the remaining height.
|
|
1402
|
+
if (hasAttachments(components)) {
|
|
1403
|
+
lines.push(
|
|
1404
|
+
` <!-- Drop files here to stage them for the NEXT message. src/main.ts wires`,
|
|
1405
|
+
` kai-files-added -> the staged list -> kai-attachments' items property. -->`,
|
|
1406
|
+
` <div style="flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem;">`,
|
|
1407
|
+
` <kai-file-upload id="upload" accept="image/*,application/pdf"></kai-file-upload>`,
|
|
1408
|
+
` <!-- items is a JS PROPERTY (arrays can't be attributes); 'removable' fires kai-remove. -->`,
|
|
1409
|
+
` <kai-attachments id="attachments" variant="inline" removable></kai-attachments>`,
|
|
1410
|
+
` </div>`,
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1148
1413
|
// SCAF-14: workspace is a structural/layout surface — emit a runnable split.
|
|
1149
1414
|
//
|
|
1150
1415
|
// This used to `return` here, which dropped every standalone companion on the
|
|
@@ -1229,6 +1494,49 @@ function componentTags(components: readonly string[], chatFill: string): string
|
|
|
1229
1494
|
function htmlModule(ctx: RenderCtx, components: readonly string[]): string {
|
|
1230
1495
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
1231
1496
|
const hasSources = components.includes('kai-sources');
|
|
1497
|
+
const attachments = hasAttachments(components);
|
|
1498
|
+
|
|
1499
|
+
// MODULE scope, like `model`/`runTool`: the staged list has to outlive `init()`
|
|
1500
|
+
// so the submit handler can read it, and `Staged` is used by the module-scope
|
|
1501
|
+
// `toAttachment` declaration below.
|
|
1502
|
+
const attachmentModuleLines = attachments
|
|
1503
|
+
? [
|
|
1504
|
+
...ATTACHMENT_WIRE_NOTE,
|
|
1505
|
+
`//`,
|
|
1506
|
+
`// AttachmentData is the kit's own type for a staged file — the same one`,
|
|
1507
|
+
`// <kai-attachments>.items holds and the same one a 'file' message part holds.`,
|
|
1508
|
+
`let staged: AttachmentData[] = [];`,
|
|
1509
|
+
``,
|
|
1510
|
+
...fileToAttachmentLines('', 'AttachmentData'),
|
|
1511
|
+
``,
|
|
1512
|
+
]
|
|
1513
|
+
: [];
|
|
1514
|
+
|
|
1515
|
+
const attachmentSetupLines = attachments
|
|
1516
|
+
? [
|
|
1517
|
+
` const uploadEl = document.getElementById('upload') as HTMLElement;`,
|
|
1518
|
+
` const attachmentsEl = document.getElementById('attachments') as KaiAttachmentsElement;`,
|
|
1519
|
+
` // Same upgrade rule as <kai-chat>: 'items' is an array, so it is a PROPERTY,`,
|
|
1520
|
+
` // and a property set before the element upgrades is dropped on upgrade.`,
|
|
1521
|
+
` await customElements.whenDefined('kai-attachments');`,
|
|
1522
|
+
` // A NEW array per write — same contract as chat.messages.`,
|
|
1523
|
+
` const showStaged = () => { attachmentsEl.items = [...staged]; };`,
|
|
1524
|
+
``,
|
|
1525
|
+
` uploadEl.addEventListener('kai-files-added', async (event: Event) => {`,
|
|
1526
|
+
` const { files } = (event as CustomEvent<{ files: File[] }>).detail;`,
|
|
1527
|
+
` // Read every file BEFORE appending: a per-file append would order the`,
|
|
1528
|
+
` // list by whichever finished reading first.`,
|
|
1529
|
+
` staged = [...staged, ...(await Promise.all(files.map(toAttachment)))];`,
|
|
1530
|
+
` showStaged();`,
|
|
1531
|
+
` });`,
|
|
1532
|
+
` attachmentsEl.addEventListener('kai-remove', (event: Event) => {`,
|
|
1533
|
+
` const { id } = (event as CustomEvent<{ id: string }>).detail;`,
|
|
1534
|
+
` staged = staged.filter((a) => a.id !== id);`,
|
|
1535
|
+
` showStaged();`,
|
|
1536
|
+
` });`,
|
|
1537
|
+
``,
|
|
1538
|
+
]
|
|
1539
|
+
: [];
|
|
1232
1540
|
|
|
1233
1541
|
// SCAF-9: the agentic archetype explains where tool + reasoning parts come
|
|
1234
1542
|
// from. It no longer SEEDS a fabricated turn — see `SAMPLE_AGENTIC_MESSAGE`.
|
|
@@ -1272,8 +1580,15 @@ function htmlModule(ctx: RenderCtx, components: readonly string[]): string {
|
|
|
1272
1580
|
|
|
1273
1581
|
// KaiSourcesElement only when a kai-sources companion is really declared: a
|
|
1274
1582
|
// stock vanilla-ts tsconfig sets noUnusedLocals, so an always-on import is a
|
|
1275
|
-
// build error on every other archetype.
|
|
1276
|
-
|
|
1583
|
+
// build error on every other archetype. KaiAttachmentsElement follows the same
|
|
1584
|
+
// rule — the `items =` assignment needs it. KaiFileUploadElement is deliberately
|
|
1585
|
+
// NOT imported: the dropzone is only ever listened to, and `as HTMLElement`
|
|
1586
|
+
// types addEventListener fully.
|
|
1587
|
+
const elementTypes = [
|
|
1588
|
+
'KaiChatElement',
|
|
1589
|
+
...(hasSources ? ['KaiSourcesElement'] : []),
|
|
1590
|
+
...(attachments ? ['KaiAttachmentsElement'] : []),
|
|
1591
|
+
].join(', ');
|
|
1277
1592
|
|
|
1278
1593
|
/**
|
|
1279
1594
|
* Same rule, applied to the kit's own `ChatMessage`.
|
|
@@ -1305,11 +1620,13 @@ function htmlModule(ctx: RenderCtx, components: readonly string[]): string {
|
|
|
1305
1620
|
cards: ctx.emitCards,
|
|
1306
1621
|
cardTools: ctx.cardProvider !== null,
|
|
1307
1622
|
mock: ctx.isMock,
|
|
1623
|
+
attachments,
|
|
1308
1624
|
}),
|
|
1309
1625
|
`import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
|
|
1310
1626
|
``,
|
|
1311
1627
|
...(ctx.isMock ? [...mockResponderInit(), ``] : []),
|
|
1312
1628
|
...modelLines,
|
|
1629
|
+
...attachmentModuleLines,
|
|
1313
1630
|
...cardsLines,
|
|
1314
1631
|
...toolsLines,
|
|
1315
1632
|
...runnerLines,
|
|
@@ -1326,14 +1643,21 @@ function htmlModule(ctx: RenderCtx, components: readonly string[]): string {
|
|
|
1326
1643
|
...cardPropLines,
|
|
1327
1644
|
...seedLines,
|
|
1328
1645
|
...sourcesSetupLines,
|
|
1646
|
+
...attachmentSetupLines,
|
|
1329
1647
|
];
|
|
1330
1648
|
|
|
1331
1649
|
// `Event`, not `CustomEvent`: addEventListener with a custom event name hands
|
|
1332
1650
|
// the listener a plain Event, so the narrowing happens in the body — the same
|
|
1333
1651
|
// shape renderAngular emits, for the same reason.
|
|
1652
|
+
//
|
|
1653
|
+
// `attachments` is on the detail whether or not this surface stages files —
|
|
1654
|
+
// <kai-chat> always sends both — but it is only NAMED in the cast where the
|
|
1655
|
+
// handler reads it, so every other scaffold keeps the narrower type it uses.
|
|
1334
1656
|
const listenerOpen = [
|
|
1335
1657
|
` chat.addEventListener('kai-submit', async (event: Event) => {`,
|
|
1336
|
-
|
|
1658
|
+
attachments
|
|
1659
|
+
? ` const e = event as CustomEvent<{ value: string; attachments: AttachmentData[] }>;`
|
|
1660
|
+
: ` const e = event as CustomEvent<{ value: string }>;`,
|
|
1337
1661
|
];
|
|
1338
1662
|
const footer = [
|
|
1339
1663
|
` });`,
|
|
@@ -1363,6 +1687,16 @@ function htmlModule(ctx: RenderCtx, components: readonly string[]): string {
|
|
|
1363
1687
|
'chat.messages ?? []',
|
|
1364
1688
|
),
|
|
1365
1689
|
mock: ctx.isMock,
|
|
1690
|
+
...(attachments
|
|
1691
|
+
? {
|
|
1692
|
+
filesExpr: 'files',
|
|
1693
|
+
afterValue: attachmentTurnLines({
|
|
1694
|
+
stagedExpr: 'staged',
|
|
1695
|
+
clear: ['staged = [];', 'showStaged();'],
|
|
1696
|
+
fromSubmitEvent: true,
|
|
1697
|
+
}),
|
|
1698
|
+
}
|
|
1699
|
+
: {}),
|
|
1366
1700
|
}),
|
|
1367
1701
|
...footer,
|
|
1368
1702
|
].join('\n');
|
|
@@ -1438,9 +1772,17 @@ function renderJsx(components: readonly string[], ctx: RenderCtx, framework: str
|
|
|
1438
1772
|
const wrapperNames = importTags.map(toPascalCase);
|
|
1439
1773
|
const importList = wrapperNames.join(', ');
|
|
1440
1774
|
|
|
1441
|
-
|
|
1775
|
+
const attachments = hasAttachments(components);
|
|
1776
|
+
|
|
1777
|
+
// SCAF-9: standalone companion tags (not kai-chat, not message-embedded, not
|
|
1778
|
+
// workspace-structural, and not the attachment pair — that gets a composed
|
|
1779
|
+
// block of its own with state behind it).
|
|
1442
1780
|
const standaloneCompanionTags = components.filter(
|
|
1443
|
-
(t) =>
|
|
1781
|
+
(t) =>
|
|
1782
|
+
t !== 'kai-chat' &&
|
|
1783
|
+
!MESSAGE_EMBEDDED_TAGS.has(t) &&
|
|
1784
|
+
!WORKSPACE_STRUCTURAL_TAGS.has(t) &&
|
|
1785
|
+
!(attachments && ATTACHMENT_TAGS.has(t)),
|
|
1444
1786
|
);
|
|
1445
1787
|
|
|
1446
1788
|
// Build companion JSX: only standalone companions with real props.
|
|
@@ -1465,6 +1807,59 @@ function renderJsx(components: readonly string[], ctx: RenderCtx, framework: str
|
|
|
1465
1807
|
}
|
|
1466
1808
|
const companions = companionJsxLines.join('\n');
|
|
1467
1809
|
|
|
1810
|
+
// The staging strip, ABOVE the chat. `<FileUpload>`/`<Attachments>` are the
|
|
1811
|
+
// generated wrappers, already in `importList` because both tags are in
|
|
1812
|
+
// `components` — no extra import bookkeeping, which is the point of keying the
|
|
1813
|
+
// import list off the components rather than off a per-capability table.
|
|
1814
|
+
const attachmentJsx = attachments
|
|
1815
|
+
? [
|
|
1816
|
+
` {/* Drop files here to stage them for the NEXT message. */}`,
|
|
1817
|
+
` <div style={{ flex: '0 0 auto', display: 'flex', flexDirection: 'column', gap: '0.5rem', padding: '0.75rem' }}>`,
|
|
1818
|
+
` <FileUpload accept="image/*,application/pdf" onFilesAdded={onFilesAdded} />`,
|
|
1819
|
+
` {/* items is an ARRAY, so the wrapper sets it as a DOM property, never an attribute. */}`,
|
|
1820
|
+
` <Attachments items={staged} variant="inline" removable onRemove={onRemoveAttachment} />`,
|
|
1821
|
+
` </div>`,
|
|
1822
|
+
].join('\n')
|
|
1823
|
+
: '';
|
|
1824
|
+
|
|
1825
|
+
// MODULE scope for `toAttachment` (it closes over nothing); component scope for
|
|
1826
|
+
// the state and the two handlers.
|
|
1827
|
+
const attachmentModuleInit = attachments
|
|
1828
|
+
? [
|
|
1829
|
+
...ATTACHMENT_WIRE_NOTE,
|
|
1830
|
+
`//`,
|
|
1831
|
+
`// AttachmentData is the kit's own type for a staged file — the same one`,
|
|
1832
|
+
`// <Attachments>.items holds and the same one a 'file' message part holds.`,
|
|
1833
|
+
...fileToAttachmentLines('', 'AttachmentData'),
|
|
1834
|
+
]
|
|
1835
|
+
: [];
|
|
1836
|
+
const attachmentStateInit = attachments
|
|
1837
|
+
? [
|
|
1838
|
+
` const [staged, setStaged] = useState<AttachmentData[]>([]);`,
|
|
1839
|
+
` const onFilesAdded = async (e: CustomEvent<{ files: File[] }>) => {`,
|
|
1840
|
+
` // Read every file BEFORE appending: a per-file append would order the`,
|
|
1841
|
+
` // list by whichever finished reading first.`,
|
|
1842
|
+
` const added = await Promise.all(e.detail.files.map(toAttachment));`,
|
|
1843
|
+
` setStaged((prev) => [...prev, ...added]);`,
|
|
1844
|
+
` };`,
|
|
1845
|
+
` const onRemoveAttachment = (e: CustomEvent<{ id: string }>) =>`,
|
|
1846
|
+
` setStaged((prev) => prev.filter((a) => a.id !== e.detail.id));`,
|
|
1847
|
+
].join('\n')
|
|
1848
|
+
: '';
|
|
1849
|
+
const attachmentSubmitOpts = attachments
|
|
1850
|
+
? {
|
|
1851
|
+
filesExpr: 'files',
|
|
1852
|
+
afterValue: attachmentTurnLines({
|
|
1853
|
+
stagedExpr: 'staged',
|
|
1854
|
+
clear: ['setStaged([]);'],
|
|
1855
|
+
fromSubmitEvent: true,
|
|
1856
|
+
}),
|
|
1857
|
+
}
|
|
1858
|
+
: {};
|
|
1859
|
+
const submitParamType = attachments
|
|
1860
|
+
? 'CustomEvent<{ value: string; attachments: AttachmentData[] }>'
|
|
1861
|
+
: 'CustomEvent<{ value: string }>';
|
|
1862
|
+
|
|
1468
1863
|
const mockInit = isMock ? mockResponderInit() : [];
|
|
1469
1864
|
|
|
1470
1865
|
// SCAF-9: no fabricated seed — see SAMPLE_AGENTIC_MESSAGE for the three ways
|
|
@@ -1528,6 +1923,7 @@ function renderJsx(components: readonly string[], ctx: RenderCtx, framework: str
|
|
|
1528
1923
|
cards: ctx.emitCards,
|
|
1529
1924
|
thread: REACT_THREAD,
|
|
1530
1925
|
mock: isMock,
|
|
1926
|
+
...attachmentSubmitOpts,
|
|
1531
1927
|
});
|
|
1532
1928
|
|
|
1533
1929
|
// SCAF-2: Next.js App Router requires 'use client' for components that use hooks/interactivity.
|
|
@@ -1564,6 +1960,7 @@ function renderJsx(components: readonly string[], ctx: RenderCtx, framework: str
|
|
|
1564
1960
|
cards: ctx.emitCards,
|
|
1565
1961
|
cardTools: ctx.cardProvider !== null,
|
|
1566
1962
|
mock: isMock,
|
|
1963
|
+
attachments,
|
|
1567
1964
|
}),
|
|
1568
1965
|
`import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
|
|
1569
1966
|
`// <kai-*> are client-only custom elements (the server has no customElements`,
|
|
@@ -1576,22 +1973,25 @@ function renderJsx(components: readonly string[], ctx: RenderCtx, framework: str
|
|
|
1576
1973
|
...(p.altNote ?? []).map((l) => `// ${l}`),
|
|
1577
1974
|
...mockInit,
|
|
1578
1975
|
``,
|
|
1976
|
+
...attachmentModuleInit,
|
|
1579
1977
|
...cardsInit,
|
|
1580
1978
|
`export default function App() {`,
|
|
1581
1979
|
sampleMessagesInit,
|
|
1582
1980
|
` const [loading, setLoading] = useState(false);`,
|
|
1583
1981
|
` const suggestions = ${jsArray(suggestions)};`,
|
|
1982
|
+
...(attachmentStateInit ? [attachmentStateInit] : []),
|
|
1584
1983
|
...(sampleSourcesInit ? [sampleSourcesInit] : []),
|
|
1585
1984
|
...(modelInit ? [modelInit] : []),
|
|
1586
1985
|
...(toolsInit ? [toolsInit] : []),
|
|
1587
1986
|
...(toolRunner ? [toolRunner] : []),
|
|
1588
1987
|
``,
|
|
1589
|
-
` async function onSubmit(e:
|
|
1988
|
+
` async function onSubmit(e: ${submitParamType}) {`,
|
|
1590
1989
|
onSubmitBody,
|
|
1591
1990
|
` }`,
|
|
1592
1991
|
``,
|
|
1593
1992
|
` return (`,
|
|
1594
1993
|
` <div style={{ ${jsxStyle(p.style)} }}>`,
|
|
1994
|
+
...(attachmentJsx ? [attachmentJsx] : []),
|
|
1595
1995
|
...(workspace
|
|
1596
1996
|
? [
|
|
1597
1997
|
` {/* SCAF-14: workspace split — chat pane left, artifact preview right. */}`,
|
|
@@ -1659,6 +2059,7 @@ function renderJsx(components: readonly string[], ctx: RenderCtx, framework: str
|
|
|
1659
2059
|
cards: ctx.emitCards,
|
|
1660
2060
|
cardTools: ctx.cardProvider !== null,
|
|
1661
2061
|
mock: isMock,
|
|
2062
|
+
attachments,
|
|
1662
2063
|
}),
|
|
1663
2064
|
`import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
|
|
1664
2065
|
``,
|
|
@@ -1667,22 +2068,25 @@ function renderJsx(components: readonly string[], ctx: RenderCtx, framework: str
|
|
|
1667
2068
|
...(p.altNote ?? []).map((l) => `// ${l}`),
|
|
1668
2069
|
...mockInit,
|
|
1669
2070
|
``,
|
|
2071
|
+
...attachmentModuleInit,
|
|
1670
2072
|
...cardsInit,
|
|
1671
2073
|
`export default function App() {`,
|
|
1672
2074
|
sampleMessagesInit,
|
|
1673
2075
|
` const [loading, setLoading] = useState(false);`,
|
|
1674
2076
|
` const suggestions = ${jsArray(suggestions)};`,
|
|
2077
|
+
...(attachmentStateInit ? [attachmentStateInit] : []),
|
|
1675
2078
|
...(sampleSourcesInit ? [sampleSourcesInit] : []),
|
|
1676
2079
|
...(modelInit ? [modelInit] : []),
|
|
1677
2080
|
...(toolsInit ? [toolsInit] : []),
|
|
1678
2081
|
...(toolRunner ? [toolRunner] : []),
|
|
1679
2082
|
``,
|
|
1680
|
-
` async function onSubmit(e:
|
|
2083
|
+
` async function onSubmit(e: ${submitParamType}) {`,
|
|
1681
2084
|
onSubmitBody,
|
|
1682
2085
|
` }`,
|
|
1683
2086
|
``,
|
|
1684
2087
|
` return (`,
|
|
1685
2088
|
` <div style={{ ${jsxStyle(p.style)} }}>`,
|
|
2089
|
+
...(attachmentJsx ? [attachmentJsx] : []),
|
|
1686
2090
|
...(workspace
|
|
1687
2091
|
? [
|
|
1688
2092
|
` {/* SCAF-14: workspace split — chat pane left, artifact preview right. */}`,
|
|
@@ -1736,8 +2140,13 @@ function renderVue(components: readonly string[], ctx: RenderCtx): string {
|
|
|
1736
2140
|
// SCAF-9: exclude message-embedded tags from companion rendering.
|
|
1737
2141
|
// SCAF-14: also exclude workspace structural tags (handled by the workspace block below).
|
|
1738
2142
|
const workspace = isWorkspace(components);
|
|
2143
|
+
const attachments = hasAttachments(components);
|
|
1739
2144
|
const standaloneCompanionTags = components.filter(
|
|
1740
|
-
(t) =>
|
|
2145
|
+
(t) =>
|
|
2146
|
+
t !== 'kai-chat' &&
|
|
2147
|
+
!MESSAGE_EMBEDDED_TAGS.has(t) &&
|
|
2148
|
+
!WORKSPACE_STRUCTURAL_TAGS.has(t) &&
|
|
2149
|
+
!(attachments && ATTACHMENT_TAGS.has(t)),
|
|
1741
2150
|
);
|
|
1742
2151
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
1743
2152
|
|
|
@@ -1758,6 +2167,55 @@ function renderVue(components: readonly string[], ctx: RenderCtx): string {
|
|
|
1758
2167
|
}
|
|
1759
2168
|
const companions = companionLines.join('\n');
|
|
1760
2169
|
|
|
2170
|
+
// `:items.prop` for the same reason `:messages.prop` carries the thread: an
|
|
2171
|
+
// array set as an ATTRIBUTE stringifies, and `<kai-attachments>` would render
|
|
2172
|
+
// the empty state. The listeners are plain `@kai-*` — Vue maps an unknown-tag
|
|
2173
|
+
// event name straight to addEventListener, which is how `@kai-submit` already
|
|
2174
|
+
// works here.
|
|
2175
|
+
const attachmentTemplate = attachments
|
|
2176
|
+
? [
|
|
2177
|
+
` <!-- Drop files here to stage them for the NEXT message. -->`,
|
|
2178
|
+
` <div style="flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem;">`,
|
|
2179
|
+
` <kai-file-upload accept="image/*,application/pdf" @kai-files-added="onFilesAdded" />`,
|
|
2180
|
+
` <kai-attachments :items.prop="staged" variant="inline" removable @kai-remove="onRemoveAttachment" />`,
|
|
2181
|
+
` </div>`,
|
|
2182
|
+
].join('\n')
|
|
2183
|
+
: '';
|
|
2184
|
+
|
|
2185
|
+
const attachmentScript = attachments
|
|
2186
|
+
? [
|
|
2187
|
+
...ATTACHMENT_WIRE_NOTE,
|
|
2188
|
+
`//`,
|
|
2189
|
+
`// AttachmentData is the kit's own type for a staged file, and it is what`,
|
|
2190
|
+
`// <kai-attachments>.items and a 'file' MessagePart both hold.`,
|
|
2191
|
+
`const staged = ref<AttachmentData[]>([]);`,
|
|
2192
|
+
``,
|
|
2193
|
+
...fileToAttachmentLines('', 'AttachmentData'),
|
|
2194
|
+
``,
|
|
2195
|
+
`async function onFilesAdded(event: Event) {`,
|
|
2196
|
+
` const { files } = (event as CustomEvent<{ files: File[] }>).detail;`,
|
|
2197
|
+
` // Read every file BEFORE appending: a per-file append would order the`,
|
|
2198
|
+
` // list by whichever finished reading first.`,
|
|
2199
|
+
` const added = await Promise.all(files.map(toAttachment));`,
|
|
2200
|
+
` staged.value = [...staged.value, ...added];`,
|
|
2201
|
+
`}`,
|
|
2202
|
+
`function onRemoveAttachment(event: Event) {`,
|
|
2203
|
+
` const { id } = (event as CustomEvent<{ id: string }>).detail;`,
|
|
2204
|
+
` staged.value = staged.value.filter((a) => a.id !== id);`,
|
|
2205
|
+
`}`,
|
|
2206
|
+
]
|
|
2207
|
+
: [];
|
|
2208
|
+
const attachmentSubmitOpts = attachments
|
|
2209
|
+
? {
|
|
2210
|
+
filesExpr: 'files',
|
|
2211
|
+
afterValue: attachmentTurnLines({
|
|
2212
|
+
stagedExpr: 'staged.value',
|
|
2213
|
+
clear: ['staged.value = [];'],
|
|
2214
|
+
fromSubmitEvent: true,
|
|
2215
|
+
}),
|
|
2216
|
+
}
|
|
2217
|
+
: {};
|
|
2218
|
+
|
|
1761
2219
|
const onSubmitBody = realStreamBody({
|
|
1762
2220
|
pad: ' ',
|
|
1763
2221
|
read: 'messages.value',
|
|
@@ -1770,6 +2228,7 @@ function renderVue(components: readonly string[], ctx: RenderCtx): string {
|
|
|
1770
2228
|
cards: ctx.emitCards,
|
|
1771
2229
|
thread: liveThreadBinding('messages.value', '(fn) => { messages.value = fn(messages.value); }'),
|
|
1772
2230
|
mock: isMock,
|
|
2231
|
+
...attachmentSubmitOpts,
|
|
1773
2232
|
});
|
|
1774
2233
|
|
|
1775
2234
|
// SCAF-10: ChatMessage declaration for strict-TS Vue consumers.
|
|
@@ -1879,6 +2338,7 @@ function renderVue(components: readonly string[], ctx: RenderCtx): string {
|
|
|
1879
2338
|
cards: ctx.emitCards,
|
|
1880
2339
|
cardTools: ctx.cardProvider !== null,
|
|
1881
2340
|
mock: isMock,
|
|
2341
|
+
attachments,
|
|
1882
2342
|
}),
|
|
1883
2343
|
`import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
|
|
1884
2344
|
vueImports,
|
|
@@ -1893,6 +2353,7 @@ function renderVue(components: readonly string[], ctx: RenderCtx): string {
|
|
|
1893
2353
|
...toolsLines,
|
|
1894
2354
|
...runnerLines,
|
|
1895
2355
|
...sourcesSeed,
|
|
2356
|
+
...attachmentScript,
|
|
1896
2357
|
``,
|
|
1897
2358
|
`// SCAF-15: kai-* register via an async dynamic import (SSR-safety). The .prop`,
|
|
1898
2359
|
`// bindings can apply before the element upgrades, which drops them — re-apply once`,
|
|
@@ -1904,13 +2365,16 @@ function renderVue(components: readonly string[], ctx: RenderCtx): string {
|
|
|
1904
2365
|
` if (el) Object.assign(el, { messages: messages.value, loading: loading.value, suggestions${cardPropAssign.length ? `, ${cardPropAssign.join(', ')}` : ''} });`,
|
|
1905
2366
|
`});`,
|
|
1906
2367
|
``,
|
|
1907
|
-
`async function onSubmit(e:
|
|
2368
|
+
`async function onSubmit(e: ${attachments
|
|
2369
|
+
? 'CustomEvent<{ value: string; attachments: AttachmentData[] }>'
|
|
2370
|
+
: 'CustomEvent<{ value: string }>'}) {`,
|
|
1908
2371
|
onSubmitBody,
|
|
1909
2372
|
`}`,
|
|
1910
2373
|
`</script>`,
|
|
1911
2374
|
``,
|
|
1912
2375
|
`<template>`,
|
|
1913
2376
|
` <div style="${p.style}">`,
|
|
2377
|
+
...(attachmentTemplate ? [attachmentTemplate] : []),
|
|
1914
2378
|
...workspaceTemplate,
|
|
1915
2379
|
` </div>`,
|
|
1916
2380
|
`</template>`,
|
|
@@ -1926,8 +2390,13 @@ function renderSvelte(components: readonly string[], ctx: RenderCtx): string {
|
|
|
1926
2390
|
// SCAF-9: exclude message-embedded tags from companion rendering.
|
|
1927
2391
|
// SCAF-14: also exclude workspace structural tags (handled by the workspace block below).
|
|
1928
2392
|
const workspace = isWorkspace(components);
|
|
2393
|
+
const attachments = hasAttachments(components);
|
|
1929
2394
|
const standaloneCompanionTags = components.filter(
|
|
1930
|
-
(t) =>
|
|
2395
|
+
(t) =>
|
|
2396
|
+
t !== 'kai-chat' &&
|
|
2397
|
+
!MESSAGE_EMBEDDED_TAGS.has(t) &&
|
|
2398
|
+
!WORKSPACE_STRUCTURAL_TAGS.has(t) &&
|
|
2399
|
+
!(attachments && ATTACHMENT_TAGS.has(t)),
|
|
1931
2400
|
);
|
|
1932
2401
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
1933
2402
|
const hasSourcesCompanion = standaloneCompanionTags.includes('kai-sources');
|
|
@@ -1949,6 +2418,17 @@ function renderSvelte(components: readonly string[], ctx: RenderCtx): string {
|
|
|
1949
2418
|
}
|
|
1950
2419
|
const companionLines = companionLinesList.join('\n');
|
|
1951
2420
|
|
|
2421
|
+
const attachmentSubmitOpts = attachments
|
|
2422
|
+
? {
|
|
2423
|
+
filesExpr: 'files',
|
|
2424
|
+
afterValue: attachmentTurnLines({
|
|
2425
|
+
stagedExpr: 'staged',
|
|
2426
|
+
clear: ['staged = [];'],
|
|
2427
|
+
fromSubmitEvent: true,
|
|
2428
|
+
}),
|
|
2429
|
+
}
|
|
2430
|
+
: {};
|
|
2431
|
+
|
|
1952
2432
|
const onSubmitBody = realStreamBody({
|
|
1953
2433
|
pad: ' ',
|
|
1954
2434
|
read: 'messages',
|
|
@@ -1961,6 +2441,7 @@ function renderSvelte(components: readonly string[], ctx: RenderCtx): string {
|
|
|
1961
2441
|
cards: ctx.emitCards,
|
|
1962
2442
|
thread: liveThreadBinding('messages', '(fn) => { messages = fn(messages); }'),
|
|
1963
2443
|
mock: isMock,
|
|
2444
|
+
...attachmentSubmitOpts,
|
|
1964
2445
|
});
|
|
1965
2446
|
|
|
1966
2447
|
// SCAF-10: ChatMessage declaration for strict-TS Svelte consumers.
|
|
@@ -2021,6 +2502,43 @@ function renderSvelte(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2021
2502
|
]
|
|
2022
2503
|
: [];
|
|
2023
2504
|
|
|
2505
|
+
// Svelte 5: the element ref is $state (bind:this writes to it), and the staged
|
|
2506
|
+
// list is $state.raw for the same reason `messages` is — a NEW array per write is
|
|
2507
|
+
// the kit's contract, and deep state would proxy every object on its way into a
|
|
2508
|
+
// Solid-backed custom element. `items` is an ARRAY, so it is applied through the
|
|
2509
|
+
// same upgrade-gated $effect chatEl.messages goes through, never as an attribute.
|
|
2510
|
+
const attachmentScript = attachments
|
|
2511
|
+
? [
|
|
2512
|
+
...ATTACHMENT_WIRE_NOTE.map((l) => ` ${l}`),
|
|
2513
|
+
` //`,
|
|
2514
|
+
` // AttachmentData is the kit's own type for a staged file, and it is what`,
|
|
2515
|
+
` // <kai-attachments>.items and a 'file' MessagePart both hold.`,
|
|
2516
|
+
` let attachmentsEl = $state<KaiAttachmentsElement | undefined>(undefined);`,
|
|
2517
|
+
` let staged = $state.raw<AttachmentData[]>([]);`,
|
|
2518
|
+
` $effect(() => { if (attachmentsEl && defined) { attachmentsEl.items = staged; } });`,
|
|
2519
|
+
``,
|
|
2520
|
+
...fileToAttachmentLines(' ', 'AttachmentData'),
|
|
2521
|
+
` async function onFilesAdded(e: CustomEvent<{ files: File[] }>) {`,
|
|
2522
|
+
` // Read every file BEFORE appending: a per-file append would order the`,
|
|
2523
|
+
` // list by whichever finished reading first.`,
|
|
2524
|
+
` const added = await Promise.all(e.detail.files.map(toAttachment));`,
|
|
2525
|
+
` staged = [...staged, ...added];`,
|
|
2526
|
+
` }`,
|
|
2527
|
+
` function onRemoveAttachment(e: CustomEvent<{ id: string }>) {`,
|
|
2528
|
+
` staged = staged.filter((a) => a.id !== e.detail.id);`,
|
|
2529
|
+
` }`,
|
|
2530
|
+
]
|
|
2531
|
+
: [];
|
|
2532
|
+
const attachmentMarkup = attachments
|
|
2533
|
+
? [
|
|
2534
|
+
` <!-- Drop files here to stage them for the NEXT message. -->`,
|
|
2535
|
+
` <div style="flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem;">`,
|
|
2536
|
+
` <kai-file-upload accept="image/*,application/pdf" onkai-files-added={onFilesAdded}></kai-file-upload>`,
|
|
2537
|
+
` <kai-attachments bind:this={attachmentsEl} variant="inline" removable onkai-remove={onRemoveAttachment}></kai-attachments>`,
|
|
2538
|
+
` </div>`,
|
|
2539
|
+
]
|
|
2540
|
+
: [];
|
|
2541
|
+
|
|
2024
2542
|
// SCAF-14: workspace template block — resizable split with chat + artifact panes.
|
|
2025
2543
|
const workspaceMarkup = workspace
|
|
2026
2544
|
? [
|
|
@@ -2055,7 +2573,11 @@ function renderSvelte(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2055
2573
|
// KaiSourcesElement is only imported when a kai-sources companion is actually
|
|
2056
2574
|
// declared below: an always-on import would be unused (and fail noUnusedLocals)
|
|
2057
2575
|
// on every archetype without kai-sources.
|
|
2058
|
-
` import type { ${
|
|
2576
|
+
` import type { ${[
|
|
2577
|
+
'KaiChatElement',
|
|
2578
|
+
...(hasSourcesCompanion ? ['KaiSourcesElement'] : []),
|
|
2579
|
+
...(attachments ? ['KaiAttachmentsElement'] : []),
|
|
2580
|
+
].join(', ')} } from '@kitn.ai/ui/elements';`,
|
|
2059
2581
|
...wireImportLines({
|
|
2060
2582
|
pad: ' ',
|
|
2061
2583
|
typed: true,
|
|
@@ -2063,6 +2585,7 @@ function renderSvelte(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2063
2585
|
cards: ctx.emitCards,
|
|
2064
2586
|
cardTools: ctx.cardProvider !== null,
|
|
2065
2587
|
mock: isMock,
|
|
2588
|
+
attachments,
|
|
2066
2589
|
}),
|
|
2067
2590
|
` import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
|
|
2068
2591
|
` import { onMount } from 'svelte';`,
|
|
@@ -2088,13 +2611,17 @@ function renderSvelte(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2088
2611
|
` if (chatEl && defined) { chatEl.messages = messages; chatEl.loading = loading; chatEl.suggestions = suggestions;${cardPropEffect} }`,
|
|
2089
2612
|
` });`,
|
|
2090
2613
|
...sourcesReactive,
|
|
2614
|
+
...attachmentScript,
|
|
2091
2615
|
``,
|
|
2092
|
-
` async function onSubmit(e:
|
|
2616
|
+
` async function onSubmit(e: ${attachments
|
|
2617
|
+
? 'CustomEvent<{ value: string; attachments: AttachmentData[] }>'
|
|
2618
|
+
: 'CustomEvent<{ value: string }>'}) {`,
|
|
2093
2619
|
onSubmitBody,
|
|
2094
2620
|
` }`,
|
|
2095
2621
|
`</script>`,
|
|
2096
2622
|
``,
|
|
2097
2623
|
`<div style="${p.style}">`,
|
|
2624
|
+
...attachmentMarkup,
|
|
2098
2625
|
...workspaceMarkup,
|
|
2099
2626
|
`</div>`,
|
|
2100
2627
|
]
|
|
@@ -2151,8 +2678,14 @@ function renderTanstackStart(components: readonly string[], ctx: RenderCtx): str
|
|
|
2151
2678
|
const wrapperNames = importTags.map(toPascalCase);
|
|
2152
2679
|
const importList = wrapperNames.join(', ');
|
|
2153
2680
|
|
|
2681
|
+
const attachments = hasAttachments(components);
|
|
2682
|
+
|
|
2154
2683
|
const standaloneCompanionTags = components.filter(
|
|
2155
|
-
(t) =>
|
|
2684
|
+
(t) =>
|
|
2685
|
+
t !== 'kai-chat' &&
|
|
2686
|
+
!MESSAGE_EMBEDDED_TAGS.has(t) &&
|
|
2687
|
+
!WORKSPACE_STRUCTURAL_TAGS.has(t) &&
|
|
2688
|
+
!(attachments && ATTACHMENT_TAGS.has(t)),
|
|
2156
2689
|
);
|
|
2157
2690
|
|
|
2158
2691
|
const companionJsxLines: string[] = [];
|
|
@@ -2175,6 +2708,54 @@ function renderTanstackStart(components: readonly string[], ctx: RenderCtx): str
|
|
|
2175
2708
|
}
|
|
2176
2709
|
const companions = companionJsxLines.join('\n');
|
|
2177
2710
|
|
|
2711
|
+
// Same three pieces as `renderJsx` — this target IS React. See there for why
|
|
2712
|
+
// the type comes from @kitn.ai/ui/state and why the strip sits above the chat.
|
|
2713
|
+
const attachmentJsx = attachments
|
|
2714
|
+
? [
|
|
2715
|
+
` {/* Drop files here to stage them for the NEXT message. */}`,
|
|
2716
|
+
` <div style={{ flex: '0 0 auto', display: 'flex', flexDirection: 'column', gap: '0.5rem', padding: '0.75rem' }}>`,
|
|
2717
|
+
` <FileUpload accept="image/*,application/pdf" onFilesAdded={onFilesAdded} />`,
|
|
2718
|
+
` {/* items is an ARRAY, so the wrapper sets it as a DOM property, never an attribute. */}`,
|
|
2719
|
+
` <Attachments items={staged} variant="inline" removable onRemove={onRemoveAttachment} />`,
|
|
2720
|
+
` </div>`,
|
|
2721
|
+
].join('\n')
|
|
2722
|
+
: '';
|
|
2723
|
+
const attachmentModuleInit = attachments
|
|
2724
|
+
? [
|
|
2725
|
+
...ATTACHMENT_WIRE_NOTE,
|
|
2726
|
+
`//`,
|
|
2727
|
+
`// AttachmentData is the kit's own type for a staged file — the same one`,
|
|
2728
|
+
`// <Attachments>.items holds and the same one a 'file' message part holds.`,
|
|
2729
|
+
...fileToAttachmentLines('', 'AttachmentData'),
|
|
2730
|
+
]
|
|
2731
|
+
: [];
|
|
2732
|
+
const attachmentStateInit = attachments
|
|
2733
|
+
? [
|
|
2734
|
+
` const [staged, setStaged] = useState<AttachmentData[]>([]);`,
|
|
2735
|
+
` const onFilesAdded = async (e: CustomEvent<{ files: File[] }>) => {`,
|
|
2736
|
+
` // Read every file BEFORE appending: a per-file append would order the`,
|
|
2737
|
+
` // list by whichever finished reading first.`,
|
|
2738
|
+
` const added = await Promise.all(e.detail.files.map(toAttachment));`,
|
|
2739
|
+
` setStaged((prev) => [...prev, ...added]);`,
|
|
2740
|
+
` };`,
|
|
2741
|
+
` const onRemoveAttachment = (e: CustomEvent<{ id: string }>) =>`,
|
|
2742
|
+
` setStaged((prev) => prev.filter((a) => a.id !== e.detail.id));`,
|
|
2743
|
+
].join('\n')
|
|
2744
|
+
: '';
|
|
2745
|
+
const attachmentSubmitOpts = attachments
|
|
2746
|
+
? {
|
|
2747
|
+
filesExpr: 'files',
|
|
2748
|
+
afterValue: attachmentTurnLines({
|
|
2749
|
+
stagedExpr: 'staged',
|
|
2750
|
+
clear: ['setStaged([]);'],
|
|
2751
|
+
fromSubmitEvent: true,
|
|
2752
|
+
}),
|
|
2753
|
+
}
|
|
2754
|
+
: {};
|
|
2755
|
+
const submitParamType = attachments
|
|
2756
|
+
? 'CustomEvent<{ value: string; attachments: AttachmentData[] }>'
|
|
2757
|
+
: 'CustomEvent<{ value: string }>';
|
|
2758
|
+
|
|
2178
2759
|
const mockInit = isMock ? mockResponderInit() : [];
|
|
2179
2760
|
|
|
2180
2761
|
// SCAF-9: no fabricated seed — see SAMPLE_AGENTIC_MESSAGE.
|
|
@@ -2227,6 +2808,7 @@ function renderTanstackStart(components: readonly string[], ctx: RenderCtx): str
|
|
|
2227
2808
|
cards: ctx.emitCards,
|
|
2228
2809
|
thread: REACT_THREAD,
|
|
2229
2810
|
mock: isMock,
|
|
2811
|
+
...attachmentSubmitOpts,
|
|
2230
2812
|
});
|
|
2231
2813
|
|
|
2232
2814
|
// File path guidance for TanStack Start (file-based routing)
|
|
@@ -2260,6 +2842,7 @@ function renderTanstackStart(components: readonly string[], ctx: RenderCtx): str
|
|
|
2260
2842
|
cards: ctx.emitCards,
|
|
2261
2843
|
cardTools: ctx.cardProvider !== null,
|
|
2262
2844
|
mock: isMock,
|
|
2845
|
+
attachments,
|
|
2263
2846
|
}),
|
|
2264
2847
|
`import '@kitn.ai/ui/theme.tokens.css' // compiled token defaults`,
|
|
2265
2848
|
``,
|
|
@@ -2267,6 +2850,7 @@ function renderTanstackStart(components: readonly string[], ctx: RenderCtx): str
|
|
|
2267
2850
|
...(p.altNote ?? []).map((l) => `// ${l}`),
|
|
2268
2851
|
...mockInit,
|
|
2269
2852
|
``,
|
|
2853
|
+
...attachmentModuleInit,
|
|
2270
2854
|
...cardsInit,
|
|
2271
2855
|
`// ssr: false keeps the Solid-based web component client-only.`,
|
|
2272
2856
|
`// Server HTML for /chat omits <kai-chat> → no hydration mismatch.`,
|
|
@@ -2279,17 +2863,19 @@ function renderTanstackStart(components: readonly string[], ctx: RenderCtx): str
|
|
|
2279
2863
|
sampleMessagesInit,
|
|
2280
2864
|
` const [loading, setLoading] = useState(false);`,
|
|
2281
2865
|
` const suggestions = ${jsArray(suggestions)};`,
|
|
2866
|
+
...(attachmentStateInit ? [attachmentStateInit] : []),
|
|
2282
2867
|
...(sampleSourcesInit ? [sampleSourcesInit] : []),
|
|
2283
2868
|
...(modelInit ? [modelInit] : []),
|
|
2284
2869
|
...(toolsInit ? [toolsInit] : []),
|
|
2285
2870
|
...(toolRunner ? [toolRunner] : []),
|
|
2286
2871
|
``,
|
|
2287
|
-
` async function onSubmit(e:
|
|
2872
|
+
` async function onSubmit(e: ${submitParamType}) {`,
|
|
2288
2873
|
onSubmitBody,
|
|
2289
2874
|
` }`,
|
|
2290
2875
|
``,
|
|
2291
2876
|
` return (`,
|
|
2292
2877
|
` <main style={{ ${jsxStyle(p.style)} }}>`,
|
|
2878
|
+
...(attachmentJsx ? [attachmentJsx] : []),
|
|
2293
2879
|
...(workspace
|
|
2294
2880
|
? [
|
|
2295
2881
|
` {/* SCAF-14: workspace split — chat pane left, artifact preview right. */}`,
|
|
@@ -2366,8 +2952,13 @@ function renderAngular(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2366
2952
|
const { p, emptyHint, suggestions, isMock, defaultModel, emitTools, emitToolLoop } = ctx;
|
|
2367
2953
|
|
|
2368
2954
|
const workspace = isWorkspace(components);
|
|
2955
|
+
const attachments = hasAttachments(components);
|
|
2369
2956
|
const standaloneCompanionTags = components.filter(
|
|
2370
|
-
(t) =>
|
|
2957
|
+
(t) =>
|
|
2958
|
+
t !== 'kai-chat' &&
|
|
2959
|
+
!MESSAGE_EMBEDDED_TAGS.has(t) &&
|
|
2960
|
+
!WORKSPACE_STRUCTURAL_TAGS.has(t) &&
|
|
2961
|
+
!(attachments && ATTACHMENT_TAGS.has(t)),
|
|
2371
2962
|
);
|
|
2372
2963
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
2373
2964
|
const hasSourcesCompanion = standaloneCompanionTags.includes('kai-sources');
|
|
@@ -2390,6 +2981,61 @@ function renderAngular(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2390
2981
|
}
|
|
2391
2982
|
}
|
|
2392
2983
|
|
|
2984
|
+
// `[items]` is a PROPERTY binding for the same reason `[messages]` is: an array
|
|
2985
|
+
// written as an attribute stringifies. Angular's own signal read goes in the
|
|
2986
|
+
// template, so no afterNextRender re-apply is needed for it — the binding
|
|
2987
|
+
// re-runs on every signal change, including the first one after upgrade.
|
|
2988
|
+
const attachmentTemplate = attachments
|
|
2989
|
+
? [
|
|
2990
|
+
` <!-- Drop files here to stage them for the NEXT message. -->`,
|
|
2991
|
+
` <div style="flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem;">`,
|
|
2992
|
+
` <kai-file-upload accept="image/*,application/pdf" (kai-files-added)="onFilesAdded($event)"></kai-file-upload>`,
|
|
2993
|
+
` <kai-attachments [items]="staged()" variant="inline" removable (kai-remove)="onRemoveAttachment($event)"></kai-attachments>`,
|
|
2994
|
+
` </div>`,
|
|
2995
|
+
]
|
|
2996
|
+
: [];
|
|
2997
|
+
// Module scope for `toAttachment` (a class holds no bare function declaration),
|
|
2998
|
+
// class scope for the signal and the two handlers the template calls.
|
|
2999
|
+
const attachmentModuleInit = attachments
|
|
3000
|
+
? [
|
|
3001
|
+
...ATTACHMENT_WIRE_NOTE,
|
|
3002
|
+
`//`,
|
|
3003
|
+
`// AttachmentData is the kit's own type for a staged file, and it is what`,
|
|
3004
|
+
`// <kai-attachments>.items and a 'file' MessagePart both hold.`,
|
|
3005
|
+
...fileToAttachmentLines('', 'AttachmentData'),
|
|
3006
|
+
``,
|
|
3007
|
+
]
|
|
3008
|
+
: [];
|
|
3009
|
+
const attachmentFields = attachments
|
|
3010
|
+
? [
|
|
3011
|
+
` readonly staged = signal<AttachmentData[]>([]);`,
|
|
3012
|
+
``,
|
|
3013
|
+
` // Same Event-not-CustomEvent rule as onSubmit below: strictTemplates types`,
|
|
3014
|
+
` // $event on an unknown custom-element event as a plain Event.`,
|
|
3015
|
+
` async onFilesAdded(event: Event) {`,
|
|
3016
|
+
` const { files } = (event as CustomEvent<{ files: File[] }>).detail;`,
|
|
3017
|
+
` // Read every file BEFORE appending: a per-file append would order the`,
|
|
3018
|
+
` // list by whichever finished reading first.`,
|
|
3019
|
+
` const added = await Promise.all(files.map(toAttachment));`,
|
|
3020
|
+
` this.staged.set([...this.staged(), ...added]);`,
|
|
3021
|
+
` }`,
|
|
3022
|
+
` onRemoveAttachment(event: Event) {`,
|
|
3023
|
+
` const { id } = (event as CustomEvent<{ id: string }>).detail;`,
|
|
3024
|
+
` this.staged.set(this.staged().filter((a) => a.id !== id));`,
|
|
3025
|
+
` }`,
|
|
3026
|
+
]
|
|
3027
|
+
: [];
|
|
3028
|
+
const attachmentSubmitOpts = attachments
|
|
3029
|
+
? {
|
|
3030
|
+
filesExpr: 'files',
|
|
3031
|
+
afterValue: attachmentTurnLines({
|
|
3032
|
+
stagedExpr: 'this.staged()',
|
|
3033
|
+
clear: ['this.staged.set([]);'],
|
|
3034
|
+
fromSubmitEvent: true,
|
|
3035
|
+
}),
|
|
3036
|
+
}
|
|
3037
|
+
: {};
|
|
3038
|
+
|
|
2393
3039
|
// Angular signals: `this.messages()` reads, `this.messages.set(next)` writes a
|
|
2394
3040
|
// BRAND-NEW array, which is what re-renders <kai-chat>.
|
|
2395
3041
|
const read = 'this.messages()';
|
|
@@ -2408,6 +3054,7 @@ function renderAngular(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2408
3054
|
cards: ctx.emitCards,
|
|
2409
3055
|
thread: accessorThreadBinding(read, commit, setter),
|
|
2410
3056
|
mock: isMock,
|
|
3057
|
+
...attachmentSubmitOpts,
|
|
2411
3058
|
});
|
|
2412
3059
|
|
|
2413
3060
|
// Module scope, exactly like vue: a class can hold neither a bare `const` nor a
|
|
@@ -2495,6 +3142,9 @@ function renderAngular(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2495
3142
|
// declared: an always-on import is unused on every other archetype, and a stock
|
|
2496
3143
|
// Angular tsconfig turns on the checks that make that a build error.
|
|
2497
3144
|
const elementTypes = hasSourcesCompanion ? 'KaiChatElement, KaiSourcesElement' : 'KaiChatElement';
|
|
3145
|
+
// No KaiAttachmentsElement here: unlike svelte/html this target never holds an
|
|
3146
|
+
// element reference for the list — `[items]` is a template binding, so nothing
|
|
3147
|
+
// in the class is typed by it.
|
|
2498
3148
|
|
|
2499
3149
|
return [
|
|
2500
3150
|
`// Angular standalone component — save as: src/app/chat.component.ts`,
|
|
@@ -2516,6 +3166,7 @@ function renderAngular(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2516
3166
|
cards: ctx.emitCards,
|
|
2517
3167
|
cardTools: ctx.cardProvider !== null,
|
|
2518
3168
|
mock: isMock,
|
|
3169
|
+
attachments,
|
|
2519
3170
|
}),
|
|
2520
3171
|
``,
|
|
2521
3172
|
`// ${ctx.label} — ${p.note}. empty-state hint: ${emptyHint}`,
|
|
@@ -2523,6 +3174,7 @@ function renderAngular(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2523
3174
|
...(isMock ? mockResponderInit() : []),
|
|
2524
3175
|
``,
|
|
2525
3176
|
...modelInit,
|
|
3177
|
+
...attachmentModuleInit,
|
|
2526
3178
|
...cardsInit,
|
|
2527
3179
|
...toolsLines,
|
|
2528
3180
|
...runnerLines,
|
|
@@ -2534,6 +3186,7 @@ function renderAngular(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2534
3186
|
` schemas: [CUSTOM_ELEMENTS_SCHEMA],`,
|
|
2535
3187
|
` template: \``,
|
|
2536
3188
|
` <div style="${p.style}">`,
|
|
3189
|
+
...attachmentTemplate,
|
|
2537
3190
|
...templateBody,
|
|
2538
3191
|
` </div>`,
|
|
2539
3192
|
` \`,`,
|
|
@@ -2566,11 +3219,13 @@ function renderAngular(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2566
3219
|
` });`,
|
|
2567
3220
|
` }`,
|
|
2568
3221
|
``,
|
|
3222
|
+
...attachmentFields,
|
|
3223
|
+
``,
|
|
2569
3224
|
` // \`Event\`, not \`CustomEvent\`: under strictTemplates Angular types \`$event\` on`,
|
|
2570
3225
|
` // an unknown custom-element event as a plain Event, so the narrowing happens`,
|
|
2571
3226
|
` // here rather than in the signature.`,
|
|
2572
3227
|
` async onSubmit(event: Event) {`,
|
|
2573
|
-
` const e = event as CustomEvent<{ value: string }>;`,
|
|
3228
|
+
` const e = event as CustomEvent<{ value: string${attachments ? '; attachments: AttachmentData[]' : ''} }>;`,
|
|
2574
3229
|
onSubmitBody,
|
|
2575
3230
|
` }`,
|
|
2576
3231
|
`}`,
|
|
@@ -2625,8 +3280,13 @@ function renderSolid(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2625
3280
|
const { p, emptyHint, suggestions, isMock, defaultModel, emitTools, emitToolLoop } = ctx;
|
|
2626
3281
|
|
|
2627
3282
|
const workspace = isWorkspace(components);
|
|
3283
|
+
const attachments = hasAttachments(components);
|
|
2628
3284
|
const standaloneCompanionTags = components.filter(
|
|
2629
|
-
(t) =>
|
|
3285
|
+
(t) =>
|
|
3286
|
+
t !== 'kai-chat' &&
|
|
3287
|
+
!MESSAGE_EMBEDDED_TAGS.has(t) &&
|
|
3288
|
+
!WORKSPACE_STRUCTURAL_TAGS.has(t) &&
|
|
3289
|
+
!(attachments && ATTACHMENT_TAGS.has(t)),
|
|
2630
3290
|
);
|
|
2631
3291
|
const hasSources = standaloneCompanionTags.includes('kai-sources');
|
|
2632
3292
|
const hasVoice = standaloneCompanionTags.includes('kai-voice-input');
|
|
@@ -2669,6 +3329,12 @@ function renderSolid(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2669
3329
|
'Tool',
|
|
2670
3330
|
...(workspace ? ['Artifact', 'ResizableHandle', 'ResizablePanel', 'ResizablePanelGroup'] : []),
|
|
2671
3331
|
...(hasVoice ? ['VoiceInput'] : []),
|
|
3332
|
+
// `Attachments`/`Attachment`/`AttachmentPreview`/`AttachmentInfo` are already
|
|
3333
|
+
// unconditional above — `renderPart` draws `file` PARTS with them whatever the
|
|
3334
|
+
// surface asked for. These three are the STAGING half, which only this
|
|
3335
|
+
// capability composes: the dropzone, its trigger, and the per-chip remove
|
|
3336
|
+
// button `renderPart` has no use for (a sent file is not removable).
|
|
3337
|
+
...(attachments ? ['AttachmentRemove', 'FileUpload', 'FileUploadTrigger'] : []),
|
|
2672
3338
|
].sort();
|
|
2673
3339
|
|
|
2674
3340
|
// Solid signals: `messages()` reads, `setMessages(next)` writes a NEW array.
|
|
@@ -2691,7 +3357,19 @@ function renderSolid(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2691
3357
|
cards: ctx.emitCards,
|
|
2692
3358
|
thread: accessorThreadBinding(read, commit, setter),
|
|
2693
3359
|
valueSource: 'input()',
|
|
2694
|
-
|
|
3360
|
+
// The staged files are captured BEFORE either list is cleared. `setInput('')`
|
|
3361
|
+
// stays last so the order reads the way it executes.
|
|
3362
|
+
afterValue: [
|
|
3363
|
+
...(attachments
|
|
3364
|
+
? attachmentTurnLines({
|
|
3365
|
+
stagedExpr: 'staged()',
|
|
3366
|
+
clear: ['setStaged([]);'],
|
|
3367
|
+
fromSubmitEvent: false,
|
|
3368
|
+
})
|
|
3369
|
+
: []),
|
|
3370
|
+
`setInput('');`,
|
|
3371
|
+
],
|
|
3372
|
+
...(attachments ? { filesExpr: 'files' } : {}),
|
|
2695
3373
|
mock: isMock,
|
|
2696
3374
|
});
|
|
2697
3375
|
|
|
@@ -2718,6 +3396,17 @@ function renderSolid(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2718
3396
|
]
|
|
2719
3397
|
: [];
|
|
2720
3398
|
|
|
3399
|
+
// The staging state. `toAttachment` is declared INSIDE App() here, unlike the
|
|
3400
|
+
// kai-* targets: this file's module scope is where `renderPart` and the card
|
|
3401
|
+
// registry live, and nothing outside the component needs it.
|
|
3402
|
+
const attachmentInit = attachments
|
|
3403
|
+
? [
|
|
3404
|
+
...ATTACHMENT_WIRE_NOTE.map((l) => ` ${l}`),
|
|
3405
|
+
` const [staged, setStaged] = createSignal<AttachmentData[]>([]);`,
|
|
3406
|
+
...fileToAttachmentLines(' ', 'AttachmentData'),
|
|
3407
|
+
]
|
|
3408
|
+
: [];
|
|
3409
|
+
|
|
2721
3410
|
const sourcesInit = hasSources
|
|
2722
3411
|
? [
|
|
2723
3412
|
` // Replace sampleSources with your real source data.`,
|
|
@@ -2802,6 +3491,37 @@ function renderSolid(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2802
3491
|
` </For>`,
|
|
2803
3492
|
` </div>`,
|
|
2804
3493
|
` </Show>`,
|
|
3494
|
+
...(attachments
|
|
3495
|
+
? [
|
|
3496
|
+
` {/* Drop files here to stage them for the NEXT message. */}`,
|
|
3497
|
+
` <div class="flex flex-col gap-2 pb-3">`,
|
|
3498
|
+
` {/* Read every file BEFORE appending: a per-file append would order`,
|
|
3499
|
+
` the list by whichever finished reading first. */}`,
|
|
3500
|
+
` <FileUpload`,
|
|
3501
|
+
` accept="image/*,application/pdf"`,
|
|
3502
|
+
` onFilesAdded={async (files) => setStaged([...staged(), ...(await Promise.all(files.map(toAttachment)))])}`,
|
|
3503
|
+
` >`,
|
|
3504
|
+
` <FileUploadTrigger class="border-border text-muted-foreground w-full rounded-xl border border-dashed px-4 py-3 text-center text-sm">`,
|
|
3505
|
+
` Click or drop files to attach`,
|
|
3506
|
+
` </FileUploadTrigger>`,
|
|
3507
|
+
` </FileUpload>`,
|
|
3508
|
+
` {/* The staged chips. onRemove is what makes <AttachmentRemove> draw its button. */}`,
|
|
3509
|
+
` <Show when={staged().length > 0}>`,
|
|
3510
|
+
` <Attachments variant="inline">`,
|
|
3511
|
+
` <For each={staged()}>`,
|
|
3512
|
+
` {(a) => (`,
|
|
3513
|
+
` <Attachment data={a} onRemove={() => setStaged(staged().filter((x) => x.id !== a.id))}>`,
|
|
3514
|
+
` <AttachmentPreview />`,
|
|
3515
|
+
` <AttachmentInfo />`,
|
|
3516
|
+
` <AttachmentRemove />`,
|
|
3517
|
+
` </Attachment>`,
|
|
3518
|
+
` )}`,
|
|
3519
|
+
` </For>`,
|
|
3520
|
+
` </Attachments>`,
|
|
3521
|
+
` </Show>`,
|
|
3522
|
+
` </div>`,
|
|
3523
|
+
]
|
|
3524
|
+
: []),
|
|
2805
3525
|
` <PromptInput value={input()} onValueChange={setInput} onSubmit={onSubmit} isLoading={loading()}>`,
|
|
2806
3526
|
` <div class="flex flex-col">`,
|
|
2807
3527
|
` <PromptInputTextarea placeholder="Send a message…" class="min-h-[44px] pt-3 pl-4" />`,
|
|
@@ -2872,11 +3592,27 @@ function renderSolid(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2872
3592
|
`// every kit utility class as unused, and the whole UI renders unstyled.`,
|
|
2873
3593
|
`// (theme.css here, not theme.tokens.css: this app compiles Tailwind itself.)`,
|
|
2874
3594
|
`import { For, Index, Match, Show, Switch, createMemo, createSignal } from 'solid-js';`,
|
|
3595
|
+
// '@kitn.ai/ui/solid', NOT the root '@kitn.ai/ui', and the difference is
|
|
3596
|
+
// invisible to every compiler on this repo's critical path: src/solid.ts is
|
|
3597
|
+
// `export * from './index'`, so ./solid is a strict SUPERSET of the root and
|
|
3598
|
+
// both specifiers typecheck identically. verify:scaffold's solid project
|
|
3599
|
+
// compiled clean the whole time this was wrong.
|
|
3600
|
+
//
|
|
3601
|
+
// The root is the shared layer EVERY consumer resolves — React, Vue, Svelte
|
|
3602
|
+
// and vanilla included — so it deliberately carries only the chat components,
|
|
3603
|
+
// types and helpers those frameworks can use. The rest of the Solid catalog
|
|
3604
|
+
// lives on ./solid, built as its own bundle so a React app never pays for it.
|
|
3605
|
+
// A Solid app pointed at the root therefore gets the smaller surface and finds
|
|
3606
|
+
// out the hard way, one missing export at a time. The published guide says so
|
|
3607
|
+
// outright ("Import Solid components from @kitn.ai/ui/solid, not from the root
|
|
3608
|
+
// @kitn.ai/ui"), and this generator contradicted it for every Solid scaffold.
|
|
2875
3609
|
`import {`,
|
|
2876
3610
|
...componentImports.map((n) => ` ${n},`),
|
|
2877
|
-
`} from '@kitn.ai/ui';`,
|
|
3611
|
+
`} from '@kitn.ai/ui/solid';`,
|
|
2878
3612
|
`// The kit's own types, from the same entry the components come from.`,
|
|
2879
|
-
`import type {
|
|
3613
|
+
`import type { ${attachments
|
|
3614
|
+
? 'AttachmentData, ChatMessage, MessagePart, MessageSource'
|
|
3615
|
+
: 'ChatMessage, MessagePart, MessageSource'} } from '@kitn.ai/ui/solid';`,
|
|
2880
3616
|
...wireImportLines({
|
|
2881
3617
|
typed: false,
|
|
2882
3618
|
toolLoop: emitToolLoop,
|
|
@@ -3080,6 +3816,7 @@ function renderSolid(components: readonly string[], ctx: RenderCtx): string {
|
|
|
3080
3816
|
` // value, no row moves, and the content updates through accessors instead.`,
|
|
3081
3817
|
` const messageKeys = createMemo(() => messages().map((m) => m.id));`,
|
|
3082
3818
|
` const suggestions = ${jsArray(suggestions)};`,
|
|
3819
|
+
...attachmentInit,
|
|
3083
3820
|
...sourcesInit,
|
|
3084
3821
|
...modelInit,
|
|
3085
3822
|
...toolsLines,
|
|
@@ -3159,7 +3896,7 @@ export interface SurfaceRequest {
|
|
|
3159
3896
|
* The `kai-*` components this surface composes — THE AXIS, not an archetype id.
|
|
3160
3897
|
*
|
|
3161
3898
|
* A list rather than a preset name is the whole point of this function. The six
|
|
3162
|
-
* archetypes are
|
|
3899
|
+
* archetypes are seven points in this space and the CLI's feature multi-select
|
|
3163
3900
|
* reaches the rest of it, so a renderer keyed on a preset id could only ever
|
|
3164
3901
|
* emit what someone had already thought to name. `kai-chat` is expected to be
|
|
3165
3902
|
* present; everything else is a capability.
|
|
@@ -3718,6 +4455,85 @@ const CHAT_REQUEST_BODY_DECL = [
|
|
|
3718
4455
|
`}`,
|
|
3719
4456
|
];
|
|
3720
4457
|
|
|
4458
|
+
/**
|
|
4459
|
+
* Attachments, on the way IN.
|
|
4460
|
+
*
|
|
4461
|
+
* A user turn's `content` is a plain string until it carries a file, at which
|
|
4462
|
+
* point `toOpenAIMessages` emits the ARRAY form. Every route that re-maps
|
|
4463
|
+
* messages into some other SDK's shape has to handle both, and the three that do
|
|
4464
|
+
* (anthropic, mastra, vercel-ai-sdk) were each written when only the string form
|
|
4465
|
+
* existed — so each one would have quietly dropped the attachment while still
|
|
4466
|
+
* compiling, which is the same defect the encoder was just fixed for.
|
|
4467
|
+
*
|
|
4468
|
+
* These two helpers are the shared half of that: flattening the wire shape is
|
|
4469
|
+
* identical everywhere, while the target shape is not, so each route maps
|
|
4470
|
+
* `WirePart[]` into its own SDK itself rather than inheriting a lowest common
|
|
4471
|
+
* denominator.
|
|
4472
|
+
*
|
|
4473
|
+
* Only injected into routes that actually call them — the eight pass-through
|
|
4474
|
+
* integrations forward `messages` untouched and need none of this, and an unused
|
|
4475
|
+
* declaration is a hard error under the gate's `--noUnusedLocals`.
|
|
4476
|
+
*/
|
|
4477
|
+
const CONTENT_PARTS_DECL = [
|
|
4478
|
+
`/** Where an attachment's bytes are: inline base64, or an address the PROVIDER`,
|
|
4479
|
+
` * fetches. Never both. */`,
|
|
4480
|
+
`type WireFileSource = { type: 'data'; data: string } | { type: 'url'; url: string };`,
|
|
4481
|
+
``,
|
|
4482
|
+
`/** One piece of a turn, with the string and array content forms flattened into`,
|
|
4483
|
+
` * a single shape. */`,
|
|
4484
|
+
`type WirePart =`,
|
|
4485
|
+
` | { kind: 'text'; text: string }`,
|
|
4486
|
+
` | { kind: 'file'; mediaType: string; filename?: string; source: WireFileSource };`,
|
|
4487
|
+
``,
|
|
4488
|
+
`const DATA_URI = /^data:([^;,]+);base64,([\\s\\S]*)$/;`,
|
|
4489
|
+
``,
|
|
4490
|
+
`/**`,
|
|
4491
|
+
` * Flatten a wire message's content into parts.`,
|
|
4492
|
+
` *`,
|
|
4493
|
+
` * An image sent by URL has no media type here — \`image_url\` carries only the`,
|
|
4494
|
+
` * address — so it reports the top-level segment \`'image'\`, which is all a URL`,
|
|
4495
|
+
` * source needs. Only images can reach that branch: the kit refuses to encode a`,
|
|
4496
|
+
` * remote PDF rather than guess at one.`,
|
|
4497
|
+
` */`,
|
|
4498
|
+
`function wireParts(content: OpenAIWireMessage['content']): WirePart[] {`,
|
|
4499
|
+
` if (content == null) return [];`,
|
|
4500
|
+
` if (typeof content === 'string') return content === '' ? [] : [{ kind: 'text', text: content }];`,
|
|
4501
|
+
` return content.map((part): WirePart => {`,
|
|
4502
|
+
` if (part.type === 'text') return { kind: 'text', text: part.text };`,
|
|
4503
|
+
` if (part.type === 'image_url') {`,
|
|
4504
|
+
` const asData = DATA_URI.exec(part.image_url.url);`,
|
|
4505
|
+
` return asData`,
|
|
4506
|
+
` ? { kind: 'file', mediaType: asData[1], source: { type: 'data', data: asData[2] } }`,
|
|
4507
|
+
` : { kind: 'file', mediaType: 'image', source: { type: 'url', url: part.image_url.url } };`,
|
|
4508
|
+
` }`,
|
|
4509
|
+
` const asData = DATA_URI.exec(part.file.file_data);`,
|
|
4510
|
+
` if (!asData) {`,
|
|
4511
|
+
` // LOUD on purpose. \`file_data\` is a data URI on this wire; anything else`,
|
|
4512
|
+
` // cannot be turned into bytes without fetching it, and forwarding a turn`,
|
|
4513
|
+
` // with the attachment quietly missing is the bug this whole path exists`,
|
|
4514
|
+
` // to prevent.`,
|
|
4515
|
+
` throw new Error(`,
|
|
4516
|
+
` 'Unsupported file content part: file_data must be a data: URI of the form data:<media type>;base64,<data>.',`,
|
|
4517
|
+
` );`,
|
|
4518
|
+
` }`,
|
|
4519
|
+
` return {`,
|
|
4520
|
+
` kind: 'file',`,
|
|
4521
|
+
` mediaType: asData[1],`,
|
|
4522
|
+
` filename: part.file.filename,`,
|
|
4523
|
+
` source: { type: 'data', data: asData[2] },`,
|
|
4524
|
+
` };`,
|
|
4525
|
+
` });`,
|
|
4526
|
+
`}`,
|
|
4527
|
+
``,
|
|
4528
|
+
`/** Just the text of a turn. System, assistant and tool messages are text-only`,
|
|
4529
|
+
` * on this wire, so this collapses the array form for them. */`,
|
|
4530
|
+
`function wireText(content: OpenAIWireMessage['content']): string {`,
|
|
4531
|
+
` return wireParts(content)`,
|
|
4532
|
+
` .map((p) => (p.kind === 'text' ? p.text : ''))`,
|
|
4533
|
+
` .join('');`,
|
|
4534
|
+
`}`,
|
|
4535
|
+
];
|
|
4536
|
+
|
|
3721
4537
|
/**
|
|
3722
4538
|
* Slot the body type in just above `chatHandler`.
|
|
3723
4539
|
*
|
|
@@ -3728,11 +4544,16 @@ const CHAT_REQUEST_BODY_DECL = [
|
|
|
3728
4544
|
* written it.
|
|
3729
4545
|
*/
|
|
3730
4546
|
function withChatRequestBody(fragment: string): string {
|
|
4547
|
+
// The content helpers ride along only where the route calls them; see
|
|
4548
|
+
// CONTENT_PARTS_DECL for why an unconditional injection would not compile.
|
|
4549
|
+
const decl = /\bwire(?:Parts|Text)\s*\(/.test(fragment)
|
|
4550
|
+
? [...CHAT_REQUEST_BODY_DECL, ``, ...CONTENT_PARTS_DECL]
|
|
4551
|
+
: CHAT_REQUEST_BODY_DECL;
|
|
3731
4552
|
const lines = fragment.split('\n');
|
|
3732
4553
|
let at = lines.findIndex((l) => /^(?:export\s+)?async function chatHandler\b/.test(l));
|
|
3733
|
-
if (at < 0) return [...
|
|
4554
|
+
if (at < 0) return [...decl, ``, ...lines].join('\n');
|
|
3734
4555
|
while (at > 0 && /^\s*(?:\/\/|\/\*|\*)/.test(lines[at - 1])) at -= 1;
|
|
3735
|
-
return [...lines.slice(0, at), ...
|
|
4556
|
+
return [...lines.slice(0, at), ...decl, ``, ...lines.slice(at)].join('\n');
|
|
3736
4557
|
}
|
|
3737
4558
|
|
|
3738
4559
|
/** Wrap an integration's portable handler in the target framework's declaration. */
|
|
@@ -4342,7 +5163,7 @@ function rejectUseCase(id: string): string {
|
|
|
4342
5163
|
``,
|
|
4343
5164
|
`Valid useCases (presets): ${valid}.`,
|
|
4344
5165
|
``,
|
|
4345
|
-
// The presets are
|
|
5166
|
+
// The presets are seven points, not the space. A harness that only ever learns
|
|
4346
5167
|
// the six ids will ask for the nearest one instead of the surface it wants,
|
|
4347
5168
|
// so the rejection that teaches the id list is the right place to say so.
|
|
4348
5169
|
`These are PRESETS over the real axis, which is \`components\`. To compose a surface no`,
|
|
@@ -4368,14 +5189,14 @@ export const scaffold: Tool = {
|
|
|
4368
5189
|
// catalogs to discover valid ids.
|
|
4369
5190
|
//
|
|
4370
5191
|
// `useCase` is OPTIONAL because `components` can carry the surface instead —
|
|
4371
|
-
// the archetypes are
|
|
5192
|
+
// the archetypes are seven points in the components space, not the space. A
|
|
4372
5193
|
// request must still name one of the two, and the handler says so when it
|
|
4373
5194
|
// names neither.
|
|
4374
5195
|
useCase: z
|
|
4375
5196
|
.string()
|
|
4376
5197
|
.optional()
|
|
4377
5198
|
.describe(
|
|
4378
|
-
'Archetype PRESET id, e.g. "drop-in-chat", "support-widget", "knowledge-base", "agentic", "workspace", "voice". ' +
|
|
5199
|
+
'Archetype PRESET id, e.g. "drop-in-chat", "support-widget", "knowledge-base", "agentic", "workspace", "voice", "attachments". ' +
|
|
4379
5200
|
'Shorthand for the preset\'s `components`. Omit it and pass `components` to compose a surface no preset names.',
|
|
4380
5201
|
),
|
|
4381
5202
|
components: z
|
|
@@ -4383,7 +5204,7 @@ export const scaffold: Tool = {
|
|
|
4383
5204
|
.optional()
|
|
4384
5205
|
.describe(
|
|
4385
5206
|
'The kai-* components this surface composes, e.g. ["kai-chat", "kai-tool", "kai-reasoning", "kai-artifact", "kai-resizable"]. ' +
|
|
4386
|
-
'The real axis: any combination is renderable, not just the
|
|
5207
|
+
'The real axis: any combination is renderable, not just the seven presets. Include "kai-chat". Wins over `useCase` when both are given.',
|
|
4387
5208
|
),
|
|
4388
5209
|
integration: z
|
|
4389
5210
|
.string()
|