@kitn.ai/ui 0.21.0 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +890 -78
- 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 +4 -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 +913 -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 +234 -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
|
@@ -8,6 +8,12 @@ import {
|
|
|
8
8
|
listArchetypes,
|
|
9
9
|
listIntegrations,
|
|
10
10
|
} from '../../registry';
|
|
11
|
+
// The kit's media-type declaration, read rather than restated. This is the one
|
|
12
|
+
// import in agent-tooling that reaches outside itself, and the module it reaches
|
|
13
|
+
// for is the reason: `wire/media-types.ts` is pure (no I/O, no DOM, no solid-js),
|
|
14
|
+
// so the Node MCP pass typechecks and bundles it unchanged. See
|
|
15
|
+
// `ATTACHMENT_ACCEPT`.
|
|
16
|
+
import { encodableMediaTypes } from '../../../wire/media-types';
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* scaffold — the keystone tool. Composes a working chat surface from four axes:
|
|
@@ -310,6 +316,20 @@ function realStreamBody(opts: {
|
|
|
310
316
|
valueSource?: string;
|
|
311
317
|
/** lines emitted right after the value is read and guarded */
|
|
312
318
|
afterValue?: string[];
|
|
319
|
+
/**
|
|
320
|
+
* The attachments capability: an EXPRESSION producing this turn's staged
|
|
321
|
+
* attachments, which become `file` parts ahead of the message's text part.
|
|
322
|
+
*
|
|
323
|
+
* An expression rather than a flag because every framework holds the list
|
|
324
|
+
* somewhere different (a module-scope `let`, a signal, a rune), and it is
|
|
325
|
+
* evaluated exactly once here — in `afterValue` each renderer has already
|
|
326
|
+
* captured it into a local, because the same lines then CLEAR the staging list
|
|
327
|
+
* and a lazily-read accessor would come back empty.
|
|
328
|
+
*
|
|
329
|
+
* It sits in `parts` before the text so the thread renders the files above the
|
|
330
|
+
* message body, which is where a composer's own attachment chips sit.
|
|
331
|
+
*/
|
|
332
|
+
filesExpr?: string;
|
|
313
333
|
/**
|
|
314
334
|
* The `mock` integration. Swaps ONLY the source of the stream — the canned
|
|
315
335
|
* responder instead of `fetch('/api/chat')` — and leaves every other line
|
|
@@ -319,14 +339,23 @@ function realStreamBody(opts: {
|
|
|
319
339
|
}): string {
|
|
320
340
|
const {
|
|
321
341
|
pad, read, commitSet, setterAdapter, setLoading, bodyPayload, strictRoles = false, toolLoop, thread,
|
|
322
|
-
cards = false, valueSource = 'e.detail.value', afterValue = [], mock = false,
|
|
342
|
+
cards = false, valueSource = 'e.detail.value', afterValue = [], mock = false, filesExpr,
|
|
323
343
|
} = opts;
|
|
324
344
|
const asConst = strictRoles ? ' as const' : '';
|
|
325
345
|
// Under strict TS an un-annotated array literal widens the part's `type` to
|
|
326
346
|
// `string`, so the later commit fails TS2322. Plain-JS contexts (html) have no
|
|
327
347
|
// type to annotate with.
|
|
328
348
|
const historyType = strictRoles ? ': ChatMessage[]' : '';
|
|
329
|
-
|
|
349
|
+
// `type: 'file' as const` even though every call site here is contextually
|
|
350
|
+
// typed by a ChatMessage[] target: the spread's element type is computed from
|
|
351
|
+
// the callback's own return, BEFORE the surrounding array literal's contextual
|
|
352
|
+
// type reaches it, so without the assertion `type` widens to string and the
|
|
353
|
+
// whole assignment fails TS2322.
|
|
354
|
+
const fileParts = filesExpr
|
|
355
|
+
? `...${filesExpr}.map((attachment) => ({ type: 'file'${asConst}, attachment })), `
|
|
356
|
+
: '';
|
|
357
|
+
const userMessage =
|
|
358
|
+
`{ id: crypto.randomUUID(), role: 'user'${asConst}, parts: [${fileParts}{ type: 'text', text: value }] }`;
|
|
330
359
|
|
|
331
360
|
const open = toolLoop
|
|
332
361
|
? thread.open({ pad, userMessage, typed: strictRoles })
|
|
@@ -631,7 +660,7 @@ function assertCardToolFormat(integration: Integration): 'openai' | 'anthropic'
|
|
|
631
660
|
*
|
|
632
661
|
* KEYED ON A COMPONENTS LIST, not an archetype id, for the same reason
|
|
633
662
|
* `renderSurface` is. A guard that took an archetype id could only ever check the
|
|
634
|
-
*
|
|
663
|
+
* seven presets, so the surfaces the feature multi-select makes reachable — the ones
|
|
635
664
|
* with no preset — would emit cards with nothing asserting they did. The archetype
|
|
636
665
|
* axis was also the narrower one: `getArchetype` returns `undefined` for anything
|
|
637
666
|
* not in the catalog, so the guard could not have been pointed at a new surface
|
|
@@ -691,13 +720,29 @@ function wireImportLines(opts: {
|
|
|
691
720
|
cardTools?: boolean;
|
|
692
721
|
/** the `mock` integration → import the shared responder, not a fetch encoder */
|
|
693
722
|
mock?: boolean;
|
|
723
|
+
/**
|
|
724
|
+
* The attachments capability → `type AttachmentData`.
|
|
725
|
+
*
|
|
726
|
+
* The kit exports it from `@kitn.ai/ui/state` precisely because it is the type
|
|
727
|
+
* BOTH ends of this feature hold: `<kai-attachments>.items` and the
|
|
728
|
+
* `{ type: 'file', attachment }` MessagePart. Naming it is better than deriving
|
|
729
|
+
* a structural equivalent per framework, which is what the first cut did — a
|
|
730
|
+
* `NonNullable<AttachmentsProps['items']>[number]` is the same type spelled so
|
|
731
|
+
* that only React can spell it.
|
|
732
|
+
*
|
|
733
|
+
* Independent of `typed` on purpose: `solid` takes `ChatMessage` from
|
|
734
|
+
* `@kitn.ai/ui` alongside its components (so `typed` is false there) and still
|
|
735
|
+
* needs this one from the state entry.
|
|
736
|
+
*/
|
|
737
|
+
attachments?: boolean;
|
|
694
738
|
}): string[] {
|
|
695
|
-
const { pad = '', typed, toolLoop = false, setMessagesType = false, cards = false, cardTools: emitsCardTools = false, mock = false } = opts;
|
|
739
|
+
const { pad = '', typed, toolLoop = false, setMessagesType = false, cards = false, cardTools: emitsCardTools = false, mock = false, attachments = false } = opts;
|
|
696
740
|
const stateNames = [
|
|
697
741
|
'createAssistantStream',
|
|
698
742
|
// The mock's canned reply comes from the kit, not from a copy pasted into
|
|
699
743
|
// this file. One implementation, shared with create-kai and the starters.
|
|
700
744
|
...(mock ? ['createMockResponder'] : []),
|
|
745
|
+
...(attachments ? ['type AttachmentData'] : []),
|
|
701
746
|
...(typed ? ['type ChatMessage'] : []),
|
|
702
747
|
...(typed && setMessagesType ? ['type SetMessages'] : []),
|
|
703
748
|
].join(', ');
|
|
@@ -1014,6 +1059,264 @@ function isWorkspace(components: readonly string[]): boolean {
|
|
|
1014
1059
|
return components.includes('kai-resizable') && components.includes('kai-artifact');
|
|
1015
1060
|
}
|
|
1016
1061
|
|
|
1062
|
+
// ── attachments ───────────────────────────────────────────────────────────────
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* The attachment staging pair. Structural in the same sense the workspace tags
|
|
1066
|
+
* are: neither is emitted as a bare sibling with nothing wired to it, because
|
|
1067
|
+
* neither does anything on its own.
|
|
1068
|
+
*
|
|
1069
|
+
* `<kai-file-upload>` is a dropzone that only EMITS (`kai-files-added`, carrying
|
|
1070
|
+
* `File[]`); it holds no list. `<kai-attachments>` only RENDERS what its `items`
|
|
1071
|
+
* property is set to, and an unset `items` draws the empty state. So the two
|
|
1072
|
+
* bare tags the generic companion fall-through used to emit were a dropzone
|
|
1073
|
+
* whose files went nowhere and a list that could never fill.
|
|
1074
|
+
*/
|
|
1075
|
+
const ATTACHMENT_TAGS = new Set(['kai-file-upload', 'kai-attachments']);
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* True when the surface stages attachments — BOTH tags, for the same reason
|
|
1079
|
+
* `isWorkspace` needs both.
|
|
1080
|
+
*
|
|
1081
|
+
* They are one capability group in `archetypes.ts`, so in practice a request
|
|
1082
|
+
* carries both or neither. Requiring both here is what keeps a hand-built
|
|
1083
|
+
* `components: ['kai-chat', 'kai-attachments']` from reaching the wiring below
|
|
1084
|
+
* and emitting a `kai-remove` listener for a list nothing can add to.
|
|
1085
|
+
*/
|
|
1086
|
+
function hasAttachments(components: readonly string[]): boolean {
|
|
1087
|
+
return components.includes('kai-file-upload') && components.includes('kai-attachments');
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* ★ THE EMITTED `accept`, DERIVED. The seven renderers below interpolate this;
|
|
1092
|
+
* none of them writes a media type.
|
|
1093
|
+
*
|
|
1094
|
+
* It used to be the literal `image/*,application/pdf` in all seven, which was
|
|
1095
|
+
* right when only images and PDFs could be encoded and silently stopped being
|
|
1096
|
+
* right when #190 made `text/*`, `application/json`, `application/xml` and YAML
|
|
1097
|
+
* encodable. The list did not become wrong in this file — it became wrong in
|
|
1098
|
+
* every app scaffolded from it, which is the worst place for a copy to live and
|
|
1099
|
+
* exactly what `wire/media-types.ts` exists to prevent. An `accept` attribute is
|
|
1100
|
+
* a harder case than the prose in `ATTACHMENT_WIRE_NOTE` only in that a comment
|
|
1101
|
+
* can point at a function and an attribute has to CONTAIN the answer; the fix is
|
|
1102
|
+
* the same either way, which is to read the declaration rather than restate it.
|
|
1103
|
+
*
|
|
1104
|
+
* WHY EMIT IT AT ALL, RATHER THAN NOTHING. An absent filter means the full
|
|
1105
|
+
* capability set to `resolveMediaPolicy`, so omitting looks equivalent and is
|
|
1106
|
+
* not, because the tags emitted here never call it. `<kai-file-upload>` (and the
|
|
1107
|
+
* Solid `<FileUpload>`) pass `accept` straight to `<input type="file">` and do no
|
|
1108
|
+
* JS filtering at all — unlike `<kai-chat accept>`, which resolves the policy in
|
|
1109
|
+
* `default-input.tsx`. So on THESE tags an absent attribute is not the capability
|
|
1110
|
+
* set, it is wider than the capability set: the dialog offers `.zip`, the emitted
|
|
1111
|
+
* `toAttachment` stages it, and the encoder throws on a file the picker
|
|
1112
|
+
* volunteered. Deriving the attribute is the only form that tracks the
|
|
1113
|
+
* declaration; omitting it is a third behaviour that matches neither layer.
|
|
1114
|
+
*
|
|
1115
|
+
* IT IS A STEER, NOT A GATE, and nothing here should be read as claiming
|
|
1116
|
+
* otherwise. `accept` filters the OS dialog, which always offers an "All Files"
|
|
1117
|
+
* escape, and it does not apply to drag-and-drop — `file-upload.tsx` hands
|
|
1118
|
+
* `dataTransfer.files` straight to `onFilesAdded`. A scaffolded app that wants a
|
|
1119
|
+
* filter that HOLDS calls `resolveMediaPolicy().decide()` on what it staged; the
|
|
1120
|
+
* emitted note points at it.
|
|
1121
|
+
*/
|
|
1122
|
+
const ATTACHMENT_ACCEPT = encodableMediaTypes().join(',');
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* What the scaffolder INTENDS to emit for one components list, so a guard can
|
|
1126
|
+
* check the seven renderers against the decision instead of restating it.
|
|
1127
|
+
*
|
|
1128
|
+
* The sibling of `cardEmitPlan`, and exported for the same reason:
|
|
1129
|
+
* `attachmentStagingCheck` in scripts/verify-scaffold-compiles.mjs would
|
|
1130
|
+
* otherwise re-derive "both attachment tags are present" itself, and a guard that
|
|
1131
|
+
* re-derives its own expectation stops being a check of anything the day the two
|
|
1132
|
+
* derivations disagree — it just agrees with itself.
|
|
1133
|
+
*
|
|
1134
|
+
* Unlike `cardEmitPlan` this takes no integration, and that is a real difference
|
|
1135
|
+
* rather than an omission: staging a file needs no model, no tools array and no
|
|
1136
|
+
* route, so `mock` emits exactly what `openai` does. What the wire then makes of
|
|
1137
|
+
* the files is decided by the encoders and by the media-type declaration behind
|
|
1138
|
+
* them, which is the same answer for every integration (see
|
|
1139
|
+
* `ATTACHMENT_WIRE_NOTE`).
|
|
1140
|
+
*/
|
|
1141
|
+
export function attachmentEmitPlan(components: readonly string[]): { staging: boolean } {
|
|
1142
|
+
return { staging: hasAttachments(components) };
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* What the emitted attachment surface does, and — the part that matters — what
|
|
1147
|
+
* it does not.
|
|
1148
|
+
*
|
|
1149
|
+
* It stages AND it sends. `toOpenAIMessages` / `toAnthropicMessages` encode
|
|
1150
|
+
* `file` parts into the content blocks each API takes, so a staged file reaches
|
|
1151
|
+
* the model. That is worth stating because it was false for a long time and the
|
|
1152
|
+
* failure was silent: before #186 an attachment-only turn encoded to nothing at
|
|
1153
|
+
* all and was dropped from the request, so a scaffold could put a paperclip on
|
|
1154
|
+
* screen, accept a PDF, render it in the thread, and send the model a message
|
|
1155
|
+
* that never mentioned it.
|
|
1156
|
+
*
|
|
1157
|
+
* WHAT IT STILL DOES NOT DO. Both remaining limits are facts about the APIs, not
|
|
1158
|
+
* gaps in this scaffold, which is why the emitted note can state them plainly:
|
|
1159
|
+
* · Neither API has an arbitrary-file content block, so a media type outside
|
|
1160
|
+
* the kit's set — a `.zip` is the one people try — has no representation on
|
|
1161
|
+
* either wire and throws at encode time rather than going missing.
|
|
1162
|
+
* · A REMOTE text file is refused. Text has to ride as text CONTENT (no wire
|
|
1163
|
+
* has a URL form for it), so inlining one would mean fetching it, and
|
|
1164
|
+
* `wire/` does no I/O by design. Stage a `data:` URI instead.
|
|
1165
|
+
* A file the browser could not name is NOT on that list any more: it is settled
|
|
1166
|
+
* by decoding its bytes, so the `.rs` and `.toml` files a coding chat is full of
|
|
1167
|
+
* go through even though Chrome hands them over with no media type at all.
|
|
1168
|
+
*
|
|
1169
|
+
* ★ THE EMITTED NOTE MUST NOT NAME MEDIA TYPES, and that is the whole reason it
|
|
1170
|
+
* reads the way it does. It lands in a user's repo, where it outlives any
|
|
1171
|
+
* limitation it describes and where nothing can ever check it — a second copy of
|
|
1172
|
+
* the capability list, at the worst possible layer. It points at
|
|
1173
|
+
* `encodableMediaTypes()` instead, which is public precisely so that nobody has
|
|
1174
|
+
* to hardcode the set. A guard in `scaffold.test.ts` fails if a media type
|
|
1175
|
+
* creeps back into it.
|
|
1176
|
+
*
|
|
1177
|
+
* This very comment has been wrong before, in the way that matters most: #186
|
|
1178
|
+
* corrected the emitted string and left the explanation above it teaching the
|
|
1179
|
+
* reverse, so the scaffolder's own documentation said files never reach the
|
|
1180
|
+
* model while the code it emitted said they do. Anything written here that a
|
|
1181
|
+
* script cannot re-derive is a claim with a shelf life.
|
|
1182
|
+
*/
|
|
1183
|
+
export const ATTACHMENT_WIRE_NOTE = [
|
|
1184
|
+
'// The staged files ride along on the message as `file` parts, so they RENDER',
|
|
1185
|
+
'// in the thread AND reach the model: toOpenAIMessages / toAnthropicMessages',
|
|
1186
|
+
'// encode each one into the content block its API takes.',
|
|
1187
|
+
'// Which is why toAttachment below stages a `data:` URI rather than a',
|
|
1188
|
+
'// URL.createObjectURL blob: an object URL previews fine and resolves ONLY',
|
|
1189
|
+
'// inside this tab, so the encoder rejects it rather than send an address the',
|
|
1190
|
+
'// provider cannot fetch.',
|
|
1191
|
+
'// What can be sent is not a list worth copying into a comment -- it moves, and',
|
|
1192
|
+
'// a copy cannot. `encodableMediaTypes()` from "@kitn.ai/ui/wire" IS the set,',
|
|
1193
|
+
'// and `resolveMediaPolicy().decide(mediaType)` answers for one file. Anything',
|
|
1194
|
+
'// outside it throws at encode time instead of going missing from the request.',
|
|
1195
|
+
'// A file the browser could not name (a .rs, a .toml) is settled by decoding',
|
|
1196
|
+
'// its bytes, never by its extension.',
|
|
1197
|
+
];
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* The `File` -> `AttachmentData` conversion, emitted as a real function.
|
|
1201
|
+
*
|
|
1202
|
+
* `<kai-file-upload>` hands over `File` objects and `<kai-attachments>` renders
|
|
1203
|
+
* `AttachmentData`, so something has to bridge them and it may as well be code
|
|
1204
|
+
* the consumer can edit.
|
|
1205
|
+
*
|
|
1206
|
+
* It reads the file with `FileReader.readAsDataURL` and stages a `data:` URI —
|
|
1207
|
+
* the same call `readAsDataUrl` in `elements/default-input.tsx` makes, for the
|
|
1208
|
+
* same reason. `URL.createObjectURL` would draw an identical thumbnail and be
|
|
1209
|
+
* meaningless to anything downstream: an object URL resolves only inside the tab
|
|
1210
|
+
* that minted it, so `toOpenAIMessages` / `toAnthropicMessages` refuse it rather
|
|
1211
|
+
* than send a provider an address it cannot fetch. A `data:` URI previews the
|
|
1212
|
+
* same and is the one form both APIs actually take. Reading is async, which is
|
|
1213
|
+
* the only reason the emitted function is.
|
|
1214
|
+
*
|
|
1215
|
+
* WHICH files survive that encoding is deliberately not stated here, and the
|
|
1216
|
+
* emitted code does not state it either: `encodableMediaTypes()` is the set, and
|
|
1217
|
+
* is public precisely so that nothing has to keep a second copy of it (the
|
|
1218
|
+
* reasoning is on `ATTACHMENT_WIRE_NOTE` above).
|
|
1219
|
+
*
|
|
1220
|
+
* The parameter type is derived from the property it feeds
|
|
1221
|
+
* (`KaiAttachmentsElement['items']` / the wrapper's `items` prop) at each call
|
|
1222
|
+
* site rather than importing `AttachmentData`, for the reason the html target
|
|
1223
|
+
* already derives its message type from `KaiChatElement['messages']`: a type
|
|
1224
|
+
* read off the assignment target cannot drift out of step with it.
|
|
1225
|
+
*
|
|
1226
|
+
* This comment taught the reverse until now, and approvingly — it described the
|
|
1227
|
+
* object URL as what made a preview a real thumbnail and called leaving it
|
|
1228
|
+
* unrevoked a deliberate choice, having gone untouched since #185 while #186
|
|
1229
|
+
* rewrote the function under it. That is the second time in this file the code
|
|
1230
|
+
* was corrected and the prose above it was not; the paragraph on
|
|
1231
|
+
* `ATTACHMENT_WIRE_NOTE` records the first. Neither claim had anything to
|
|
1232
|
+
* disagree with, which is the whole hazard of explaining emitted code in prose.
|
|
1233
|
+
*/
|
|
1234
|
+
function fileToAttachmentLines(pad: string, typeName: string): string[] {
|
|
1235
|
+
return [
|
|
1236
|
+
`${pad}/** <kai-file-upload> hands over File objects; <kai-attachments> renders these. */`,
|
|
1237
|
+
`${pad}async function toAttachment(file: File): Promise<${typeName}> {`,
|
|
1238
|
+
`${pad} // A data: URI, NOT URL.createObjectURL. Both preview identically, but an`,
|
|
1239
|
+
`${pad} // object URL resolves only inside this tab, so it can never reach a model —`,
|
|
1240
|
+
`${pad} // the encoder rejects one rather than send an address the provider cannot`,
|
|
1241
|
+
`${pad} // fetch. Reading is async, which is why this function is.`,
|
|
1242
|
+
`${pad} const url = await new Promise<string>((resolve, reject) => {`,
|
|
1243
|
+
`${pad} const reader = new FileReader();`,
|
|
1244
|
+
`${pad} reader.onload = () => resolve(String(reader.result));`,
|
|
1245
|
+
`${pad} reader.onerror = () => reject(reader.error ?? new Error('Could not read ' + file.name));`,
|
|
1246
|
+
`${pad} reader.readAsDataURL(file);`,
|
|
1247
|
+
`${pad} });`,
|
|
1248
|
+
`${pad} return {`,
|
|
1249
|
+
`${pad} id: crypto.randomUUID(),`,
|
|
1250
|
+
`${pad} type: 'file',`,
|
|
1251
|
+
`${pad} filename: file.name,`,
|
|
1252
|
+
`${pad} // '' for an extensionless file, and an empty mediaType would render as a`,
|
|
1253
|
+
`${pad} // blank detail line rather than falling back to the generic icon.`,
|
|
1254
|
+
`${pad} mediaType: file.type || undefined,`,
|
|
1255
|
+
`${pad} url,`,
|
|
1256
|
+
`${pad} };`,
|
|
1257
|
+
`${pad}}`,
|
|
1258
|
+
];
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Why the submit handler merges TWO sources of staged files.
|
|
1263
|
+
*
|
|
1264
|
+
* `<kai-chat>` renders its own paperclip unconditionally — `ChatThread` always
|
|
1265
|
+
* passes `onAttachmentsChange` to the composer, and there is no prop on the
|
|
1266
|
+
* element that turns it off — so every scaffold ALREADY has an attach button,
|
|
1267
|
+
* and every scaffold before this one dropped whatever was attached through it on
|
|
1268
|
+
* the floor: `kai-submit` carries `{ value, attachments }` and every emitted
|
|
1269
|
+
* handler read only `.value`.
|
|
1270
|
+
*
|
|
1271
|
+
* So reading only the dropzone would leave that bug in place on the one surface
|
|
1272
|
+
* that is supposed to be about attachments. Both are folded in.
|
|
1273
|
+
*/
|
|
1274
|
+
const ATTACHMENT_MERGE_NOTE = [
|
|
1275
|
+
"// <kai-chat> renders its OWN paperclip (there is no prop that hides it), and its",
|
|
1276
|
+
'// staged files arrive on this event as detail.attachments. The dropzone above is',
|
|
1277
|
+
'// the second source. Both are folded in — reading either one alone silently',
|
|
1278
|
+
'// drops files the user really attached.',
|
|
1279
|
+
];
|
|
1280
|
+
|
|
1281
|
+
/**
|
|
1282
|
+
* The lines every framework emits at the TOP of a submit, before the message is
|
|
1283
|
+
* built: capture this turn's attachments into `files`, then empty the staging
|
|
1284
|
+
* list.
|
|
1285
|
+
*
|
|
1286
|
+
* They go through `afterValue` rather than into the message expression because
|
|
1287
|
+
* the order is load-bearing and easy to get backwards — `filesExpr` is spliced
|
|
1288
|
+
* into `parts` AFTER these run, so the capture has to be into a local. Reading
|
|
1289
|
+
* the staging list lazily from `parts` would read it post-clear and attach
|
|
1290
|
+
* nothing, which is precisely the silent failure this whole capability is being
|
|
1291
|
+
* added to stop.
|
|
1292
|
+
*
|
|
1293
|
+
* `fromSubmitEvent` is false for exactly one target: `solid` renders the SolidJS
|
|
1294
|
+
* `PromptInput` primitives directly rather than `<kai-chat>`, so there is no
|
|
1295
|
+
* `kai-submit` event and no second source to merge — its dropzone is the only
|
|
1296
|
+
* one. Merging a `detail` that does not exist there would be a runtime TypeError
|
|
1297
|
+
* on every submit.
|
|
1298
|
+
*/
|
|
1299
|
+
function attachmentTurnLines(opts: {
|
|
1300
|
+
/** expression reading the dropzone's staged list */
|
|
1301
|
+
stagedExpr: string;
|
|
1302
|
+
/** statement(s) that empty the staging list (and re-render it) */
|
|
1303
|
+
clear: string[];
|
|
1304
|
+
/** the framework submits through `kai-submit`, so `e.detail.attachments` exists */
|
|
1305
|
+
fromSubmitEvent: boolean;
|
|
1306
|
+
}): string[] {
|
|
1307
|
+
const { stagedExpr, clear, fromSubmitEvent } = opts;
|
|
1308
|
+
return [
|
|
1309
|
+
...(fromSubmitEvent ? ATTACHMENT_MERGE_NOTE : [
|
|
1310
|
+
'// This target renders PromptInput directly rather than <kai-chat>, so the',
|
|
1311
|
+
'// dropzone is the only source of staged files — there is no kai-submit detail.',
|
|
1312
|
+
]),
|
|
1313
|
+
fromSubmitEvent
|
|
1314
|
+
? `const files = [...${stagedExpr}, ...e.detail.attachments];`
|
|
1315
|
+
: `const files = [...${stagedExpr}];`,
|
|
1316
|
+
...clear,
|
|
1317
|
+
];
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1017
1320
|
/**
|
|
1018
1321
|
* A sample assistant message showing embedded tool + reasoning.
|
|
1019
1322
|
*
|
|
@@ -1136,15 +1439,37 @@ interface RenderCtx {
|
|
|
1136
1439
|
*
|
|
1137
1440
|
* SCAF-14: workspace structural types (kai-resizable, kai-artifact) are emitted
|
|
1138
1441
|
* as a properly composed split layout — chat in one pane, artifact in the other.
|
|
1442
|
+
*
|
|
1443
|
+
* The attachment pair (kai-file-upload, kai-attachments) is filtered out of the
|
|
1444
|
+
* companion loop for the same reason the workspace pair is: it gets a composed
|
|
1445
|
+
* block of its own, above the chat, with ids the emitted module wires together.
|
|
1139
1446
|
*/
|
|
1140
1447
|
function componentTags(components: readonly string[], chatFill: string): string {
|
|
1141
1448
|
const companionTags = components.filter(
|
|
1142
|
-
(t) =>
|
|
1449
|
+
(t) =>
|
|
1450
|
+
t !== 'kai-chat' &&
|
|
1451
|
+
!MESSAGE_EMBEDDED_TAGS.has(t) &&
|
|
1452
|
+
!WORKSPACE_STRUCTURAL_TAGS.has(t) &&
|
|
1453
|
+
!(hasAttachments(components) && ATTACHMENT_TAGS.has(t)),
|
|
1143
1454
|
);
|
|
1144
1455
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
1145
1456
|
const hasStandaloneCompanions = companionTags.length > 0;
|
|
1146
1457
|
|
|
1147
1458
|
const lines: string[] = [];
|
|
1459
|
+
// The staging strip, ABOVE the thread: drop files in, see them as removable
|
|
1460
|
+
// chips, and they ride out on the next message. `flex: 0 0 auto` keeps it out
|
|
1461
|
+
// of the chat's `flex: 1` claim on the remaining height.
|
|
1462
|
+
if (hasAttachments(components)) {
|
|
1463
|
+
lines.push(
|
|
1464
|
+
` <!-- Drop files here to stage them for the NEXT message. src/main.ts wires`,
|
|
1465
|
+
` kai-files-added -> the staged list -> kai-attachments' items property. -->`,
|
|
1466
|
+
` <div style="flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem;">`,
|
|
1467
|
+
` <kai-file-upload id="upload" accept="${ATTACHMENT_ACCEPT}"></kai-file-upload>`,
|
|
1468
|
+
` <!-- items is a JS PROPERTY (arrays can't be attributes); 'removable' fires kai-remove. -->`,
|
|
1469
|
+
` <kai-attachments id="attachments" variant="inline" removable></kai-attachments>`,
|
|
1470
|
+
` </div>`,
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1148
1473
|
// SCAF-14: workspace is a structural/layout surface — emit a runnable split.
|
|
1149
1474
|
//
|
|
1150
1475
|
// This used to `return` here, which dropped every standalone companion on the
|
|
@@ -1229,6 +1554,49 @@ function componentTags(components: readonly string[], chatFill: string): string
|
|
|
1229
1554
|
function htmlModule(ctx: RenderCtx, components: readonly string[]): string {
|
|
1230
1555
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
1231
1556
|
const hasSources = components.includes('kai-sources');
|
|
1557
|
+
const attachments = hasAttachments(components);
|
|
1558
|
+
|
|
1559
|
+
// MODULE scope, like `model`/`runTool`: the staged list has to outlive `init()`
|
|
1560
|
+
// so the submit handler can read it, and `Staged` is used by the module-scope
|
|
1561
|
+
// `toAttachment` declaration below.
|
|
1562
|
+
const attachmentModuleLines = attachments
|
|
1563
|
+
? [
|
|
1564
|
+
...ATTACHMENT_WIRE_NOTE,
|
|
1565
|
+
`//`,
|
|
1566
|
+
`// AttachmentData is the kit's own type for a staged file — the same one`,
|
|
1567
|
+
`// <kai-attachments>.items holds and the same one a 'file' message part holds.`,
|
|
1568
|
+
`let staged: AttachmentData[] = [];`,
|
|
1569
|
+
``,
|
|
1570
|
+
...fileToAttachmentLines('', 'AttachmentData'),
|
|
1571
|
+
``,
|
|
1572
|
+
]
|
|
1573
|
+
: [];
|
|
1574
|
+
|
|
1575
|
+
const attachmentSetupLines = attachments
|
|
1576
|
+
? [
|
|
1577
|
+
` const uploadEl = document.getElementById('upload') as HTMLElement;`,
|
|
1578
|
+
` const attachmentsEl = document.getElementById('attachments') as KaiAttachmentsElement;`,
|
|
1579
|
+
` // Same upgrade rule as <kai-chat>: 'items' is an array, so it is a PROPERTY,`,
|
|
1580
|
+
` // and a property set before the element upgrades is dropped on upgrade.`,
|
|
1581
|
+
` await customElements.whenDefined('kai-attachments');`,
|
|
1582
|
+
` // A NEW array per write — same contract as chat.messages.`,
|
|
1583
|
+
` const showStaged = () => { attachmentsEl.items = [...staged]; };`,
|
|
1584
|
+
``,
|
|
1585
|
+
` uploadEl.addEventListener('kai-files-added', async (event: Event) => {`,
|
|
1586
|
+
` const { files } = (event as CustomEvent<{ files: File[] }>).detail;`,
|
|
1587
|
+
` // Read every file BEFORE appending: a per-file append would order the`,
|
|
1588
|
+
` // list by whichever finished reading first.`,
|
|
1589
|
+
` staged = [...staged, ...(await Promise.all(files.map(toAttachment)))];`,
|
|
1590
|
+
` showStaged();`,
|
|
1591
|
+
` });`,
|
|
1592
|
+
` attachmentsEl.addEventListener('kai-remove', (event: Event) => {`,
|
|
1593
|
+
` const { id } = (event as CustomEvent<{ id: string }>).detail;`,
|
|
1594
|
+
` staged = staged.filter((a) => a.id !== id);`,
|
|
1595
|
+
` showStaged();`,
|
|
1596
|
+
` });`,
|
|
1597
|
+
``,
|
|
1598
|
+
]
|
|
1599
|
+
: [];
|
|
1232
1600
|
|
|
1233
1601
|
// SCAF-9: the agentic archetype explains where tool + reasoning parts come
|
|
1234
1602
|
// from. It no longer SEEDS a fabricated turn — see `SAMPLE_AGENTIC_MESSAGE`.
|
|
@@ -1272,8 +1640,15 @@ function htmlModule(ctx: RenderCtx, components: readonly string[]): string {
|
|
|
1272
1640
|
|
|
1273
1641
|
// KaiSourcesElement only when a kai-sources companion is really declared: a
|
|
1274
1642
|
// stock vanilla-ts tsconfig sets noUnusedLocals, so an always-on import is a
|
|
1275
|
-
// build error on every other archetype.
|
|
1276
|
-
|
|
1643
|
+
// build error on every other archetype. KaiAttachmentsElement follows the same
|
|
1644
|
+
// rule — the `items =` assignment needs it. KaiFileUploadElement is deliberately
|
|
1645
|
+
// NOT imported: the dropzone is only ever listened to, and `as HTMLElement`
|
|
1646
|
+
// types addEventListener fully.
|
|
1647
|
+
const elementTypes = [
|
|
1648
|
+
'KaiChatElement',
|
|
1649
|
+
...(hasSources ? ['KaiSourcesElement'] : []),
|
|
1650
|
+
...(attachments ? ['KaiAttachmentsElement'] : []),
|
|
1651
|
+
].join(', ');
|
|
1277
1652
|
|
|
1278
1653
|
/**
|
|
1279
1654
|
* Same rule, applied to the kit's own `ChatMessage`.
|
|
@@ -1305,11 +1680,13 @@ function htmlModule(ctx: RenderCtx, components: readonly string[]): string {
|
|
|
1305
1680
|
cards: ctx.emitCards,
|
|
1306
1681
|
cardTools: ctx.cardProvider !== null,
|
|
1307
1682
|
mock: ctx.isMock,
|
|
1683
|
+
attachments,
|
|
1308
1684
|
}),
|
|
1309
1685
|
`import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
|
|
1310
1686
|
``,
|
|
1311
1687
|
...(ctx.isMock ? [...mockResponderInit(), ``] : []),
|
|
1312
1688
|
...modelLines,
|
|
1689
|
+
...attachmentModuleLines,
|
|
1313
1690
|
...cardsLines,
|
|
1314
1691
|
...toolsLines,
|
|
1315
1692
|
...runnerLines,
|
|
@@ -1326,14 +1703,21 @@ function htmlModule(ctx: RenderCtx, components: readonly string[]): string {
|
|
|
1326
1703
|
...cardPropLines,
|
|
1327
1704
|
...seedLines,
|
|
1328
1705
|
...sourcesSetupLines,
|
|
1706
|
+
...attachmentSetupLines,
|
|
1329
1707
|
];
|
|
1330
1708
|
|
|
1331
1709
|
// `Event`, not `CustomEvent`: addEventListener with a custom event name hands
|
|
1332
1710
|
// the listener a plain Event, so the narrowing happens in the body — the same
|
|
1333
1711
|
// shape renderAngular emits, for the same reason.
|
|
1712
|
+
//
|
|
1713
|
+
// `attachments` is on the detail whether or not this surface stages files —
|
|
1714
|
+
// <kai-chat> always sends both — but it is only NAMED in the cast where the
|
|
1715
|
+
// handler reads it, so every other scaffold keeps the narrower type it uses.
|
|
1334
1716
|
const listenerOpen = [
|
|
1335
1717
|
` chat.addEventListener('kai-submit', async (event: Event) => {`,
|
|
1336
|
-
|
|
1718
|
+
attachments
|
|
1719
|
+
? ` const e = event as CustomEvent<{ value: string; attachments: AttachmentData[] }>;`
|
|
1720
|
+
: ` const e = event as CustomEvent<{ value: string }>;`,
|
|
1337
1721
|
];
|
|
1338
1722
|
const footer = [
|
|
1339
1723
|
` });`,
|
|
@@ -1363,6 +1747,16 @@ function htmlModule(ctx: RenderCtx, components: readonly string[]): string {
|
|
|
1363
1747
|
'chat.messages ?? []',
|
|
1364
1748
|
),
|
|
1365
1749
|
mock: ctx.isMock,
|
|
1750
|
+
...(attachments
|
|
1751
|
+
? {
|
|
1752
|
+
filesExpr: 'files',
|
|
1753
|
+
afterValue: attachmentTurnLines({
|
|
1754
|
+
stagedExpr: 'staged',
|
|
1755
|
+
clear: ['staged = [];', 'showStaged();'],
|
|
1756
|
+
fromSubmitEvent: true,
|
|
1757
|
+
}),
|
|
1758
|
+
}
|
|
1759
|
+
: {}),
|
|
1366
1760
|
}),
|
|
1367
1761
|
...footer,
|
|
1368
1762
|
].join('\n');
|
|
@@ -1438,9 +1832,17 @@ function renderJsx(components: readonly string[], ctx: RenderCtx, framework: str
|
|
|
1438
1832
|
const wrapperNames = importTags.map(toPascalCase);
|
|
1439
1833
|
const importList = wrapperNames.join(', ');
|
|
1440
1834
|
|
|
1441
|
-
|
|
1835
|
+
const attachments = hasAttachments(components);
|
|
1836
|
+
|
|
1837
|
+
// SCAF-9: standalone companion tags (not kai-chat, not message-embedded, not
|
|
1838
|
+
// workspace-structural, and not the attachment pair — that gets a composed
|
|
1839
|
+
// block of its own with state behind it).
|
|
1442
1840
|
const standaloneCompanionTags = components.filter(
|
|
1443
|
-
(t) =>
|
|
1841
|
+
(t) =>
|
|
1842
|
+
t !== 'kai-chat' &&
|
|
1843
|
+
!MESSAGE_EMBEDDED_TAGS.has(t) &&
|
|
1844
|
+
!WORKSPACE_STRUCTURAL_TAGS.has(t) &&
|
|
1845
|
+
!(attachments && ATTACHMENT_TAGS.has(t)),
|
|
1444
1846
|
);
|
|
1445
1847
|
|
|
1446
1848
|
// Build companion JSX: only standalone companions with real props.
|
|
@@ -1465,6 +1867,59 @@ function renderJsx(components: readonly string[], ctx: RenderCtx, framework: str
|
|
|
1465
1867
|
}
|
|
1466
1868
|
const companions = companionJsxLines.join('\n');
|
|
1467
1869
|
|
|
1870
|
+
// The staging strip, ABOVE the chat. `<FileUpload>`/`<Attachments>` are the
|
|
1871
|
+
// generated wrappers, already in `importList` because both tags are in
|
|
1872
|
+
// `components` — no extra import bookkeeping, which is the point of keying the
|
|
1873
|
+
// import list off the components rather than off a per-capability table.
|
|
1874
|
+
const attachmentJsx = attachments
|
|
1875
|
+
? [
|
|
1876
|
+
` {/* Drop files here to stage them for the NEXT message. */}`,
|
|
1877
|
+
` <div style={{ flex: '0 0 auto', display: 'flex', flexDirection: 'column', gap: '0.5rem', padding: '0.75rem' }}>`,
|
|
1878
|
+
` <FileUpload accept="${ATTACHMENT_ACCEPT}" onFilesAdded={onFilesAdded} />`,
|
|
1879
|
+
` {/* items is an ARRAY, so the wrapper sets it as a DOM property, never an attribute. */}`,
|
|
1880
|
+
` <Attachments items={staged} variant="inline" removable onRemove={onRemoveAttachment} />`,
|
|
1881
|
+
` </div>`,
|
|
1882
|
+
].join('\n')
|
|
1883
|
+
: '';
|
|
1884
|
+
|
|
1885
|
+
// MODULE scope for `toAttachment` (it closes over nothing); component scope for
|
|
1886
|
+
// the state and the two handlers.
|
|
1887
|
+
const attachmentModuleInit = attachments
|
|
1888
|
+
? [
|
|
1889
|
+
...ATTACHMENT_WIRE_NOTE,
|
|
1890
|
+
`//`,
|
|
1891
|
+
`// AttachmentData is the kit's own type for a staged file — the same one`,
|
|
1892
|
+
`// <Attachments>.items holds and the same one a 'file' message part holds.`,
|
|
1893
|
+
...fileToAttachmentLines('', 'AttachmentData'),
|
|
1894
|
+
]
|
|
1895
|
+
: [];
|
|
1896
|
+
const attachmentStateInit = attachments
|
|
1897
|
+
? [
|
|
1898
|
+
` const [staged, setStaged] = useState<AttachmentData[]>([]);`,
|
|
1899
|
+
` const onFilesAdded = async (e: CustomEvent<{ files: File[] }>) => {`,
|
|
1900
|
+
` // Read every file BEFORE appending: a per-file append would order the`,
|
|
1901
|
+
` // list by whichever finished reading first.`,
|
|
1902
|
+
` const added = await Promise.all(e.detail.files.map(toAttachment));`,
|
|
1903
|
+
` setStaged((prev) => [...prev, ...added]);`,
|
|
1904
|
+
` };`,
|
|
1905
|
+
` const onRemoveAttachment = (e: CustomEvent<{ id: string }>) =>`,
|
|
1906
|
+
` setStaged((prev) => prev.filter((a) => a.id !== e.detail.id));`,
|
|
1907
|
+
].join('\n')
|
|
1908
|
+
: '';
|
|
1909
|
+
const attachmentSubmitOpts = attachments
|
|
1910
|
+
? {
|
|
1911
|
+
filesExpr: 'files',
|
|
1912
|
+
afterValue: attachmentTurnLines({
|
|
1913
|
+
stagedExpr: 'staged',
|
|
1914
|
+
clear: ['setStaged([]);'],
|
|
1915
|
+
fromSubmitEvent: true,
|
|
1916
|
+
}),
|
|
1917
|
+
}
|
|
1918
|
+
: {};
|
|
1919
|
+
const submitParamType = attachments
|
|
1920
|
+
? 'CustomEvent<{ value: string; attachments: AttachmentData[] }>'
|
|
1921
|
+
: 'CustomEvent<{ value: string }>';
|
|
1922
|
+
|
|
1468
1923
|
const mockInit = isMock ? mockResponderInit() : [];
|
|
1469
1924
|
|
|
1470
1925
|
// SCAF-9: no fabricated seed — see SAMPLE_AGENTIC_MESSAGE for the three ways
|
|
@@ -1528,6 +1983,7 @@ function renderJsx(components: readonly string[], ctx: RenderCtx, framework: str
|
|
|
1528
1983
|
cards: ctx.emitCards,
|
|
1529
1984
|
thread: REACT_THREAD,
|
|
1530
1985
|
mock: isMock,
|
|
1986
|
+
...attachmentSubmitOpts,
|
|
1531
1987
|
});
|
|
1532
1988
|
|
|
1533
1989
|
// SCAF-2: Next.js App Router requires 'use client' for components that use hooks/interactivity.
|
|
@@ -1564,6 +2020,7 @@ function renderJsx(components: readonly string[], ctx: RenderCtx, framework: str
|
|
|
1564
2020
|
cards: ctx.emitCards,
|
|
1565
2021
|
cardTools: ctx.cardProvider !== null,
|
|
1566
2022
|
mock: isMock,
|
|
2023
|
+
attachments,
|
|
1567
2024
|
}),
|
|
1568
2025
|
`import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
|
|
1569
2026
|
`// <kai-*> are client-only custom elements (the server has no customElements`,
|
|
@@ -1576,22 +2033,25 @@ function renderJsx(components: readonly string[], ctx: RenderCtx, framework: str
|
|
|
1576
2033
|
...(p.altNote ?? []).map((l) => `// ${l}`),
|
|
1577
2034
|
...mockInit,
|
|
1578
2035
|
``,
|
|
2036
|
+
...attachmentModuleInit,
|
|
1579
2037
|
...cardsInit,
|
|
1580
2038
|
`export default function App() {`,
|
|
1581
2039
|
sampleMessagesInit,
|
|
1582
2040
|
` const [loading, setLoading] = useState(false);`,
|
|
1583
2041
|
` const suggestions = ${jsArray(suggestions)};`,
|
|
2042
|
+
...(attachmentStateInit ? [attachmentStateInit] : []),
|
|
1584
2043
|
...(sampleSourcesInit ? [sampleSourcesInit] : []),
|
|
1585
2044
|
...(modelInit ? [modelInit] : []),
|
|
1586
2045
|
...(toolsInit ? [toolsInit] : []),
|
|
1587
2046
|
...(toolRunner ? [toolRunner] : []),
|
|
1588
2047
|
``,
|
|
1589
|
-
` async function onSubmit(e:
|
|
2048
|
+
` async function onSubmit(e: ${submitParamType}) {`,
|
|
1590
2049
|
onSubmitBody,
|
|
1591
2050
|
` }`,
|
|
1592
2051
|
``,
|
|
1593
2052
|
` return (`,
|
|
1594
2053
|
` <div style={{ ${jsxStyle(p.style)} }}>`,
|
|
2054
|
+
...(attachmentJsx ? [attachmentJsx] : []),
|
|
1595
2055
|
...(workspace
|
|
1596
2056
|
? [
|
|
1597
2057
|
` {/* SCAF-14: workspace split — chat pane left, artifact preview right. */}`,
|
|
@@ -1659,6 +2119,7 @@ function renderJsx(components: readonly string[], ctx: RenderCtx, framework: str
|
|
|
1659
2119
|
cards: ctx.emitCards,
|
|
1660
2120
|
cardTools: ctx.cardProvider !== null,
|
|
1661
2121
|
mock: isMock,
|
|
2122
|
+
attachments,
|
|
1662
2123
|
}),
|
|
1663
2124
|
`import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
|
|
1664
2125
|
``,
|
|
@@ -1667,22 +2128,25 @@ function renderJsx(components: readonly string[], ctx: RenderCtx, framework: str
|
|
|
1667
2128
|
...(p.altNote ?? []).map((l) => `// ${l}`),
|
|
1668
2129
|
...mockInit,
|
|
1669
2130
|
``,
|
|
2131
|
+
...attachmentModuleInit,
|
|
1670
2132
|
...cardsInit,
|
|
1671
2133
|
`export default function App() {`,
|
|
1672
2134
|
sampleMessagesInit,
|
|
1673
2135
|
` const [loading, setLoading] = useState(false);`,
|
|
1674
2136
|
` const suggestions = ${jsArray(suggestions)};`,
|
|
2137
|
+
...(attachmentStateInit ? [attachmentStateInit] : []),
|
|
1675
2138
|
...(sampleSourcesInit ? [sampleSourcesInit] : []),
|
|
1676
2139
|
...(modelInit ? [modelInit] : []),
|
|
1677
2140
|
...(toolsInit ? [toolsInit] : []),
|
|
1678
2141
|
...(toolRunner ? [toolRunner] : []),
|
|
1679
2142
|
``,
|
|
1680
|
-
` async function onSubmit(e:
|
|
2143
|
+
` async function onSubmit(e: ${submitParamType}) {`,
|
|
1681
2144
|
onSubmitBody,
|
|
1682
2145
|
` }`,
|
|
1683
2146
|
``,
|
|
1684
2147
|
` return (`,
|
|
1685
2148
|
` <div style={{ ${jsxStyle(p.style)} }}>`,
|
|
2149
|
+
...(attachmentJsx ? [attachmentJsx] : []),
|
|
1686
2150
|
...(workspace
|
|
1687
2151
|
? [
|
|
1688
2152
|
` {/* SCAF-14: workspace split — chat pane left, artifact preview right. */}`,
|
|
@@ -1736,8 +2200,13 @@ function renderVue(components: readonly string[], ctx: RenderCtx): string {
|
|
|
1736
2200
|
// SCAF-9: exclude message-embedded tags from companion rendering.
|
|
1737
2201
|
// SCAF-14: also exclude workspace structural tags (handled by the workspace block below).
|
|
1738
2202
|
const workspace = isWorkspace(components);
|
|
2203
|
+
const attachments = hasAttachments(components);
|
|
1739
2204
|
const standaloneCompanionTags = components.filter(
|
|
1740
|
-
(t) =>
|
|
2205
|
+
(t) =>
|
|
2206
|
+
t !== 'kai-chat' &&
|
|
2207
|
+
!MESSAGE_EMBEDDED_TAGS.has(t) &&
|
|
2208
|
+
!WORKSPACE_STRUCTURAL_TAGS.has(t) &&
|
|
2209
|
+
!(attachments && ATTACHMENT_TAGS.has(t)),
|
|
1741
2210
|
);
|
|
1742
2211
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
1743
2212
|
|
|
@@ -1758,6 +2227,55 @@ function renderVue(components: readonly string[], ctx: RenderCtx): string {
|
|
|
1758
2227
|
}
|
|
1759
2228
|
const companions = companionLines.join('\n');
|
|
1760
2229
|
|
|
2230
|
+
// `:items.prop` for the same reason `:messages.prop` carries the thread: an
|
|
2231
|
+
// array set as an ATTRIBUTE stringifies, and `<kai-attachments>` would render
|
|
2232
|
+
// the empty state. The listeners are plain `@kai-*` — Vue maps an unknown-tag
|
|
2233
|
+
// event name straight to addEventListener, which is how `@kai-submit` already
|
|
2234
|
+
// works here.
|
|
2235
|
+
const attachmentTemplate = attachments
|
|
2236
|
+
? [
|
|
2237
|
+
` <!-- Drop files here to stage them for the NEXT message. -->`,
|
|
2238
|
+
` <div style="flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem;">`,
|
|
2239
|
+
` <kai-file-upload accept="${ATTACHMENT_ACCEPT}" @kai-files-added="onFilesAdded" />`,
|
|
2240
|
+
` <kai-attachments :items.prop="staged" variant="inline" removable @kai-remove="onRemoveAttachment" />`,
|
|
2241
|
+
` </div>`,
|
|
2242
|
+
].join('\n')
|
|
2243
|
+
: '';
|
|
2244
|
+
|
|
2245
|
+
const attachmentScript = attachments
|
|
2246
|
+
? [
|
|
2247
|
+
...ATTACHMENT_WIRE_NOTE,
|
|
2248
|
+
`//`,
|
|
2249
|
+
`// AttachmentData is the kit's own type for a staged file, and it is what`,
|
|
2250
|
+
`// <kai-attachments>.items and a 'file' MessagePart both hold.`,
|
|
2251
|
+
`const staged = ref<AttachmentData[]>([]);`,
|
|
2252
|
+
``,
|
|
2253
|
+
...fileToAttachmentLines('', 'AttachmentData'),
|
|
2254
|
+
``,
|
|
2255
|
+
`async function onFilesAdded(event: Event) {`,
|
|
2256
|
+
` const { files } = (event as CustomEvent<{ files: File[] }>).detail;`,
|
|
2257
|
+
` // Read every file BEFORE appending: a per-file append would order the`,
|
|
2258
|
+
` // list by whichever finished reading first.`,
|
|
2259
|
+
` const added = await Promise.all(files.map(toAttachment));`,
|
|
2260
|
+
` staged.value = [...staged.value, ...added];`,
|
|
2261
|
+
`}`,
|
|
2262
|
+
`function onRemoveAttachment(event: Event) {`,
|
|
2263
|
+
` const { id } = (event as CustomEvent<{ id: string }>).detail;`,
|
|
2264
|
+
` staged.value = staged.value.filter((a) => a.id !== id);`,
|
|
2265
|
+
`}`,
|
|
2266
|
+
]
|
|
2267
|
+
: [];
|
|
2268
|
+
const attachmentSubmitOpts = attachments
|
|
2269
|
+
? {
|
|
2270
|
+
filesExpr: 'files',
|
|
2271
|
+
afterValue: attachmentTurnLines({
|
|
2272
|
+
stagedExpr: 'staged.value',
|
|
2273
|
+
clear: ['staged.value = [];'],
|
|
2274
|
+
fromSubmitEvent: true,
|
|
2275
|
+
}),
|
|
2276
|
+
}
|
|
2277
|
+
: {};
|
|
2278
|
+
|
|
1761
2279
|
const onSubmitBody = realStreamBody({
|
|
1762
2280
|
pad: ' ',
|
|
1763
2281
|
read: 'messages.value',
|
|
@@ -1770,6 +2288,7 @@ function renderVue(components: readonly string[], ctx: RenderCtx): string {
|
|
|
1770
2288
|
cards: ctx.emitCards,
|
|
1771
2289
|
thread: liveThreadBinding('messages.value', '(fn) => { messages.value = fn(messages.value); }'),
|
|
1772
2290
|
mock: isMock,
|
|
2291
|
+
...attachmentSubmitOpts,
|
|
1773
2292
|
});
|
|
1774
2293
|
|
|
1775
2294
|
// SCAF-10: ChatMessage declaration for strict-TS Vue consumers.
|
|
@@ -1879,6 +2398,7 @@ function renderVue(components: readonly string[], ctx: RenderCtx): string {
|
|
|
1879
2398
|
cards: ctx.emitCards,
|
|
1880
2399
|
cardTools: ctx.cardProvider !== null,
|
|
1881
2400
|
mock: isMock,
|
|
2401
|
+
attachments,
|
|
1882
2402
|
}),
|
|
1883
2403
|
`import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
|
|
1884
2404
|
vueImports,
|
|
@@ -1893,6 +2413,7 @@ function renderVue(components: readonly string[], ctx: RenderCtx): string {
|
|
|
1893
2413
|
...toolsLines,
|
|
1894
2414
|
...runnerLines,
|
|
1895
2415
|
...sourcesSeed,
|
|
2416
|
+
...attachmentScript,
|
|
1896
2417
|
``,
|
|
1897
2418
|
`// SCAF-15: kai-* register via an async dynamic import (SSR-safety). The .prop`,
|
|
1898
2419
|
`// bindings can apply before the element upgrades, which drops them — re-apply once`,
|
|
@@ -1904,13 +2425,16 @@ function renderVue(components: readonly string[], ctx: RenderCtx): string {
|
|
|
1904
2425
|
` if (el) Object.assign(el, { messages: messages.value, loading: loading.value, suggestions${cardPropAssign.length ? `, ${cardPropAssign.join(', ')}` : ''} });`,
|
|
1905
2426
|
`});`,
|
|
1906
2427
|
``,
|
|
1907
|
-
`async function onSubmit(e:
|
|
2428
|
+
`async function onSubmit(e: ${attachments
|
|
2429
|
+
? 'CustomEvent<{ value: string; attachments: AttachmentData[] }>'
|
|
2430
|
+
: 'CustomEvent<{ value: string }>'}) {`,
|
|
1908
2431
|
onSubmitBody,
|
|
1909
2432
|
`}`,
|
|
1910
2433
|
`</script>`,
|
|
1911
2434
|
``,
|
|
1912
2435
|
`<template>`,
|
|
1913
2436
|
` <div style="${p.style}">`,
|
|
2437
|
+
...(attachmentTemplate ? [attachmentTemplate] : []),
|
|
1914
2438
|
...workspaceTemplate,
|
|
1915
2439
|
` </div>`,
|
|
1916
2440
|
`</template>`,
|
|
@@ -1926,8 +2450,13 @@ function renderSvelte(components: readonly string[], ctx: RenderCtx): string {
|
|
|
1926
2450
|
// SCAF-9: exclude message-embedded tags from companion rendering.
|
|
1927
2451
|
// SCAF-14: also exclude workspace structural tags (handled by the workspace block below).
|
|
1928
2452
|
const workspace = isWorkspace(components);
|
|
2453
|
+
const attachments = hasAttachments(components);
|
|
1929
2454
|
const standaloneCompanionTags = components.filter(
|
|
1930
|
-
(t) =>
|
|
2455
|
+
(t) =>
|
|
2456
|
+
t !== 'kai-chat' &&
|
|
2457
|
+
!MESSAGE_EMBEDDED_TAGS.has(t) &&
|
|
2458
|
+
!WORKSPACE_STRUCTURAL_TAGS.has(t) &&
|
|
2459
|
+
!(attachments && ATTACHMENT_TAGS.has(t)),
|
|
1931
2460
|
);
|
|
1932
2461
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
1933
2462
|
const hasSourcesCompanion = standaloneCompanionTags.includes('kai-sources');
|
|
@@ -1949,6 +2478,17 @@ function renderSvelte(components: readonly string[], ctx: RenderCtx): string {
|
|
|
1949
2478
|
}
|
|
1950
2479
|
const companionLines = companionLinesList.join('\n');
|
|
1951
2480
|
|
|
2481
|
+
const attachmentSubmitOpts = attachments
|
|
2482
|
+
? {
|
|
2483
|
+
filesExpr: 'files',
|
|
2484
|
+
afterValue: attachmentTurnLines({
|
|
2485
|
+
stagedExpr: 'staged',
|
|
2486
|
+
clear: ['staged = [];'],
|
|
2487
|
+
fromSubmitEvent: true,
|
|
2488
|
+
}),
|
|
2489
|
+
}
|
|
2490
|
+
: {};
|
|
2491
|
+
|
|
1952
2492
|
const onSubmitBody = realStreamBody({
|
|
1953
2493
|
pad: ' ',
|
|
1954
2494
|
read: 'messages',
|
|
@@ -1961,6 +2501,7 @@ function renderSvelte(components: readonly string[], ctx: RenderCtx): string {
|
|
|
1961
2501
|
cards: ctx.emitCards,
|
|
1962
2502
|
thread: liveThreadBinding('messages', '(fn) => { messages = fn(messages); }'),
|
|
1963
2503
|
mock: isMock,
|
|
2504
|
+
...attachmentSubmitOpts,
|
|
1964
2505
|
});
|
|
1965
2506
|
|
|
1966
2507
|
// SCAF-10: ChatMessage declaration for strict-TS Svelte consumers.
|
|
@@ -2021,6 +2562,43 @@ function renderSvelte(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2021
2562
|
]
|
|
2022
2563
|
: [];
|
|
2023
2564
|
|
|
2565
|
+
// Svelte 5: the element ref is $state (bind:this writes to it), and the staged
|
|
2566
|
+
// list is $state.raw for the same reason `messages` is — a NEW array per write is
|
|
2567
|
+
// the kit's contract, and deep state would proxy every object on its way into a
|
|
2568
|
+
// Solid-backed custom element. `items` is an ARRAY, so it is applied through the
|
|
2569
|
+
// same upgrade-gated $effect chatEl.messages goes through, never as an attribute.
|
|
2570
|
+
const attachmentScript = attachments
|
|
2571
|
+
? [
|
|
2572
|
+
...ATTACHMENT_WIRE_NOTE.map((l) => ` ${l}`),
|
|
2573
|
+
` //`,
|
|
2574
|
+
` // AttachmentData is the kit's own type for a staged file, and it is what`,
|
|
2575
|
+
` // <kai-attachments>.items and a 'file' MessagePart both hold.`,
|
|
2576
|
+
` let attachmentsEl = $state<KaiAttachmentsElement | undefined>(undefined);`,
|
|
2577
|
+
` let staged = $state.raw<AttachmentData[]>([]);`,
|
|
2578
|
+
` $effect(() => { if (attachmentsEl && defined) { attachmentsEl.items = staged; } });`,
|
|
2579
|
+
``,
|
|
2580
|
+
...fileToAttachmentLines(' ', 'AttachmentData'),
|
|
2581
|
+
` async function onFilesAdded(e: CustomEvent<{ files: File[] }>) {`,
|
|
2582
|
+
` // Read every file BEFORE appending: a per-file append would order the`,
|
|
2583
|
+
` // list by whichever finished reading first.`,
|
|
2584
|
+
` const added = await Promise.all(e.detail.files.map(toAttachment));`,
|
|
2585
|
+
` staged = [...staged, ...added];`,
|
|
2586
|
+
` }`,
|
|
2587
|
+
` function onRemoveAttachment(e: CustomEvent<{ id: string }>) {`,
|
|
2588
|
+
` staged = staged.filter((a) => a.id !== e.detail.id);`,
|
|
2589
|
+
` }`,
|
|
2590
|
+
]
|
|
2591
|
+
: [];
|
|
2592
|
+
const attachmentMarkup = attachments
|
|
2593
|
+
? [
|
|
2594
|
+
` <!-- Drop files here to stage them for the NEXT message. -->`,
|
|
2595
|
+
` <div style="flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem;">`,
|
|
2596
|
+
` <kai-file-upload accept="${ATTACHMENT_ACCEPT}" onkai-files-added={onFilesAdded}></kai-file-upload>`,
|
|
2597
|
+
` <kai-attachments bind:this={attachmentsEl} variant="inline" removable onkai-remove={onRemoveAttachment}></kai-attachments>`,
|
|
2598
|
+
` </div>`,
|
|
2599
|
+
]
|
|
2600
|
+
: [];
|
|
2601
|
+
|
|
2024
2602
|
// SCAF-14: workspace template block — resizable split with chat + artifact panes.
|
|
2025
2603
|
const workspaceMarkup = workspace
|
|
2026
2604
|
? [
|
|
@@ -2055,7 +2633,11 @@ function renderSvelte(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2055
2633
|
// KaiSourcesElement is only imported when a kai-sources companion is actually
|
|
2056
2634
|
// declared below: an always-on import would be unused (and fail noUnusedLocals)
|
|
2057
2635
|
// on every archetype without kai-sources.
|
|
2058
|
-
` import type { ${
|
|
2636
|
+
` import type { ${[
|
|
2637
|
+
'KaiChatElement',
|
|
2638
|
+
...(hasSourcesCompanion ? ['KaiSourcesElement'] : []),
|
|
2639
|
+
...(attachments ? ['KaiAttachmentsElement'] : []),
|
|
2640
|
+
].join(', ')} } from '@kitn.ai/ui/elements';`,
|
|
2059
2641
|
...wireImportLines({
|
|
2060
2642
|
pad: ' ',
|
|
2061
2643
|
typed: true,
|
|
@@ -2063,6 +2645,7 @@ function renderSvelte(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2063
2645
|
cards: ctx.emitCards,
|
|
2064
2646
|
cardTools: ctx.cardProvider !== null,
|
|
2065
2647
|
mock: isMock,
|
|
2648
|
+
attachments,
|
|
2066
2649
|
}),
|
|
2067
2650
|
` import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
|
|
2068
2651
|
` import { onMount } from 'svelte';`,
|
|
@@ -2088,13 +2671,17 @@ function renderSvelte(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2088
2671
|
` if (chatEl && defined) { chatEl.messages = messages; chatEl.loading = loading; chatEl.suggestions = suggestions;${cardPropEffect} }`,
|
|
2089
2672
|
` });`,
|
|
2090
2673
|
...sourcesReactive,
|
|
2674
|
+
...attachmentScript,
|
|
2091
2675
|
``,
|
|
2092
|
-
` async function onSubmit(e:
|
|
2676
|
+
` async function onSubmit(e: ${attachments
|
|
2677
|
+
? 'CustomEvent<{ value: string; attachments: AttachmentData[] }>'
|
|
2678
|
+
: 'CustomEvent<{ value: string }>'}) {`,
|
|
2093
2679
|
onSubmitBody,
|
|
2094
2680
|
` }`,
|
|
2095
2681
|
`</script>`,
|
|
2096
2682
|
``,
|
|
2097
2683
|
`<div style="${p.style}">`,
|
|
2684
|
+
...attachmentMarkup,
|
|
2098
2685
|
...workspaceMarkup,
|
|
2099
2686
|
`</div>`,
|
|
2100
2687
|
]
|
|
@@ -2151,8 +2738,14 @@ function renderTanstackStart(components: readonly string[], ctx: RenderCtx): str
|
|
|
2151
2738
|
const wrapperNames = importTags.map(toPascalCase);
|
|
2152
2739
|
const importList = wrapperNames.join(', ');
|
|
2153
2740
|
|
|
2741
|
+
const attachments = hasAttachments(components);
|
|
2742
|
+
|
|
2154
2743
|
const standaloneCompanionTags = components.filter(
|
|
2155
|
-
(t) =>
|
|
2744
|
+
(t) =>
|
|
2745
|
+
t !== 'kai-chat' &&
|
|
2746
|
+
!MESSAGE_EMBEDDED_TAGS.has(t) &&
|
|
2747
|
+
!WORKSPACE_STRUCTURAL_TAGS.has(t) &&
|
|
2748
|
+
!(attachments && ATTACHMENT_TAGS.has(t)),
|
|
2156
2749
|
);
|
|
2157
2750
|
|
|
2158
2751
|
const companionJsxLines: string[] = [];
|
|
@@ -2175,6 +2768,54 @@ function renderTanstackStart(components: readonly string[], ctx: RenderCtx): str
|
|
|
2175
2768
|
}
|
|
2176
2769
|
const companions = companionJsxLines.join('\n');
|
|
2177
2770
|
|
|
2771
|
+
// Same three pieces as `renderJsx` — this target IS React. See there for why
|
|
2772
|
+
// the type comes from @kitn.ai/ui/state and why the strip sits above the chat.
|
|
2773
|
+
const attachmentJsx = attachments
|
|
2774
|
+
? [
|
|
2775
|
+
` {/* Drop files here to stage them for the NEXT message. */}`,
|
|
2776
|
+
` <div style={{ flex: '0 0 auto', display: 'flex', flexDirection: 'column', gap: '0.5rem', padding: '0.75rem' }}>`,
|
|
2777
|
+
` <FileUpload accept="${ATTACHMENT_ACCEPT}" onFilesAdded={onFilesAdded} />`,
|
|
2778
|
+
` {/* items is an ARRAY, so the wrapper sets it as a DOM property, never an attribute. */}`,
|
|
2779
|
+
` <Attachments items={staged} variant="inline" removable onRemove={onRemoveAttachment} />`,
|
|
2780
|
+
` </div>`,
|
|
2781
|
+
].join('\n')
|
|
2782
|
+
: '';
|
|
2783
|
+
const attachmentModuleInit = attachments
|
|
2784
|
+
? [
|
|
2785
|
+
...ATTACHMENT_WIRE_NOTE,
|
|
2786
|
+
`//`,
|
|
2787
|
+
`// AttachmentData is the kit's own type for a staged file — the same one`,
|
|
2788
|
+
`// <Attachments>.items holds and the same one a 'file' message part holds.`,
|
|
2789
|
+
...fileToAttachmentLines('', 'AttachmentData'),
|
|
2790
|
+
]
|
|
2791
|
+
: [];
|
|
2792
|
+
const attachmentStateInit = attachments
|
|
2793
|
+
? [
|
|
2794
|
+
` const [staged, setStaged] = useState<AttachmentData[]>([]);`,
|
|
2795
|
+
` const onFilesAdded = async (e: CustomEvent<{ files: File[] }>) => {`,
|
|
2796
|
+
` // Read every file BEFORE appending: a per-file append would order the`,
|
|
2797
|
+
` // list by whichever finished reading first.`,
|
|
2798
|
+
` const added = await Promise.all(e.detail.files.map(toAttachment));`,
|
|
2799
|
+
` setStaged((prev) => [...prev, ...added]);`,
|
|
2800
|
+
` };`,
|
|
2801
|
+
` const onRemoveAttachment = (e: CustomEvent<{ id: string }>) =>`,
|
|
2802
|
+
` setStaged((prev) => prev.filter((a) => a.id !== e.detail.id));`,
|
|
2803
|
+
].join('\n')
|
|
2804
|
+
: '';
|
|
2805
|
+
const attachmentSubmitOpts = attachments
|
|
2806
|
+
? {
|
|
2807
|
+
filesExpr: 'files',
|
|
2808
|
+
afterValue: attachmentTurnLines({
|
|
2809
|
+
stagedExpr: 'staged',
|
|
2810
|
+
clear: ['setStaged([]);'],
|
|
2811
|
+
fromSubmitEvent: true,
|
|
2812
|
+
}),
|
|
2813
|
+
}
|
|
2814
|
+
: {};
|
|
2815
|
+
const submitParamType = attachments
|
|
2816
|
+
? 'CustomEvent<{ value: string; attachments: AttachmentData[] }>'
|
|
2817
|
+
: 'CustomEvent<{ value: string }>';
|
|
2818
|
+
|
|
2178
2819
|
const mockInit = isMock ? mockResponderInit() : [];
|
|
2179
2820
|
|
|
2180
2821
|
// SCAF-9: no fabricated seed — see SAMPLE_AGENTIC_MESSAGE.
|
|
@@ -2227,6 +2868,7 @@ function renderTanstackStart(components: readonly string[], ctx: RenderCtx): str
|
|
|
2227
2868
|
cards: ctx.emitCards,
|
|
2228
2869
|
thread: REACT_THREAD,
|
|
2229
2870
|
mock: isMock,
|
|
2871
|
+
...attachmentSubmitOpts,
|
|
2230
2872
|
});
|
|
2231
2873
|
|
|
2232
2874
|
// File path guidance for TanStack Start (file-based routing)
|
|
@@ -2260,6 +2902,7 @@ function renderTanstackStart(components: readonly string[], ctx: RenderCtx): str
|
|
|
2260
2902
|
cards: ctx.emitCards,
|
|
2261
2903
|
cardTools: ctx.cardProvider !== null,
|
|
2262
2904
|
mock: isMock,
|
|
2905
|
+
attachments,
|
|
2263
2906
|
}),
|
|
2264
2907
|
`import '@kitn.ai/ui/theme.tokens.css' // compiled token defaults`,
|
|
2265
2908
|
``,
|
|
@@ -2267,6 +2910,7 @@ function renderTanstackStart(components: readonly string[], ctx: RenderCtx): str
|
|
|
2267
2910
|
...(p.altNote ?? []).map((l) => `// ${l}`),
|
|
2268
2911
|
...mockInit,
|
|
2269
2912
|
``,
|
|
2913
|
+
...attachmentModuleInit,
|
|
2270
2914
|
...cardsInit,
|
|
2271
2915
|
`// ssr: false keeps the Solid-based web component client-only.`,
|
|
2272
2916
|
`// Server HTML for /chat omits <kai-chat> → no hydration mismatch.`,
|
|
@@ -2279,17 +2923,19 @@ function renderTanstackStart(components: readonly string[], ctx: RenderCtx): str
|
|
|
2279
2923
|
sampleMessagesInit,
|
|
2280
2924
|
` const [loading, setLoading] = useState(false);`,
|
|
2281
2925
|
` const suggestions = ${jsArray(suggestions)};`,
|
|
2926
|
+
...(attachmentStateInit ? [attachmentStateInit] : []),
|
|
2282
2927
|
...(sampleSourcesInit ? [sampleSourcesInit] : []),
|
|
2283
2928
|
...(modelInit ? [modelInit] : []),
|
|
2284
2929
|
...(toolsInit ? [toolsInit] : []),
|
|
2285
2930
|
...(toolRunner ? [toolRunner] : []),
|
|
2286
2931
|
``,
|
|
2287
|
-
` async function onSubmit(e:
|
|
2932
|
+
` async function onSubmit(e: ${submitParamType}) {`,
|
|
2288
2933
|
onSubmitBody,
|
|
2289
2934
|
` }`,
|
|
2290
2935
|
``,
|
|
2291
2936
|
` return (`,
|
|
2292
2937
|
` <main style={{ ${jsxStyle(p.style)} }}>`,
|
|
2938
|
+
...(attachmentJsx ? [attachmentJsx] : []),
|
|
2293
2939
|
...(workspace
|
|
2294
2940
|
? [
|
|
2295
2941
|
` {/* SCAF-14: workspace split — chat pane left, artifact preview right. */}`,
|
|
@@ -2366,8 +3012,13 @@ function renderAngular(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2366
3012
|
const { p, emptyHint, suggestions, isMock, defaultModel, emitTools, emitToolLoop } = ctx;
|
|
2367
3013
|
|
|
2368
3014
|
const workspace = isWorkspace(components);
|
|
3015
|
+
const attachments = hasAttachments(components);
|
|
2369
3016
|
const standaloneCompanionTags = components.filter(
|
|
2370
|
-
(t) =>
|
|
3017
|
+
(t) =>
|
|
3018
|
+
t !== 'kai-chat' &&
|
|
3019
|
+
!MESSAGE_EMBEDDED_TAGS.has(t) &&
|
|
3020
|
+
!WORKSPACE_STRUCTURAL_TAGS.has(t) &&
|
|
3021
|
+
!(attachments && ATTACHMENT_TAGS.has(t)),
|
|
2371
3022
|
);
|
|
2372
3023
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
2373
3024
|
const hasSourcesCompanion = standaloneCompanionTags.includes('kai-sources');
|
|
@@ -2390,6 +3041,61 @@ function renderAngular(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2390
3041
|
}
|
|
2391
3042
|
}
|
|
2392
3043
|
|
|
3044
|
+
// `[items]` is a PROPERTY binding for the same reason `[messages]` is: an array
|
|
3045
|
+
// written as an attribute stringifies. Angular's own signal read goes in the
|
|
3046
|
+
// template, so no afterNextRender re-apply is needed for it — the binding
|
|
3047
|
+
// re-runs on every signal change, including the first one after upgrade.
|
|
3048
|
+
const attachmentTemplate = attachments
|
|
3049
|
+
? [
|
|
3050
|
+
` <!-- Drop files here to stage them for the NEXT message. -->`,
|
|
3051
|
+
` <div style="flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem;">`,
|
|
3052
|
+
` <kai-file-upload accept="${ATTACHMENT_ACCEPT}" (kai-files-added)="onFilesAdded($event)"></kai-file-upload>`,
|
|
3053
|
+
` <kai-attachments [items]="staged()" variant="inline" removable (kai-remove)="onRemoveAttachment($event)"></kai-attachments>`,
|
|
3054
|
+
` </div>`,
|
|
3055
|
+
]
|
|
3056
|
+
: [];
|
|
3057
|
+
// Module scope for `toAttachment` (a class holds no bare function declaration),
|
|
3058
|
+
// class scope for the signal and the two handlers the template calls.
|
|
3059
|
+
const attachmentModuleInit = attachments
|
|
3060
|
+
? [
|
|
3061
|
+
...ATTACHMENT_WIRE_NOTE,
|
|
3062
|
+
`//`,
|
|
3063
|
+
`// AttachmentData is the kit's own type for a staged file, and it is what`,
|
|
3064
|
+
`// <kai-attachments>.items and a 'file' MessagePart both hold.`,
|
|
3065
|
+
...fileToAttachmentLines('', 'AttachmentData'),
|
|
3066
|
+
``,
|
|
3067
|
+
]
|
|
3068
|
+
: [];
|
|
3069
|
+
const attachmentFields = attachments
|
|
3070
|
+
? [
|
|
3071
|
+
` readonly staged = signal<AttachmentData[]>([]);`,
|
|
3072
|
+
``,
|
|
3073
|
+
` // Same Event-not-CustomEvent rule as onSubmit below: strictTemplates types`,
|
|
3074
|
+
` // $event on an unknown custom-element event as a plain Event.`,
|
|
3075
|
+
` async onFilesAdded(event: Event) {`,
|
|
3076
|
+
` const { files } = (event as CustomEvent<{ files: File[] }>).detail;`,
|
|
3077
|
+
` // Read every file BEFORE appending: a per-file append would order the`,
|
|
3078
|
+
` // list by whichever finished reading first.`,
|
|
3079
|
+
` const added = await Promise.all(files.map(toAttachment));`,
|
|
3080
|
+
` this.staged.set([...this.staged(), ...added]);`,
|
|
3081
|
+
` }`,
|
|
3082
|
+
` onRemoveAttachment(event: Event) {`,
|
|
3083
|
+
` const { id } = (event as CustomEvent<{ id: string }>).detail;`,
|
|
3084
|
+
` this.staged.set(this.staged().filter((a) => a.id !== id));`,
|
|
3085
|
+
` }`,
|
|
3086
|
+
]
|
|
3087
|
+
: [];
|
|
3088
|
+
const attachmentSubmitOpts = attachments
|
|
3089
|
+
? {
|
|
3090
|
+
filesExpr: 'files',
|
|
3091
|
+
afterValue: attachmentTurnLines({
|
|
3092
|
+
stagedExpr: 'this.staged()',
|
|
3093
|
+
clear: ['this.staged.set([]);'],
|
|
3094
|
+
fromSubmitEvent: true,
|
|
3095
|
+
}),
|
|
3096
|
+
}
|
|
3097
|
+
: {};
|
|
3098
|
+
|
|
2393
3099
|
// Angular signals: `this.messages()` reads, `this.messages.set(next)` writes a
|
|
2394
3100
|
// BRAND-NEW array, which is what re-renders <kai-chat>.
|
|
2395
3101
|
const read = 'this.messages()';
|
|
@@ -2408,6 +3114,7 @@ function renderAngular(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2408
3114
|
cards: ctx.emitCards,
|
|
2409
3115
|
thread: accessorThreadBinding(read, commit, setter),
|
|
2410
3116
|
mock: isMock,
|
|
3117
|
+
...attachmentSubmitOpts,
|
|
2411
3118
|
});
|
|
2412
3119
|
|
|
2413
3120
|
// Module scope, exactly like vue: a class can hold neither a bare `const` nor a
|
|
@@ -2495,6 +3202,9 @@ function renderAngular(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2495
3202
|
// declared: an always-on import is unused on every other archetype, and a stock
|
|
2496
3203
|
// Angular tsconfig turns on the checks that make that a build error.
|
|
2497
3204
|
const elementTypes = hasSourcesCompanion ? 'KaiChatElement, KaiSourcesElement' : 'KaiChatElement';
|
|
3205
|
+
// No KaiAttachmentsElement here: unlike svelte/html this target never holds an
|
|
3206
|
+
// element reference for the list — `[items]` is a template binding, so nothing
|
|
3207
|
+
// in the class is typed by it.
|
|
2498
3208
|
|
|
2499
3209
|
return [
|
|
2500
3210
|
`// Angular standalone component — save as: src/app/chat.component.ts`,
|
|
@@ -2516,6 +3226,7 @@ function renderAngular(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2516
3226
|
cards: ctx.emitCards,
|
|
2517
3227
|
cardTools: ctx.cardProvider !== null,
|
|
2518
3228
|
mock: isMock,
|
|
3229
|
+
attachments,
|
|
2519
3230
|
}),
|
|
2520
3231
|
``,
|
|
2521
3232
|
`// ${ctx.label} — ${p.note}. empty-state hint: ${emptyHint}`,
|
|
@@ -2523,6 +3234,7 @@ function renderAngular(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2523
3234
|
...(isMock ? mockResponderInit() : []),
|
|
2524
3235
|
``,
|
|
2525
3236
|
...modelInit,
|
|
3237
|
+
...attachmentModuleInit,
|
|
2526
3238
|
...cardsInit,
|
|
2527
3239
|
...toolsLines,
|
|
2528
3240
|
...runnerLines,
|
|
@@ -2534,6 +3246,7 @@ function renderAngular(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2534
3246
|
` schemas: [CUSTOM_ELEMENTS_SCHEMA],`,
|
|
2535
3247
|
` template: \``,
|
|
2536
3248
|
` <div style="${p.style}">`,
|
|
3249
|
+
...attachmentTemplate,
|
|
2537
3250
|
...templateBody,
|
|
2538
3251
|
` </div>`,
|
|
2539
3252
|
` \`,`,
|
|
@@ -2566,11 +3279,13 @@ function renderAngular(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2566
3279
|
` });`,
|
|
2567
3280
|
` }`,
|
|
2568
3281
|
``,
|
|
3282
|
+
...attachmentFields,
|
|
3283
|
+
``,
|
|
2569
3284
|
` // \`Event\`, not \`CustomEvent\`: under strictTemplates Angular types \`$event\` on`,
|
|
2570
3285
|
` // an unknown custom-element event as a plain Event, so the narrowing happens`,
|
|
2571
3286
|
` // here rather than in the signature.`,
|
|
2572
3287
|
` async onSubmit(event: Event) {`,
|
|
2573
|
-
` const e = event as CustomEvent<{ value: string }>;`,
|
|
3288
|
+
` const e = event as CustomEvent<{ value: string${attachments ? '; attachments: AttachmentData[]' : ''} }>;`,
|
|
2574
3289
|
onSubmitBody,
|
|
2575
3290
|
` }`,
|
|
2576
3291
|
`}`,
|
|
@@ -2625,8 +3340,13 @@ function renderSolid(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2625
3340
|
const { p, emptyHint, suggestions, isMock, defaultModel, emitTools, emitToolLoop } = ctx;
|
|
2626
3341
|
|
|
2627
3342
|
const workspace = isWorkspace(components);
|
|
3343
|
+
const attachments = hasAttachments(components);
|
|
2628
3344
|
const standaloneCompanionTags = components.filter(
|
|
2629
|
-
(t) =>
|
|
3345
|
+
(t) =>
|
|
3346
|
+
t !== 'kai-chat' &&
|
|
3347
|
+
!MESSAGE_EMBEDDED_TAGS.has(t) &&
|
|
3348
|
+
!WORKSPACE_STRUCTURAL_TAGS.has(t) &&
|
|
3349
|
+
!(attachments && ATTACHMENT_TAGS.has(t)),
|
|
2630
3350
|
);
|
|
2631
3351
|
const hasSources = standaloneCompanionTags.includes('kai-sources');
|
|
2632
3352
|
const hasVoice = standaloneCompanionTags.includes('kai-voice-input');
|
|
@@ -2669,6 +3389,12 @@ function renderSolid(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2669
3389
|
'Tool',
|
|
2670
3390
|
...(workspace ? ['Artifact', 'ResizableHandle', 'ResizablePanel', 'ResizablePanelGroup'] : []),
|
|
2671
3391
|
...(hasVoice ? ['VoiceInput'] : []),
|
|
3392
|
+
// `Attachments`/`Attachment`/`AttachmentPreview`/`AttachmentInfo` are already
|
|
3393
|
+
// unconditional above — `renderPart` draws `file` PARTS with them whatever the
|
|
3394
|
+
// surface asked for. These three are the STAGING half, which only this
|
|
3395
|
+
// capability composes: the dropzone, its trigger, and the per-chip remove
|
|
3396
|
+
// button `renderPart` has no use for (a sent file is not removable).
|
|
3397
|
+
...(attachments ? ['AttachmentRemove', 'FileUpload', 'FileUploadTrigger'] : []),
|
|
2672
3398
|
].sort();
|
|
2673
3399
|
|
|
2674
3400
|
// Solid signals: `messages()` reads, `setMessages(next)` writes a NEW array.
|
|
@@ -2691,7 +3417,19 @@ function renderSolid(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2691
3417
|
cards: ctx.emitCards,
|
|
2692
3418
|
thread: accessorThreadBinding(read, commit, setter),
|
|
2693
3419
|
valueSource: 'input()',
|
|
2694
|
-
|
|
3420
|
+
// The staged files are captured BEFORE either list is cleared. `setInput('')`
|
|
3421
|
+
// stays last so the order reads the way it executes.
|
|
3422
|
+
afterValue: [
|
|
3423
|
+
...(attachments
|
|
3424
|
+
? attachmentTurnLines({
|
|
3425
|
+
stagedExpr: 'staged()',
|
|
3426
|
+
clear: ['setStaged([]);'],
|
|
3427
|
+
fromSubmitEvent: false,
|
|
3428
|
+
})
|
|
3429
|
+
: []),
|
|
3430
|
+
`setInput('');`,
|
|
3431
|
+
],
|
|
3432
|
+
...(attachments ? { filesExpr: 'files' } : {}),
|
|
2695
3433
|
mock: isMock,
|
|
2696
3434
|
});
|
|
2697
3435
|
|
|
@@ -2718,6 +3456,17 @@ function renderSolid(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2718
3456
|
]
|
|
2719
3457
|
: [];
|
|
2720
3458
|
|
|
3459
|
+
// The staging state. `toAttachment` is declared INSIDE App() here, unlike the
|
|
3460
|
+
// kai-* targets: this file's module scope is where `renderPart` and the card
|
|
3461
|
+
// registry live, and nothing outside the component needs it.
|
|
3462
|
+
const attachmentInit = attachments
|
|
3463
|
+
? [
|
|
3464
|
+
...ATTACHMENT_WIRE_NOTE.map((l) => ` ${l}`),
|
|
3465
|
+
` const [staged, setStaged] = createSignal<AttachmentData[]>([]);`,
|
|
3466
|
+
...fileToAttachmentLines(' ', 'AttachmentData'),
|
|
3467
|
+
]
|
|
3468
|
+
: [];
|
|
3469
|
+
|
|
2721
3470
|
const sourcesInit = hasSources
|
|
2722
3471
|
? [
|
|
2723
3472
|
` // Replace sampleSources with your real source data.`,
|
|
@@ -2802,6 +3551,37 @@ function renderSolid(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2802
3551
|
` </For>`,
|
|
2803
3552
|
` </div>`,
|
|
2804
3553
|
` </Show>`,
|
|
3554
|
+
...(attachments
|
|
3555
|
+
? [
|
|
3556
|
+
` {/* Drop files here to stage them for the NEXT message. */}`,
|
|
3557
|
+
` <div class="flex flex-col gap-2 pb-3">`,
|
|
3558
|
+
` {/* Read every file BEFORE appending: a per-file append would order`,
|
|
3559
|
+
` the list by whichever finished reading first. */}`,
|
|
3560
|
+
` <FileUpload`,
|
|
3561
|
+
` accept="${ATTACHMENT_ACCEPT}"`,
|
|
3562
|
+
` onFilesAdded={async (files) => setStaged([...staged(), ...(await Promise.all(files.map(toAttachment)))])}`,
|
|
3563
|
+
` >`,
|
|
3564
|
+
` <FileUploadTrigger class="border-border text-muted-foreground w-full rounded-xl border border-dashed px-4 py-3 text-center text-sm">`,
|
|
3565
|
+
` Click or drop files to attach`,
|
|
3566
|
+
` </FileUploadTrigger>`,
|
|
3567
|
+
` </FileUpload>`,
|
|
3568
|
+
` {/* The staged chips. onRemove is what makes <AttachmentRemove> draw its button. */}`,
|
|
3569
|
+
` <Show when={staged().length > 0}>`,
|
|
3570
|
+
` <Attachments variant="inline">`,
|
|
3571
|
+
` <For each={staged()}>`,
|
|
3572
|
+
` {(a) => (`,
|
|
3573
|
+
` <Attachment data={a} onRemove={() => setStaged(staged().filter((x) => x.id !== a.id))}>`,
|
|
3574
|
+
` <AttachmentPreview />`,
|
|
3575
|
+
` <AttachmentInfo />`,
|
|
3576
|
+
` <AttachmentRemove />`,
|
|
3577
|
+
` </Attachment>`,
|
|
3578
|
+
` )}`,
|
|
3579
|
+
` </For>`,
|
|
3580
|
+
` </Attachments>`,
|
|
3581
|
+
` </Show>`,
|
|
3582
|
+
` </div>`,
|
|
3583
|
+
]
|
|
3584
|
+
: []),
|
|
2805
3585
|
` <PromptInput value={input()} onValueChange={setInput} onSubmit={onSubmit} isLoading={loading()}>`,
|
|
2806
3586
|
` <div class="flex flex-col">`,
|
|
2807
3587
|
` <PromptInputTextarea placeholder="Send a message…" class="min-h-[44px] pt-3 pl-4" />`,
|
|
@@ -2872,11 +3652,27 @@ function renderSolid(components: readonly string[], ctx: RenderCtx): string {
|
|
|
2872
3652
|
`// every kit utility class as unused, and the whole UI renders unstyled.`,
|
|
2873
3653
|
`// (theme.css here, not theme.tokens.css: this app compiles Tailwind itself.)`,
|
|
2874
3654
|
`import { For, Index, Match, Show, Switch, createMemo, createSignal } from 'solid-js';`,
|
|
3655
|
+
// '@kitn.ai/ui/solid', NOT the root '@kitn.ai/ui', and the difference is
|
|
3656
|
+
// invisible to every compiler on this repo's critical path: src/solid.ts is
|
|
3657
|
+
// `export * from './index'`, so ./solid is a strict SUPERSET of the root and
|
|
3658
|
+
// both specifiers typecheck identically. verify:scaffold's solid project
|
|
3659
|
+
// compiled clean the whole time this was wrong.
|
|
3660
|
+
//
|
|
3661
|
+
// The root is the shared layer EVERY consumer resolves — React, Vue, Svelte
|
|
3662
|
+
// and vanilla included — so it deliberately carries only the chat components,
|
|
3663
|
+
// types and helpers those frameworks can use. The rest of the Solid catalog
|
|
3664
|
+
// lives on ./solid, built as its own bundle so a React app never pays for it.
|
|
3665
|
+
// A Solid app pointed at the root therefore gets the smaller surface and finds
|
|
3666
|
+
// out the hard way, one missing export at a time. The published guide says so
|
|
3667
|
+
// outright ("Import Solid components from @kitn.ai/ui/solid, not from the root
|
|
3668
|
+
// @kitn.ai/ui"), and this generator contradicted it for every Solid scaffold.
|
|
2875
3669
|
`import {`,
|
|
2876
3670
|
...componentImports.map((n) => ` ${n},`),
|
|
2877
|
-
`} from '@kitn.ai/ui';`,
|
|
3671
|
+
`} from '@kitn.ai/ui/solid';`,
|
|
2878
3672
|
`// The kit's own types, from the same entry the components come from.`,
|
|
2879
|
-
`import type {
|
|
3673
|
+
`import type { ${attachments
|
|
3674
|
+
? 'AttachmentData, ChatMessage, MessagePart, MessageSource'
|
|
3675
|
+
: 'ChatMessage, MessagePart, MessageSource'} } from '@kitn.ai/ui/solid';`,
|
|
2880
3676
|
...wireImportLines({
|
|
2881
3677
|
typed: false,
|
|
2882
3678
|
toolLoop: emitToolLoop,
|
|
@@ -3080,6 +3876,7 @@ function renderSolid(components: readonly string[], ctx: RenderCtx): string {
|
|
|
3080
3876
|
` // value, no row moves, and the content updates through accessors instead.`,
|
|
3081
3877
|
` const messageKeys = createMemo(() => messages().map((m) => m.id));`,
|
|
3082
3878
|
` const suggestions = ${jsArray(suggestions)};`,
|
|
3879
|
+
...attachmentInit,
|
|
3083
3880
|
...sourcesInit,
|
|
3084
3881
|
...modelInit,
|
|
3085
3882
|
...toolsLines,
|
|
@@ -3159,7 +3956,7 @@ export interface SurfaceRequest {
|
|
|
3159
3956
|
* The `kai-*` components this surface composes — THE AXIS, not an archetype id.
|
|
3160
3957
|
*
|
|
3161
3958
|
* A list rather than a preset name is the whole point of this function. The six
|
|
3162
|
-
* archetypes are
|
|
3959
|
+
* archetypes are seven points in this space and the CLI's feature multi-select
|
|
3163
3960
|
* reaches the rest of it, so a renderer keyed on a preset id could only ever
|
|
3164
3961
|
* emit what someone had already thought to name. `kai-chat` is expected to be
|
|
3165
3962
|
* present; everything else is a capability.
|
|
@@ -3718,6 +4515,85 @@ const CHAT_REQUEST_BODY_DECL = [
|
|
|
3718
4515
|
`}`,
|
|
3719
4516
|
];
|
|
3720
4517
|
|
|
4518
|
+
/**
|
|
4519
|
+
* Attachments, on the way IN.
|
|
4520
|
+
*
|
|
4521
|
+
* A user turn's `content` is a plain string until it carries a file, at which
|
|
4522
|
+
* point `toOpenAIMessages` emits the ARRAY form. Every route that re-maps
|
|
4523
|
+
* messages into some other SDK's shape has to handle both, and the three that do
|
|
4524
|
+
* (anthropic, mastra, vercel-ai-sdk) were each written when only the string form
|
|
4525
|
+
* existed — so each one would have quietly dropped the attachment while still
|
|
4526
|
+
* compiling, which is the same defect the encoder was just fixed for.
|
|
4527
|
+
*
|
|
4528
|
+
* These two helpers are the shared half of that: flattening the wire shape is
|
|
4529
|
+
* identical everywhere, while the target shape is not, so each route maps
|
|
4530
|
+
* `WirePart[]` into its own SDK itself rather than inheriting a lowest common
|
|
4531
|
+
* denominator.
|
|
4532
|
+
*
|
|
4533
|
+
* Only injected into routes that actually call them — the eight pass-through
|
|
4534
|
+
* integrations forward `messages` untouched and need none of this, and an unused
|
|
4535
|
+
* declaration is a hard error under the gate's `--noUnusedLocals`.
|
|
4536
|
+
*/
|
|
4537
|
+
const CONTENT_PARTS_DECL = [
|
|
4538
|
+
`/** Where an attachment's bytes are: inline base64, or an address the PROVIDER`,
|
|
4539
|
+
` * fetches. Never both. */`,
|
|
4540
|
+
`type WireFileSource = { type: 'data'; data: string } | { type: 'url'; url: string };`,
|
|
4541
|
+
``,
|
|
4542
|
+
`/** One piece of a turn, with the string and array content forms flattened into`,
|
|
4543
|
+
` * a single shape. */`,
|
|
4544
|
+
`type WirePart =`,
|
|
4545
|
+
` | { kind: 'text'; text: string }`,
|
|
4546
|
+
` | { kind: 'file'; mediaType: string; filename?: string; source: WireFileSource };`,
|
|
4547
|
+
``,
|
|
4548
|
+
`const DATA_URI = /^data:([^;,]+);base64,([\\s\\S]*)$/;`,
|
|
4549
|
+
``,
|
|
4550
|
+
`/**`,
|
|
4551
|
+
` * Flatten a wire message's content into parts.`,
|
|
4552
|
+
` *`,
|
|
4553
|
+
` * An image sent by URL has no media type here — \`image_url\` carries only the`,
|
|
4554
|
+
` * address — so it reports the top-level segment \`'image'\`, which is all a URL`,
|
|
4555
|
+
` * source needs. Only images can reach that branch: the kit refuses to encode a`,
|
|
4556
|
+
` * remote PDF rather than guess at one.`,
|
|
4557
|
+
` */`,
|
|
4558
|
+
`function wireParts(content: OpenAIWireMessage['content']): WirePart[] {`,
|
|
4559
|
+
` if (content == null) return [];`,
|
|
4560
|
+
` if (typeof content === 'string') return content === '' ? [] : [{ kind: 'text', text: content }];`,
|
|
4561
|
+
` return content.map((part): WirePart => {`,
|
|
4562
|
+
` if (part.type === 'text') return { kind: 'text', text: part.text };`,
|
|
4563
|
+
` if (part.type === 'image_url') {`,
|
|
4564
|
+
` const asData = DATA_URI.exec(part.image_url.url);`,
|
|
4565
|
+
` return asData`,
|
|
4566
|
+
` ? { kind: 'file', mediaType: asData[1], source: { type: 'data', data: asData[2] } }`,
|
|
4567
|
+
` : { kind: 'file', mediaType: 'image', source: { type: 'url', url: part.image_url.url } };`,
|
|
4568
|
+
` }`,
|
|
4569
|
+
` const asData = DATA_URI.exec(part.file.file_data);`,
|
|
4570
|
+
` if (!asData) {`,
|
|
4571
|
+
` // LOUD on purpose. \`file_data\` is a data URI on this wire; anything else`,
|
|
4572
|
+
` // cannot be turned into bytes without fetching it, and forwarding a turn`,
|
|
4573
|
+
` // with the attachment quietly missing is the bug this whole path exists`,
|
|
4574
|
+
` // to prevent.`,
|
|
4575
|
+
` throw new Error(`,
|
|
4576
|
+
` 'Unsupported file content part: file_data must be a data: URI of the form data:<media type>;base64,<data>.',`,
|
|
4577
|
+
` );`,
|
|
4578
|
+
` }`,
|
|
4579
|
+
` return {`,
|
|
4580
|
+
` kind: 'file',`,
|
|
4581
|
+
` mediaType: asData[1],`,
|
|
4582
|
+
` filename: part.file.filename,`,
|
|
4583
|
+
` source: { type: 'data', data: asData[2] },`,
|
|
4584
|
+
` };`,
|
|
4585
|
+
` });`,
|
|
4586
|
+
`}`,
|
|
4587
|
+
``,
|
|
4588
|
+
`/** Just the text of a turn. System, assistant and tool messages are text-only`,
|
|
4589
|
+
` * on this wire, so this collapses the array form for them. */`,
|
|
4590
|
+
`function wireText(content: OpenAIWireMessage['content']): string {`,
|
|
4591
|
+
` return wireParts(content)`,
|
|
4592
|
+
` .map((p) => (p.kind === 'text' ? p.text : ''))`,
|
|
4593
|
+
` .join('');`,
|
|
4594
|
+
`}`,
|
|
4595
|
+
];
|
|
4596
|
+
|
|
3721
4597
|
/**
|
|
3722
4598
|
* Slot the body type in just above `chatHandler`.
|
|
3723
4599
|
*
|
|
@@ -3728,11 +4604,16 @@ const CHAT_REQUEST_BODY_DECL = [
|
|
|
3728
4604
|
* written it.
|
|
3729
4605
|
*/
|
|
3730
4606
|
function withChatRequestBody(fragment: string): string {
|
|
4607
|
+
// The content helpers ride along only where the route calls them; see
|
|
4608
|
+
// CONTENT_PARTS_DECL for why an unconditional injection would not compile.
|
|
4609
|
+
const decl = /\bwire(?:Parts|Text)\s*\(/.test(fragment)
|
|
4610
|
+
? [...CHAT_REQUEST_BODY_DECL, ``, ...CONTENT_PARTS_DECL]
|
|
4611
|
+
: CHAT_REQUEST_BODY_DECL;
|
|
3731
4612
|
const lines = fragment.split('\n');
|
|
3732
4613
|
let at = lines.findIndex((l) => /^(?:export\s+)?async function chatHandler\b/.test(l));
|
|
3733
|
-
if (at < 0) return [...
|
|
4614
|
+
if (at < 0) return [...decl, ``, ...lines].join('\n');
|
|
3734
4615
|
while (at > 0 && /^\s*(?:\/\/|\/\*|\*)/.test(lines[at - 1])) at -= 1;
|
|
3735
|
-
return [...lines.slice(0, at), ...
|
|
4616
|
+
return [...lines.slice(0, at), ...decl, ``, ...lines.slice(at)].join('\n');
|
|
3736
4617
|
}
|
|
3737
4618
|
|
|
3738
4619
|
/** Wrap an integration's portable handler in the target framework's declaration. */
|
|
@@ -4342,7 +5223,7 @@ function rejectUseCase(id: string): string {
|
|
|
4342
5223
|
``,
|
|
4343
5224
|
`Valid useCases (presets): ${valid}.`,
|
|
4344
5225
|
``,
|
|
4345
|
-
// The presets are
|
|
5226
|
+
// The presets are seven points, not the space. A harness that only ever learns
|
|
4346
5227
|
// the six ids will ask for the nearest one instead of the surface it wants,
|
|
4347
5228
|
// so the rejection that teaches the id list is the right place to say so.
|
|
4348
5229
|
`These are PRESETS over the real axis, which is \`components\`. To compose a surface no`,
|
|
@@ -4368,14 +5249,14 @@ export const scaffold: Tool = {
|
|
|
4368
5249
|
// catalogs to discover valid ids.
|
|
4369
5250
|
//
|
|
4370
5251
|
// `useCase` is OPTIONAL because `components` can carry the surface instead —
|
|
4371
|
-
// the archetypes are
|
|
5252
|
+
// the archetypes are seven points in the components space, not the space. A
|
|
4372
5253
|
// request must still name one of the two, and the handler says so when it
|
|
4373
5254
|
// names neither.
|
|
4374
5255
|
useCase: z
|
|
4375
5256
|
.string()
|
|
4376
5257
|
.optional()
|
|
4377
5258
|
.describe(
|
|
4378
|
-
'Archetype PRESET id, e.g. "drop-in-chat", "support-widget", "knowledge-base", "agentic", "workspace", "voice". ' +
|
|
5259
|
+
'Archetype PRESET id, e.g. "drop-in-chat", "support-widget", "knowledge-base", "agentic", "workspace", "voice", "attachments". ' +
|
|
4379
5260
|
'Shorthand for the preset\'s `components`. Omit it and pass `components` to compose a surface no preset names.',
|
|
4380
5261
|
),
|
|
4381
5262
|
components: z
|
|
@@ -4383,7 +5264,7 @@ export const scaffold: Tool = {
|
|
|
4383
5264
|
.optional()
|
|
4384
5265
|
.describe(
|
|
4385
5266
|
'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
|
|
5267
|
+
'The real axis: any combination is renderable, not just the seven presets. Include "kai-chat". Wins over `useCase` when both are given.',
|
|
4387
5268
|
),
|
|
4388
5269
|
integration: z
|
|
4389
5270
|
.string()
|