@kitn.ai/ui 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/chat-thread.d.ts +9 -0
- package/dist/{create-tween-D3Y5qiij.js → create-tween-COnPwAQN.js} +1 -1
- package/dist/{create-tween-DGKY1FDY.js → create-tween-DmTUaXWE.js} +1 -1
- package/dist/{create-tween-paho5Rh-.js → create-tween-qs-jjvNA.js} +1 -1
- package/dist/custom-elements.json +43 -7
- package/dist/elements/agent-card.js +1 -1
- package/dist/elements/artifact.js +1 -1
- package/dist/elements/attachments.js +1 -1
- package/dist/elements/audio-visualizer.js +1 -1
- package/dist/elements/avatar.js +1 -1
- package/dist/elements/badge.js +1 -1
- package/dist/elements/button.js +1 -1
- package/dist/elements/card.js +1 -1
- package/dist/elements/cards.js +1 -1
- package/dist/elements/chain-of-thought.js +1 -1
- package/dist/elements/chat-scope-picker.js +1 -1
- package/dist/elements/chat-workspace.js +1 -1
- package/dist/elements/chat.js +1 -1
- package/dist/elements/checkpoint.js +1 -1
- package/dist/elements/choice.js +1 -1
- package/dist/elements/chunks/{Icon-C-2LvGDG.js → Icon-CHG5z5L7.js} +1 -1
- package/dist/elements/chunks/{action-icons-CaYoImEk.js → action-icons-BAtCEEgg.js} +1 -1
- package/dist/elements/chunks/arrow-left-CFPTeL1Y.js +1 -0
- package/dist/elements/chunks/{artifact-BBIscOxJ.js → artifact-fBZOE4SO.js} +1 -1
- package/dist/elements/chunks/{attachments-BB56zoji.js → attachments-DpYMe5VQ.js} +1 -1
- package/dist/elements/chunks/{audio-visualizer-BxOx4THz.js → audio-visualizer-DFwzAcuL.js} +1 -1
- package/dist/elements/chunks/{badge-SxLSZ19R.js → badge-D6zodm6E.js} +1 -1
- package/dist/elements/chunks/{button-CmkUkz6c.js → button-C42vrGvO.js} +1 -1
- package/dist/elements/chunks/{card-renderer-Db4ffkWv.js → card-renderer-DMZsSuvU.js} +1 -1
- package/dist/elements/chunks/{chat-thread-B3Ks-cKr.js → chat-thread-CNNEhol-.js} +1 -1
- package/dist/elements/chunks/check-7Q3K-iXE.js +1 -0
- package/dist/elements/chunks/chevron-down-CpNYLn7E.js +1 -0
- package/dist/elements/chunks/chevron-right-DrH85Fha.js +1 -0
- package/dist/elements/chunks/{choice-card-Q9cYYDyM.js → choice-card-YoWjpsFV.js} +1 -1
- package/dist/elements/chunks/circle-C9KRjKYv.js +1 -0
- package/dist/elements/chunks/circle-check-Dmi7Y4zw.js +1 -0
- package/dist/elements/chunks/{circle-x-D0MEfxOM.js → circle-x-QIxXda6k.js} +1 -1
- package/dist/elements/chunks/{code-block-C8VzAraE.js → code-block-CqeugReY.js} +1 -1
- package/dist/elements/chunks/{collapsible-Cvj61obE.js → collapsible-sTv-_5lF.js} +1 -1
- package/dist/elements/chunks/{composer-D86ztyL0.js → composer-ldQyeVEB.js} +1 -1
- package/dist/elements/chunks/{confirm-card-BdyDAGLw.js → confirm-card-C0RNOwfn.js} +1 -1
- package/dist/elements/chunks/context-5qgypNHC.js +1 -0
- package/dist/elements/chunks/conversation-list-DLzOacXz.js +1 -0
- package/dist/elements/chunks/{create-tween-C_A_B638.js → create-tween-V4TXZ93X.js} +1 -1
- package/dist/elements/chunks/default-input-DeeRSDHc.js +1 -0
- package/dist/elements/chunks/{define-BzPnNsXg.js → define-lE-QvQFn.js} +1 -1
- package/dist/elements/chunks/{disclosure-DVKsoc1F.js → disclosure-DZSa9jog.js} +1 -1
- package/dist/elements/chunks/{download-BjA54IMJ.js → download-B2ZmnGqW.js} +1 -1
- package/dist/elements/chunks/{dropdown-BjVVHWXH.js → dropdown-R3B7cSRE.js} +1 -1
- package/dist/elements/chunks/{ellipsis-BNedmqfi.js → ellipsis-BmxceY4l.js} +1 -1
- package/dist/elements/chunks/{embed-DQsl4XEq.js → embed-BVrtTZPo.js} +1 -1
- package/dist/elements/chunks/{external-link-Di-dw2CU.js → external-link-BbdR7YO7.js} +1 -1
- package/dist/elements/chunks/{file-text-B3sAODCV.js → file-text-D7dijo1A.js} +1 -1
- package/dist/elements/chunks/{file-tree-Dgto-bMn.js → file-tree-0upJ5jeb.js} +1 -1
- package/dist/elements/chunks/{folder-D6rMadFV.js → folder-D8z58t7B.js} +1 -1
- package/dist/elements/chunks/{form-CILxvhQ7.js → form-p5BKJNw5.js} +1 -1
- package/dist/elements/chunks/{hover-card-BtiF4XiU.js → hover-card-CN9s2md5.js} +1 -1
- package/dist/elements/chunks/{icon-DKkmze3y.js → icon-CNiKP37E.js} +1 -1
- package/dist/elements/chunks/{info-CWRjZkuy.js → info-BSnuJiHI.js} +1 -1
- package/dist/elements/chunks/{input--QJBK6BA.js → input-BauDmzxX.js} +1 -1
- package/dist/elements/chunks/{kbd-C3zuKh8R.js → kbd-BlHJaP9b.js} +1 -1
- package/dist/elements/chunks/{link-BL5JA7Q0.js → link-DEi5iUWC.js} +1 -1
- package/dist/elements/chunks/{link-preview-CU37bbXh.js → link-preview-BaHyM6W4.js} +1 -1
- package/dist/elements/chunks/{loader-XmpyxyRH.js → loader-CmJG_Fkq.js} +1 -1
- package/dist/elements/chunks/{markdown-3Gv5UZ_o.js → markdown-D2jaMpCN.js} +1 -1
- package/dist/elements/chunks/{message-X5NZbq-W.js → message-C5UfXZ2Z.js} +1 -1
- package/dist/elements/chunks/{message-CnBA2OUt.js → message-CXWLmmLn.js} +1 -1
- package/dist/elements/chunks/message-square-hXylwsy0.js +1 -0
- package/dist/elements/chunks/{minimize-2-DwyrUTcM.js → minimize-2-C9ytZFMU.js} +1 -1
- package/dist/elements/chunks/model-switcher-CivGfD7-.js +1 -0
- package/dist/elements/chunks/{overlay-Cepd-KcS.js → overlay-DJL_sbOm.js} +1 -1
- package/dist/elements/chunks/{paperclip-CdwKjrlu.js → paperclip-CFaB8naM.js} +1 -1
- package/dist/elements/chunks/{progress-bar-DMVXBD3b.js → progress-bar-Br5SksfR.js} +1 -1
- package/dist/elements/chunks/{prompt-suggestion-Di_WZE_5.js → prompt-suggestion-CMIvtAhU.js} +1 -1
- package/dist/elements/chunks/{reasoning-D0jMch5u.js → reasoning-DpZcNzLq.js} +1 -1
- package/dist/elements/chunks/{resizable-1znbL1Gv.js → resizable-Q8i3T9a_.js} +1 -1
- package/dist/elements/chunks/{rotate-cw-DVhhgx8n.js → rotate-cw-B5IHCBFV.js} +1 -1
- package/dist/elements/chunks/{scroll-area-Bs4bFS1P.js → scroll-area-eLeydBje.js} +1 -1
- package/dist/elements/chunks/{scroll-button-Bet5NLc4.js → scroll-button-DWbz0yA1.js} +1 -1
- package/dist/elements/chunks/{separator-C5Gh8qGC.js → separator-Xic78AD_.js} +1 -1
- package/dist/elements/chunks/{settings-5vmEFg0K.js → settings-B8H51pAx.js} +1 -1
- package/dist/elements/chunks/{settings-group-BWLB38kz.js → settings-group-CsG9eYLe.js} +1 -1
- package/dist/elements/chunks/{share-BZ3G_DaB.js → share-CNTx0z__.js} +1 -1
- package/dist/elements/chunks/{skeleton-DafPJQTp.js → skeleton-CLlHTcMI.js} +1 -1
- package/dist/elements/chunks/{source-D_qj_6uJ.js → source-BoVcU5ok.js} +1 -1
- package/dist/elements/chunks/{star-Cuv7j2IE.js → star-CxQVTVhI.js} +1 -1
- package/dist/elements/chunks/{store-C1StPHfW.js → store-CLtS5CUG.js} +1 -1
- package/dist/elements/chunks/{tasks-card-EyoBZfpA.js → tasks-card-CzlkLmUt.js} +1 -1
- package/dist/elements/chunks/{text-shimmer-D9gRSh34.js → text-shimmer-D5NhpIQ1.js} +1 -1
- package/dist/elements/chunks/{textarea-DuhtHfcm.js → textarea-x5Uxsmtm.js} +1 -1
- package/dist/elements/chunks/{thumbs-up-B9hwv43w.js → thumbs-up-UobcJnx9.js} +1 -1
- package/dist/elements/chunks/{toast-store-Bm3DUEDe.js → toast-store-BOfs9eXq.js} +1 -1
- package/dist/elements/chunks/{tool-CDtW7L0z.js → tool-Be6Dh_dt.js} +1 -1
- package/dist/elements/chunks/{tooltip-eB226mGK.js → tooltip-RxMxyCLF.js} +1 -1
- package/dist/elements/chunks/{triangle-alert-C1SNzTqK.js → triangle-alert-C4yxH2XN.js} +1 -1
- package/dist/elements/chunks/{use-card-resolution-YZh2MvI5.js → use-card-resolution-jva9ehH6.js} +1 -1
- package/dist/elements/chunks/{variant-aurora-Cj1F7D0l.js → variant-aurora-Dx4VRcjS.js} +1 -1
- package/dist/elements/chunks/{variant-custom-T9gpt7QE.js → variant-custom-Y1ZTWgEa.js} +1 -1
- package/dist/elements/chunks/{variant-wave-CxU9z-TE.js → variant-wave-B9gml9Cg.js} +1 -1
- package/dist/elements/chunks/{video-CK4R7Rus.js → video-CrPHsytT.js} +1 -1
- package/dist/elements/chunks/x-_-OPHept.js +1 -0
- package/dist/elements/coachmark.js +1 -1
- package/dist/elements/code-block.js +1 -1
- package/dist/elements/command.js +1 -1
- package/dist/elements/compare.js +1 -1
- package/dist/elements/composer.js +1 -1
- package/dist/elements/confirm-card.js +1 -1
- package/dist/elements/context-meter.js +1 -1
- package/dist/elements/conversation-list.js +1 -1
- package/dist/elements/default-input.d.ts +27 -0
- package/dist/elements/dialog.js +1 -1
- package/dist/elements/editable-label.js +1 -1
- package/dist/elements/embed.js +1 -1
- package/dist/elements/empty.js +1 -1
- package/dist/elements/feedback-bar.js +1 -1
- package/dist/elements/file-tree.js +1 -1
- package/dist/elements/file-upload.js +1 -1
- package/dist/elements/form.js +1 -1
- package/dist/elements/hover-card.js +1 -1
- package/dist/elements/icon.js +1 -1
- package/dist/elements/image.js +1 -1
- package/dist/elements/input.js +1 -1
- package/dist/elements/kbd.js +1 -1
- package/dist/elements/link-preview.js +1 -1
- package/dist/elements/loader.js +1 -1
- package/dist/elements/markdown.js +1 -1
- package/dist/elements/menu.js +1 -1
- package/dist/elements/message-skills.js +1 -1
- package/dist/elements/message.js +1 -1
- package/dist/elements/model-switcher.js +1 -1
- package/dist/elements/nav.js +1 -1
- package/dist/elements/notice.js +1 -1
- package/dist/elements/pane-group.js +1 -1
- package/dist/elements/pane.js +1 -1
- package/dist/elements/popover.js +1 -1
- package/dist/elements/progress-bar.js +1 -1
- package/dist/elements/prompt-dock.js +1 -1
- package/dist/elements/prompt-input.js +1 -1
- package/dist/elements/prompt-suggestions.js +1 -1
- package/dist/elements/reasoning.js +1 -1
- package/dist/elements/remote.js +1 -1
- package/dist/elements/resizable.js +1 -1
- package/dist/elements/response-stream.js +2 -2
- package/dist/elements/screen.js +1 -1
- package/dist/elements/scroll-area.js +1 -1
- package/dist/elements/scroll-button.js +1 -1
- package/dist/elements/search.js +1 -1
- package/dist/elements/segmented.js +1 -1
- package/dist/elements/separator.js +1 -1
- package/dist/elements/setting-item.js +1 -1
- package/dist/elements/settings-group.js +1 -1
- package/dist/elements/skeleton.js +1 -1
- package/dist/elements/slot-text.d.ts +51 -0
- package/dist/elements/slots.d.ts +5 -0
- package/dist/elements/source.js +1 -1
- package/dist/elements/status.js +1 -1
- package/dist/elements/switch.js +1 -1
- package/dist/elements/tabs.js +1 -1
- package/dist/elements/tasks.js +1 -1
- package/dist/elements/text-shimmer.js +1 -1
- package/dist/elements/thinking-bar.js +1 -1
- package/dist/elements/thread.js +1 -1
- package/dist/elements/toast.js +1 -1
- package/dist/elements/tool.js +1 -1
- package/dist/elements/tooltip.js +1 -1
- package/dist/elements/voice-input.js +1 -1
- package/dist/elements/voice-output.js +1 -1
- package/dist/elements.d.ts +12 -6
- package/dist/index.js +1535 -1487
- package/dist/index.server.js +2330 -2296
- package/dist/kai.es.js +1 -1
- package/dist/llms/llms-full.txt +12 -3
- package/dist/mcp.es.js +809 -75
- package/dist/react/index.d.ts +13 -3
- package/dist/react.js +14 -14
- package/dist/register-impl-AxIlqjo7.js +145 -0
- package/dist/{solid-BCJUQqwS.js → solid-BGYSJyHG.js} +4901 -4691
- package/dist/{solid-BdENBcEq.js → solid-Dt2nEwfl.js} +4406 -4320
- package/dist/solid.js +1 -1
- package/dist/solid.server.js +1 -1
- package/dist/{variant-aurora-DOb_o4pC.js → variant-aurora-B44X69AG.js} +1 -1
- package/dist/{variant-aurora-BKHHiOgL.js → variant-aurora-BRhV8Flk.js} +2 -2
- package/dist/{variant-aurora-1wMcXRUS.js → variant-aurora-Ds5ou9BH.js} +2 -2
- package/dist/{variant-custom-DWSSgmd2.js → variant-custom-B20FhtYj.js} +2 -2
- package/dist/{variant-custom-CJGB0Kbw.js → variant-custom-BvA1Schl.js} +1 -1
- package/dist/{variant-custom-Co0CkXJF.js → variant-custom-D6Rtvneb.js} +2 -2
- package/dist/{variant-wave-CEODq-9q.js → variant-wave-BUQtkryq.js} +1 -1
- package/dist/{variant-wave-C3-45cL6.js → variant-wave-BVYnqy9L.js} +2 -2
- package/dist/{variant-wave-jdXBnCUZ.js → variant-wave-DXC6_LcH.js} +2 -2
- package/dist/wire/encode.d.ts +79 -6
- package/dist/wire/files.d.ts +116 -0
- package/dist/wire/index.d.ts +3 -1
- package/dist/wire/media-types.d.ts +106 -0
- package/dist/wire.js +596 -368
- package/frameworks/react/index.tsx +9 -5
- package/llms-full.txt +12 -3
- package/package.json +2 -1
- package/src/agent-tooling/archetypes.ts +43 -12
- package/src/agent-tooling/integrations/anthropic.ts +29 -5
- package/src/agent-tooling/integrations/cloudflare.ts +5 -0
- package/src/agent-tooling/integrations/langgraph.ts +13 -0
- package/src/agent-tooling/integrations/mastra.ts +46 -5
- package/src/agent-tooling/integrations/mock.ts +4 -0
- package/src/agent-tooling/integrations/ollama.ts +7 -0
- package/src/agent-tooling/integrations/openai.ts +2 -0
- package/src/agent-tooling/integrations/openrouter.ts +2 -0
- package/src/agent-tooling/integrations/pi.ts +26 -1
- package/src/agent-tooling/integrations/pydantic-ai.ts +37 -3
- package/src/agent-tooling/integrations/vercel-ai-sdk.ts +42 -6
- package/src/agent-tooling/mcp/tools/scaffold.ts +853 -32
- package/src/agent-tooling/registry.ts +57 -0
- package/src/agent-tooling/types.ts +132 -0
- package/src/components/chat-thread.tsx +10 -1
- package/src/components/context.tsx +18 -12
- package/src/components/conversation-item.tsx +6 -0
- package/src/components/model-switcher.tsx +38 -8
- package/src/components/toast.tsx +6 -1
- package/src/elements/button.tsx +26 -3
- package/src/elements/chat.tsx +28 -2
- package/src/elements/default-input.tsx +128 -5
- package/src/elements/element-meta.json +30 -4
- package/src/elements/element-types.d.ts +12 -6
- package/src/elements/menu.tsx +30 -3
- package/src/elements/slot-text.ts +73 -0
- package/src/elements/slots.ts +10 -0
- package/src/index.ts +9 -5
- package/src/wire/encode.ts +229 -14
- package/src/wire/files.ts +342 -0
- package/src/wire/index.ts +21 -0
- package/src/wire/media-types.ts +344 -0
- package/dist/elements/chunks/arrow-left-Bb46H7w9.js +0 -1
- package/dist/elements/chunks/check-C8desc_h.js +0 -1
- package/dist/elements/chunks/chevron-down-DKtc2HNB.js +0 -1
- package/dist/elements/chunks/chevron-right-Dw4GPhwL.js +0 -1
- package/dist/elements/chunks/circle-BeokBsvB.js +0 -1
- package/dist/elements/chunks/circle-check-wJSqt859.js +0 -1
- package/dist/elements/chunks/context-WHDl-xSx.js +0 -1
- package/dist/elements/chunks/conversation-list-CdR14_Y4.js +0 -1
- package/dist/elements/chunks/default-input-CSZqUM81.js +0 -1
- package/dist/elements/chunks/message-square-CnpAm_P0.js +0 -1
- package/dist/elements/chunks/model-switcher-CclaJEHt.js +0 -1
- package/dist/elements/chunks/x-D1qx0aLd.js +0 -1
- package/dist/register-impl-bzrZ44ft.js +0 -145
package/dist/mcp.es.js
CHANGED
|
@@ -805,12 +805,15 @@ const StreamFormat = z$1.enum(["openai-sse", "ai-sdk", "native"]);
|
|
|
805
805
|
const Framework = z$1.enum(["html", "react", "next", "vue", "svelte", "angular", "solid", "fastapi", "express", "worker", "tanstack-start"]);
|
|
806
806
|
const Placement = z$1.enum(["side", "full-page", "docked-widget", "inline"]);
|
|
807
807
|
const KeyExposure = z$1.enum(["frontend-safe", "needs-proxy"]);
|
|
808
|
+
const OutOfBand = z$1.enum(["none", "local-server", "local-binary", "language-runtime"]);
|
|
808
809
|
const DepsSchema = z$1.object({
|
|
809
810
|
npm: z$1.array(z$1.string()).default([]),
|
|
810
811
|
pip: z$1.array(z$1.string()).default([])
|
|
811
812
|
});
|
|
812
813
|
const SECRET_ENV_VAR = /(?:KEY|TOKEN|SECRET|PASSWORD)$/;
|
|
813
814
|
const AUTH_HEADER = /Authorization\s*:|['"]x-api-key['"]\s*:/i;
|
|
815
|
+
const LOOPBACK_FETCH = /\b(?:fetch|URL)\s*\(\s*['"`]https?:\/\/(?:localhost|127\.0\.0\.1)/;
|
|
816
|
+
const SPAWNS_PROCESS = /\bfrom\s+['"]node:child_process['"]|\bspawn\s*\(/;
|
|
814
817
|
z$1.object({
|
|
815
818
|
id: z$1.string(),
|
|
816
819
|
title: z$1.string(),
|
|
@@ -897,7 +900,14 @@ z$1.object({
|
|
|
897
900
|
* failed, so a plain required field would report "Invalid input" and this is
|
|
898
901
|
* the one field where the reader needs to be told what is at stake.
|
|
899
902
|
*/
|
|
900
|
-
keyExposure: KeyExposure.optional()
|
|
903
|
+
keyExposure: KeyExposure.optional(),
|
|
904
|
+
/**
|
|
905
|
+
* See {@link OutOfBand}. Optional in the TYPE only so the refinement below owns
|
|
906
|
+
* the error message — Zod skips refinements when the base object already
|
|
907
|
+
* failed, so a plain required field would report "Invalid input" and this is a
|
|
908
|
+
* field whose reader needs to be told what the four values mean.
|
|
909
|
+
*/
|
|
910
|
+
outOfBand: OutOfBand.optional()
|
|
901
911
|
}).superRefine((integration, ctx) => {
|
|
902
912
|
if (integration.forwardsFromClient.includes("tools") && integration.clientToolFormat === void 0) {
|
|
903
913
|
ctx.addIssue({
|
|
@@ -934,6 +944,40 @@ z$1.object({
|
|
|
934
944
|
});
|
|
935
945
|
}
|
|
936
946
|
}
|
|
947
|
+
if (integration.outOfBand === void 0) {
|
|
948
|
+
ctx.addIssue({
|
|
949
|
+
code: z$1.ZodIssueCode.custom,
|
|
950
|
+
path: ["outOfBand"],
|
|
951
|
+
message: `integration '${integration.id}' declares no outOfBand. State what the developer must supply BEYOND an install and a key, because that is the group create-kai's gateway prompt puts it in: 'none' (a remote endpoint, or the agent runs inside the route), 'local-server' (a process must already be listening, e.g. 'ollama serve'), 'local-binary' (an executable on PATH that the route spawns), or 'language-runtime' (the emitted backend needs a runtime the JS toolchain does not provide, e.g. python + uvicorn). Read the route and the runNote to decide, not the category: 'provider' holds both openai and ollama. There is no default on purpose — a missing value read as 'none' is a scaffold that says "npm run dev" to someone who has nothing to connect to.`
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
if (integration.outOfBand === "none") {
|
|
955
|
+
const routeSources = [
|
|
956
|
+
...Object.values(integration.routeTemplates),
|
|
957
|
+
...integration.webRoute ? [integration.webRoute] : []
|
|
958
|
+
];
|
|
959
|
+
if (integration.language === "python") {
|
|
960
|
+
ctx.addIssue({
|
|
961
|
+
code: z$1.ZodIssueCode.custom,
|
|
962
|
+
path: ["outOfBand"],
|
|
963
|
+
message: `integration '${integration.id}' claims outOfBand 'none' but its route is python, so a consumer whose whole toolchain is node cannot run it without an interpreter and an ASGI server they start themselves. Use 'language-runtime'.`
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
if (routeSources.some((code) => SPAWNS_PROCESS.test(code))) {
|
|
967
|
+
ctx.addIssue({
|
|
968
|
+
code: z$1.ZodIssueCode.custom,
|
|
969
|
+
path: ["outOfBand"],
|
|
970
|
+
message: `integration '${integration.id}' claims outOfBand 'none' but its route spawns a child process, so the program it spawns has to be installed on the host — nothing in package.json puts it there. Use 'local-binary'.`
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
if (routeSources.some((code) => LOOPBACK_FETCH.test(code))) {
|
|
974
|
+
ctx.addIssue({
|
|
975
|
+
code: z$1.ZodIssueCode.custom,
|
|
976
|
+
path: ["outOfBand"],
|
|
977
|
+
message: `integration '${integration.id}' claims outOfBand 'none' but its route fetches a loopback address, so something local has to be listening before the first message works. Use 'local-server'.`
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
}
|
|
937
981
|
});
|
|
938
982
|
z$1.object({
|
|
939
983
|
id: z$1.string(),
|
|
@@ -1022,7 +1066,9 @@ const openai = {
|
|
|
1022
1066
|
deps: { npm: [], pip: [] },
|
|
1023
1067
|
// The route reads OPENAI_API_KEY and puts it in an `Authorization: Bearer`
|
|
1024
1068
|
// header. A static bundle carrying that is a published key.
|
|
1025
|
-
keyExposure: "needs-proxy"
|
|
1069
|
+
keyExposure: "needs-proxy",
|
|
1070
|
+
// A remote HTTPS endpoint and a key. Nothing to install, nothing to start.
|
|
1071
|
+
outOfBand: "none"
|
|
1026
1072
|
};
|
|
1027
1073
|
const anthropic = {
|
|
1028
1074
|
id: "anthropic",
|
|
@@ -1037,10 +1083,19 @@ const anthropic = {
|
|
|
1037
1083
|
// No per-framework templates: the handler below is web-standard, so the
|
|
1038
1084
|
// scaffolder wraps it in whatever the target framework routes with.
|
|
1039
1085
|
routeTemplates: {},
|
|
1040
|
-
webRoute: `/**
|
|
1086
|
+
webRoute: `/** Where an image or document's bytes come from. Anthropic takes both forms
|
|
1087
|
+
* for both block types, which is why this route has no gap where the
|
|
1088
|
+
* OpenAI-shaped one does: a chat-completions \`file\` part is base64-only. */
|
|
1089
|
+
type AnthropicSource =
|
|
1090
|
+
| { type: 'base64'; media_type: string; data: string }
|
|
1091
|
+
| { type: 'url'; url: string };
|
|
1092
|
+
|
|
1093
|
+
/** One Anthropic content block. Open union on the wire; these are the ones the
|
|
1041
1094
|
* scaffold's own thread can produce. */
|
|
1042
1095
|
type AnthropicBlock =
|
|
1043
1096
|
| { type: 'text'; text: string }
|
|
1097
|
+
| { type: 'image'; source: AnthropicSource }
|
|
1098
|
+
| { type: 'document'; source: AnthropicSource }
|
|
1044
1099
|
| { type: 'tool_use'; id: string; name: string; input: unknown }
|
|
1045
1100
|
| { type: 'tool_result'; tool_use_id: string; content: string };
|
|
1046
1101
|
|
|
@@ -1123,12 +1178,24 @@ function toAnthropicBody(messages: ChatRequestBody['messages']): {
|
|
|
1123
1178
|
switch (message.role) {
|
|
1124
1179
|
case 'system': {
|
|
1125
1180
|
// TOP-LEVEL, not a message. Several system turns concatenate.
|
|
1126
|
-
const text = message.content
|
|
1181
|
+
const text = wireText(message.content);
|
|
1127
1182
|
system = system === undefined ? text : \`\${system}\\n\\n\${text}\`;
|
|
1128
1183
|
break;
|
|
1129
1184
|
}
|
|
1130
1185
|
case 'user': {
|
|
1131
|
-
|
|
1186
|
+
// Attachments ride HERE, as image and document blocks beside the text,
|
|
1187
|
+
// in the order the thread authored them.
|
|
1188
|
+
const blocks = wireParts(message.content).map((part): AnthropicBlock => {
|
|
1189
|
+
if (part.kind === 'text') return { type: 'text', text: part.text };
|
|
1190
|
+
const source: AnthropicSource =
|
|
1191
|
+
part.source.type === 'data'
|
|
1192
|
+
? { type: 'base64', media_type: part.mediaType, data: part.source.data }
|
|
1193
|
+
: { type: 'url', url: part.source.url };
|
|
1194
|
+
// \`image/png\` and the bare top-level \`image\` both mean an image; a URL
|
|
1195
|
+
// source reports only the segment because the wire carries no more.
|
|
1196
|
+
return part.mediaType.startsWith('image') ? { type: 'image', source } : { type: 'document', source };
|
|
1197
|
+
});
|
|
1198
|
+
if (blocks.length > 0) pushUser(blocks);
|
|
1132
1199
|
break;
|
|
1133
1200
|
}
|
|
1134
1201
|
case 'tool': {
|
|
@@ -1137,14 +1204,15 @@ function toAnthropicBody(messages: ChatRequestBody['messages']): {
|
|
|
1137
1204
|
{
|
|
1138
1205
|
type: 'tool_result',
|
|
1139
1206
|
tool_use_id: message.tool_call_id ?? '',
|
|
1140
|
-
content: message.content
|
|
1207
|
+
content: wireText(message.content),
|
|
1141
1208
|
},
|
|
1142
1209
|
]);
|
|
1143
1210
|
break;
|
|
1144
1211
|
}
|
|
1145
1212
|
case 'assistant': {
|
|
1146
1213
|
const content: AnthropicBlock[] = [];
|
|
1147
|
-
|
|
1214
|
+
const text = wireText(message.content);
|
|
1215
|
+
if (text) content.push({ type: 'text', text });
|
|
1148
1216
|
for (const call of message.tool_calls ?? []) {
|
|
1149
1217
|
content.push({
|
|
1150
1218
|
type: 'tool_use',
|
|
@@ -1406,7 +1474,9 @@ async function chatHandler(request: Request): Promise<Response> {
|
|
|
1406
1474
|
// The route reads ANTHROPIC_API_KEY into an `x-api-key` header. Note the
|
|
1407
1475
|
// header is NOT `Authorization`, which is why the schema's detector matches
|
|
1408
1476
|
// both spellings.
|
|
1409
|
-
keyExposure: "needs-proxy"
|
|
1477
|
+
keyExposure: "needs-proxy",
|
|
1478
|
+
// A remote HTTPS endpoint and a key. Nothing to install, nothing to start.
|
|
1479
|
+
outOfBand: "none"
|
|
1410
1480
|
};
|
|
1411
1481
|
const openrouter = {
|
|
1412
1482
|
id: "openrouter",
|
|
@@ -1475,7 +1545,9 @@ const openrouter = {
|
|
|
1475
1545
|
// Nothing to install. The route is global `fetch` and imports no module.
|
|
1476
1546
|
deps: { npm: [], pip: [] },
|
|
1477
1547
|
// The route reads OPENROUTER_API_KEY into an `Authorization: Bearer` header.
|
|
1478
|
-
keyExposure: "needs-proxy"
|
|
1548
|
+
keyExposure: "needs-proxy",
|
|
1549
|
+
// A remote HTTPS endpoint and a key. Nothing to install, nothing to start.
|
|
1550
|
+
outOfBand: "none"
|
|
1479
1551
|
};
|
|
1480
1552
|
const vercelAiSdk = {
|
|
1481
1553
|
id: "vercel-ai-sdk",
|
|
@@ -1488,12 +1560,32 @@ const vercelAiSdk = {
|
|
|
1488
1560
|
// scaffolder wraps it in the target framework's own route declaration.
|
|
1489
1561
|
routeTemplates: {},
|
|
1490
1562
|
webRoute: `import { streamText } from 'ai';
|
|
1491
|
-
import type { ModelMessage, AssistantContent, ToolResultPart } from 'ai';
|
|
1563
|
+
import type { ModelMessage, AssistantContent, UserContent, FilePart, ToolResultPart } from 'ai';
|
|
1492
1564
|
|
|
1493
1565
|
// Next.js only: add \`export const maxDuration = 30\` to the route file to allow
|
|
1494
1566
|
// long streaming responses. It is a Next route-segment config, not part of the
|
|
1495
1567
|
// handler, so it lives in the file rather than in here.
|
|
1496
1568
|
|
|
1569
|
+
/**
|
|
1570
|
+
* One attachment to an AI SDK FilePart.
|
|
1571
|
+
*
|
|
1572
|
+
* \`data\` takes the BARE shorthand — a base64 string, or a URL object for a
|
|
1573
|
+
* remote file — rather than the newer tagged \`{ type: 'data' | 'url' }\` form.
|
|
1574
|
+
* Both are accepted by the installed SDK; the shorthand is also what AI SDK v5
|
|
1575
|
+
* and v6 accept, so this route keeps compiling if the app pins an older \`ai\`.
|
|
1576
|
+
*
|
|
1577
|
+
* Images arrive as \`image_url\` and become FileParts too. \`ImagePart\` still
|
|
1578
|
+
* exists but is deprecated in favour of exactly this.
|
|
1579
|
+
*/
|
|
1580
|
+
function toFilePart(part: Extract<WirePart, { kind: 'file' }>): FilePart {
|
|
1581
|
+
return {
|
|
1582
|
+
type: 'file',
|
|
1583
|
+
mediaType: part.mediaType,
|
|
1584
|
+
...(part.filename === undefined ? {} : { filename: part.filename }),
|
|
1585
|
+
data: part.source.type === 'data' ? part.source.data : new URL(part.source.url),
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1497
1589
|
// The kit's wire format is OpenAI-shaped (tool_calls on the assistant message,
|
|
1498
1590
|
// a flat content string on the tool message). The AI SDK's ModelMessage is not:
|
|
1499
1591
|
// a tool call is a CONTENT PART on the assistant message, and a tool result is a
|
|
@@ -1502,22 +1594,35 @@ import type { ModelMessage, AssistantContent, ToolResultPart } from 'ai';
|
|
|
1502
1594
|
function toModelMessages(messages: ChatRequestBody['messages']): ModelMessage[] {
|
|
1503
1595
|
return messages.map((message): ModelMessage => {
|
|
1504
1596
|
switch (message.role) {
|
|
1597
|
+
// A system prompt is text-only on every wire, so the array form collapses.
|
|
1505
1598
|
case 'system':
|
|
1506
|
-
|
|
1507
|
-
|
|
1599
|
+
return { role: 'system', content: wireText(message.content) };
|
|
1600
|
+
case 'user': {
|
|
1601
|
+
const parts = wireParts(message.content);
|
|
1602
|
+
// Plain string for an ordinary turn: identical to what this route sent
|
|
1603
|
+
// before attachments existed, and the shape the SDK docs lead with.
|
|
1604
|
+
if (parts.every((p) => p.kind === 'text')) {
|
|
1605
|
+
return { role: 'user', content: wireText(message.content) };
|
|
1606
|
+
}
|
|
1607
|
+
const content: UserContent = parts.map((p) =>
|
|
1608
|
+
p.kind === 'text' ? { type: 'text' as const, text: p.text } : toFilePart(p),
|
|
1609
|
+
);
|
|
1610
|
+
return { role: 'user', content };
|
|
1611
|
+
}
|
|
1508
1612
|
case 'tool': {
|
|
1509
1613
|
const result: ToolResultPart = {
|
|
1510
1614
|
type: 'tool-result',
|
|
1511
1615
|
toolCallId: message.tool_call_id ?? '',
|
|
1512
1616
|
toolName: message.name ?? '',
|
|
1513
|
-
output: { type: 'text', value: message.content
|
|
1617
|
+
output: { type: 'text', value: wireText(message.content) },
|
|
1514
1618
|
};
|
|
1515
1619
|
return { role: 'tool', content: [result] };
|
|
1516
1620
|
}
|
|
1517
1621
|
case 'assistant': {
|
|
1518
|
-
|
|
1622
|
+
const text = wireText(message.content);
|
|
1623
|
+
if (!message.tool_calls?.length) return { role: 'assistant', content: text };
|
|
1519
1624
|
const content: AssistantContent = [];
|
|
1520
|
-
if (
|
|
1625
|
+
if (text) content.push({ type: 'text', text });
|
|
1521
1626
|
for (const call of message.tool_calls) {
|
|
1522
1627
|
content.push({
|
|
1523
1628
|
type: 'tool-call',
|
|
@@ -1586,7 +1691,10 @@ async function chatHandler(request: Request): Promise<Response> {
|
|
|
1586
1691
|
// streamText(). This is the case that makes inference from route source
|
|
1587
1692
|
// unsafe and the declaration necessary; `envVars` is what the schema's safety
|
|
1588
1693
|
// net can still see.
|
|
1589
|
-
keyExposure: "needs-proxy"
|
|
1694
|
+
keyExposure: "needs-proxy",
|
|
1695
|
+
// The AI Gateway is a remote HTTPS endpoint reached through the `ai` package,
|
|
1696
|
+
// which is an ordinary npm dependency (see `deps`). Nothing to start.
|
|
1697
|
+
outOfBand: "none"
|
|
1590
1698
|
};
|
|
1591
1699
|
const langgraph = {
|
|
1592
1700
|
id: "langgraph",
|
|
@@ -1696,7 +1804,20 @@ async function chatHandler(request: Request): Promise<Response> {
|
|
|
1696
1804
|
deps: { npm: ["@langchain/langgraph", "@langchain/openai", "@langchain/core", "zod"], pip: [] },
|
|
1697
1805
|
// No key appears in the route: `new ChatOpenAI(...)` reads OPENAI_API_KEY from
|
|
1698
1806
|
// the environment itself. Same invisible-key shape as vercel-ai-sdk.
|
|
1699
|
-
keyExposure: "needs-proxy"
|
|
1807
|
+
keyExposure: "needs-proxy",
|
|
1808
|
+
// 'none', and this is the entry most likely to be "corrected" to something
|
|
1809
|
+
// else, so the reason is here rather than assumed. The create-kai spec lists
|
|
1810
|
+
// LangGraph under "Bring a server or runtime". That is WRONG for this route:
|
|
1811
|
+
// the graph is built and compiled IN PROCESS above (`createReactAgent` over a
|
|
1812
|
+
// `new ChatOpenAI(...)`), so there is no LangGraph server, no port and no
|
|
1813
|
+
// second process — the four packages in `deps` are the whole install and
|
|
1814
|
+
// `runNote` asks for a key and nothing else.
|
|
1815
|
+
//
|
|
1816
|
+
// LangGraph Platform / `langgraph dev` IS a server, and an integration that
|
|
1817
|
+
// talked to one over HTTP would be 'local-server'. That is a different route
|
|
1818
|
+
// than the one above, and if it is ever added it must be added as its own
|
|
1819
|
+
// catalog entry rather than by changing this line.
|
|
1820
|
+
outOfBand: "none"
|
|
1700
1821
|
};
|
|
1701
1822
|
const cloudflare = {
|
|
1702
1823
|
id: "cloudflare",
|
|
@@ -1863,7 +1984,12 @@ export default {
|
|
|
1863
1984
|
// The two routes differ and the stricter one governs: the worker template uses
|
|
1864
1985
|
// the `env.AI` binding and holds no token in code at all, but a binding is a
|
|
1865
1986
|
// Worker capability, not a browser one. Either way a server is required.
|
|
1866
|
-
keyExposure: "needs-proxy"
|
|
1987
|
+
keyExposure: "needs-proxy",
|
|
1988
|
+
// Workers AI is a remote HTTPS endpoint (the REST route) or a platform binding
|
|
1989
|
+
// (the worker route) — either way there is nothing on the developer's machine
|
|
1990
|
+
// to install or run. `wrangler` is a devDependency of a Worker app, not an
|
|
1991
|
+
// out-of-band prerequisite of this integration.
|
|
1992
|
+
outOfBand: "none"
|
|
1867
1993
|
};
|
|
1868
1994
|
const ollama = {
|
|
1869
1995
|
id: "ollama",
|
|
@@ -1942,7 +2068,14 @@ const ollama = {
|
|
|
1942
2068
|
// empty, so there is no secret for a bundle to leak. runNote already documents
|
|
1943
2069
|
// the browser-direct path (set OLLAMA_ORIGINS to allow the page origin), which
|
|
1944
2070
|
// is the path this value unlocks.
|
|
1945
|
-
keyExposure: "frontend-safe"
|
|
2071
|
+
keyExposure: "frontend-safe",
|
|
2072
|
+
// The route fetches http://localhost:11434, so Ollama must ALREADY be running:
|
|
2073
|
+
// `ollama serve`, then `ollama pull <model>` for the model the route pins.
|
|
2074
|
+
// Nothing in package.json installs or starts it — `deps.npm` is empty because
|
|
2075
|
+
// the route is bare `fetch`, which is exactly why this cannot be inferred from
|
|
2076
|
+
// the dependency list. The schema's LOOPBACK_FETCH net catches this one
|
|
2077
|
+
// independently, so a later edit to 'none' fails at the catalog boundary.
|
|
2078
|
+
outOfBand: "local-server"
|
|
1946
2079
|
};
|
|
1947
2080
|
const mastra = {
|
|
1948
2081
|
id: "mastra",
|
|
@@ -1979,17 +2112,43 @@ async function chatHandler(request: Request): Promise<Response> {
|
|
|
1979
2112
|
// is never null. A 'tool' wire message is OUR client-side tool bookkeeping;
|
|
1980
2113
|
// this agent owns its tools server-side (forwardsFromClient is empty), so it
|
|
1981
2114
|
// has no schema for that shape and the entry is dropped rather than guessed.
|
|
1982
|
-
|
|
2115
|
+
//
|
|
2116
|
+
// Only the USER role takes content parts: a system prompt is text by
|
|
2117
|
+
// definition, and an assistant turn here is replayed history, not a new
|
|
2118
|
+
// upload. \`data\` uses the bare base64-or-URL shorthand, which every AI SDK
|
|
2119
|
+
// major Mastra accepts understands.
|
|
2120
|
+
type MastraFilePart = { type: 'file'; data: string | URL; mediaType: string; filename?: string };
|
|
2121
|
+
type MastraUserContent = string | Array<{ type: 'text'; text: string } | MastraFilePart>;
|
|
2122
|
+
type MastraMessage = { role: 'system'; content: string } | { role: 'user'; content: MastraUserContent } | { role: 'assistant'; content: string };
|
|
1983
2123
|
const mastraMessages: MastraMessage[] = [];
|
|
1984
2124
|
for (const m of messages) {
|
|
1985
|
-
const content = m.content ?? '';
|
|
1986
2125
|
// Each branch constructs a literal with ONE fixed role, not m.role (which is
|
|
1987
2126
|
// still typed as the 4-way union): a union-VALUED field on a single object
|
|
1988
2127
|
// does not structurally match a union of role-discriminated objects, so
|
|
1989
2128
|
// widening back to m.role here would reintroduce the original TS2345.
|
|
1990
|
-
if (m.role === 'system') mastraMessages.push({ role: 'system', content });
|
|
1991
|
-
else if (m.role === '
|
|
1992
|
-
else if (m.role === '
|
|
2129
|
+
if (m.role === 'system') mastraMessages.push({ role: 'system', content: wireText(m.content) });
|
|
2130
|
+
else if (m.role === 'assistant') mastraMessages.push({ role: 'assistant', content: wireText(m.content) });
|
|
2131
|
+
else if (m.role === 'user') {
|
|
2132
|
+
const parts = wireParts(m.content);
|
|
2133
|
+
// Plain string unless the turn actually carries an attachment.
|
|
2134
|
+
if (parts.every((p) => p.kind === 'text')) {
|
|
2135
|
+
mastraMessages.push({ role: 'user', content: wireText(m.content) });
|
|
2136
|
+
} else {
|
|
2137
|
+
mastraMessages.push({
|
|
2138
|
+
role: 'user',
|
|
2139
|
+
content: parts.map((p) =>
|
|
2140
|
+
p.kind === 'text'
|
|
2141
|
+
? { type: 'text' as const, text: p.text }
|
|
2142
|
+
: {
|
|
2143
|
+
type: 'file' as const,
|
|
2144
|
+
mediaType: p.mediaType,
|
|
2145
|
+
...(p.filename === undefined ? {} : { filename: p.filename }),
|
|
2146
|
+
data: p.source.type === 'data' ? p.source.data : new URL(p.source.url),
|
|
2147
|
+
},
|
|
2148
|
+
),
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
1993
2152
|
}
|
|
1994
2153
|
|
|
1995
2154
|
let agentStream: Awaited<ReturnType<ReturnType<typeof mastra.getAgent>['stream']>>;
|
|
@@ -2073,7 +2232,22 @@ async function chatHandler(request: Request): Promise<Response> {
|
|
|
2073
2232
|
// agent endpoint — a decision a scaffolder must not make silently, even though
|
|
2074
2233
|
// no key leaks. The conservative direction is the cheap one: a needless server
|
|
2075
2234
|
// hop costs a process, the other error costs the endpoint.
|
|
2076
|
-
keyExposure: "needs-proxy"
|
|
2235
|
+
keyExposure: "needs-proxy",
|
|
2236
|
+
// THE JUDGEMENT CALL FOR THIS FIELD, exactly as keyExposure above is, and for
|
|
2237
|
+
// a related reason: no automatic check can decide it. MASTRA_URL is a base URL
|
|
2238
|
+
// for a server this integration does not ship and cannot start; the route's
|
|
2239
|
+
// only mention of loopback is inside a `throw new Error(...)` string, which
|
|
2240
|
+
// LOOPBACK_FETCH deliberately does not match (a net that fired on prose would
|
|
2241
|
+
// be right here by accident and wrong elsewhere). So 'none' would parse
|
|
2242
|
+
// cleanly.
|
|
2243
|
+
//
|
|
2244
|
+
// It is 'local-server' on what the integration actually requires: a Mastra
|
|
2245
|
+
// server has to be reachable at MASTRA_URL before the first message works,
|
|
2246
|
+
// `mastra dev` is how a developer gets one (port 4111), and `@mastra/client-js`
|
|
2247
|
+
// is a CLIENT for it, not the server itself. Printing "start your Mastra
|
|
2248
|
+
// server" costs a line; omitting it produces a scaffold that fetches a URL
|
|
2249
|
+
// nothing answers.
|
|
2250
|
+
outOfBand: "local-server"
|
|
2077
2251
|
};
|
|
2078
2252
|
const pi = {
|
|
2079
2253
|
id: "pi",
|
|
@@ -2098,7 +2272,25 @@ app.use(express.json());
|
|
|
2098
2272
|
// POST /api/chat: bridge a Pi RPC session to the browser as SSE.
|
|
2099
2273
|
app.post('/api/chat', (req, res) => {
|
|
2100
2274
|
const { messages } = req.body as { messages: OpenAIWireMessage[] };
|
|
2101
|
-
const
|
|
2275
|
+
const last = messages.at(-1)?.content;
|
|
2276
|
+
// \`content\` is a plain string until the turn carries an attachment, at which
|
|
2277
|
+
// point it is an ARRAY of content parts. Pi's RPC mode takes a TEXT prompt and
|
|
2278
|
+
// has no channel for a file, so an attachment is REFUSED here. Passing the
|
|
2279
|
+
// array straight through would JSON.stringify an object graph into the prompt
|
|
2280
|
+
// — no type error, no crash, just a model reading serialised noise.
|
|
2281
|
+
if (Array.isArray(last) && last.some((part) => part.type !== 'text')) {
|
|
2282
|
+
res.status(400).json({
|
|
2283
|
+
error: {
|
|
2284
|
+
message:
|
|
2285
|
+
'This Pi bridge forwards a text prompt only and has no channel for an attachment. Extract the file content into the message text, or send it through a tool.',
|
|
2286
|
+
},
|
|
2287
|
+
});
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
const prompt =
|
|
2291
|
+
typeof last === 'string'
|
|
2292
|
+
? last
|
|
2293
|
+
: (last ?? []).map((part) => (part.type === 'text' ? part.text : '')).join('');
|
|
2102
2294
|
|
|
2103
2295
|
// Headers before the first frame: without text/event-stream the browser
|
|
2104
2296
|
// buffers the body and readOpenAIStream never sees a frame.
|
|
@@ -2165,7 +2357,14 @@ app.listen(3001, () => console.log('chat api: http://localhost:3001/api/chat'));
|
|
|
2165
2357
|
// warns that Pi runs with full user permissions and wants sandboxing before it
|
|
2166
2358
|
// is exposed. "No key" is not the same fact as "safe in the browser", and
|
|
2167
2359
|
// collapsing the two is exactly how this flag would be got wrong.
|
|
2168
|
-
keyExposure: "needs-proxy"
|
|
2360
|
+
keyExposure: "needs-proxy",
|
|
2361
|
+
// Not 'local-server': nothing is listening in advance. The bridge spawns
|
|
2362
|
+
// `pi --mode rpc` per request, so what the developer supplies is an EXECUTABLE
|
|
2363
|
+
// on PATH ("Pi must be installed locally and available on PATH as 'pi'"), and
|
|
2364
|
+
// "start the server first" would be the wrong instruction to print. Pi is not
|
|
2365
|
+
// in `deps` for the same reason — the route reaches it with spawn(), not an
|
|
2366
|
+
// import. The schema's SPAWNS_PROCESS net catches this one independently.
|
|
2367
|
+
outOfBand: "local-binary"
|
|
2169
2368
|
};
|
|
2170
2369
|
const pydanticAi = {
|
|
2171
2370
|
id: "pydantic-ai",
|
|
@@ -2177,7 +2376,7 @@ const pydanticAi = {
|
|
|
2177
2376
|
routeTemplates: {
|
|
2178
2377
|
fastapi: `# main.py
|
|
2179
2378
|
import json
|
|
2180
|
-
from fastapi import FastAPI
|
|
2379
|
+
from fastapi import FastAPI, HTTPException
|
|
2181
2380
|
from fastapi.middleware.cors import CORSMiddleware
|
|
2182
2381
|
from fastapi.responses import StreamingResponse
|
|
2183
2382
|
from pydantic import BaseModel
|
|
@@ -2190,15 +2389,41 @@ app.add_middleware(
|
|
|
2190
2389
|
CORSMiddleware, allow_origins=['*'], allow_methods=['*'], allow_headers=['*']
|
|
2191
2390
|
)
|
|
2192
2391
|
|
|
2392
|
+
class ContentPart(BaseModel):
|
|
2393
|
+
type: str
|
|
2394
|
+
text: str | None = None
|
|
2395
|
+
|
|
2193
2396
|
class Message(BaseModel):
|
|
2194
2397
|
role: str
|
|
2195
|
-
|
|
2398
|
+
# A plain string until the turn carries an attachment, at which point the
|
|
2399
|
+
# wire sends an ARRAY of content parts. Declaring this \`str\` alone made a
|
|
2400
|
+
# message with a file a 422 that named pydantic rather than the cause.
|
|
2401
|
+
content: str | list[ContentPart] | None = None
|
|
2196
2402
|
|
|
2197
2403
|
class ChatRequest(BaseModel):
|
|
2198
2404
|
messages: list[Message]
|
|
2199
2405
|
|
|
2406
|
+
def prompt_text(message: Message) -> str:
|
|
2407
|
+
"""The text of a turn, refusing what this route cannot carry.
|
|
2408
|
+
|
|
2409
|
+
agent.run_stream() takes a TEXT prompt, so an image or a document has no
|
|
2410
|
+
channel here. Dropping it would send the model a turn that silently lost the
|
|
2411
|
+
user's file; a 400 that names the reason is the honest failure.
|
|
2412
|
+
"""
|
|
2413
|
+
if message.content is None:
|
|
2414
|
+
return ''
|
|
2415
|
+
if isinstance(message.content, str):
|
|
2416
|
+
return message.content
|
|
2417
|
+
if any(part.type != 'text' for part in message.content):
|
|
2418
|
+
raise HTTPException(
|
|
2419
|
+
status_code=400,
|
|
2420
|
+
detail='This route forwards a text prompt only and has no channel for an attachment. '
|
|
2421
|
+
'Extract the file content into the message text, or send it through a tool.',
|
|
2422
|
+
)
|
|
2423
|
+
return ''.join(part.text or '' for part in message.content)
|
|
2424
|
+
|
|
2200
2425
|
async def openai_sse(messages: list[Message]):
|
|
2201
|
-
prompt = messages[-1]
|
|
2426
|
+
prompt = prompt_text(messages[-1]) if messages else ''
|
|
2202
2427
|
async with agent.run_stream(prompt) as result:
|
|
2203
2428
|
async for delta in result.stream_text(delta=True):
|
|
2204
2429
|
chunk = {'choices': [{'delta': {'content': delta}}]}
|
|
@@ -2233,7 +2458,15 @@ async def chat(req: ChatRequest):
|
|
|
2233
2458
|
// DIRECTLY on :8000, with no JS proxy in front. That still is not
|
|
2234
2459
|
// 'frontend-safe' — the flag asks where the SECRET lives, and it lives in the
|
|
2235
2460
|
// python process. The FastAPI service IS the server hop.
|
|
2236
|
-
keyExposure: "needs-proxy"
|
|
2461
|
+
keyExposure: "needs-proxy",
|
|
2462
|
+
// The emitted backend is a FastAPI service: a python interpreter, the four
|
|
2463
|
+
// `deps.pip` packages, and `uvicorn main:app` on its own port — none of which a
|
|
2464
|
+
// node toolchain provides or starts. It also needs OPENAI_API_KEY, so this is
|
|
2465
|
+
// the entry that proves the groups are not mutually exclusive: a runtime
|
|
2466
|
+
// prerequisite and a key at once. The prompt should lead with the runtime,
|
|
2467
|
+
// because a key is useless until the service runs. The schema's `language ===
|
|
2468
|
+
// 'python'` net catches this one independently.
|
|
2469
|
+
outOfBand: "language-runtime"
|
|
2237
2470
|
};
|
|
2238
2471
|
const mock = {
|
|
2239
2472
|
id: "mock",
|
|
@@ -2257,7 +2490,11 @@ const mock = {
|
|
|
2257
2490
|
// and no secret, so there is nothing a public bundle could give away. This is
|
|
2258
2491
|
// the one place where "declares nothing" genuinely means safe — which is
|
|
2259
2492
|
// precisely why it still has to SAY so rather than be left blank.
|
|
2260
|
-
keyExposure: "frontend-safe"
|
|
2493
|
+
keyExposure: "frontend-safe",
|
|
2494
|
+
// Nothing at all, and here that is the literal truth rather than a shorthand:
|
|
2495
|
+
// there is no route, no upstream and no process. This is the "No backend"
|
|
2496
|
+
// group of create-kai's gateway prompt all by itself.
|
|
2497
|
+
outOfBand: "none"
|
|
2261
2498
|
};
|
|
2262
2499
|
const archetypes$1 = [
|
|
2263
2500
|
{
|
|
@@ -2301,6 +2538,34 @@ const archetypes$1 = [
|
|
|
2301
2538
|
components: ["kai-chat", "kai-voice-input"],
|
|
2302
2539
|
defaultPlacement: "full-page",
|
|
2303
2540
|
docsSlug: "examples/voice-assistant"
|
|
2541
|
+
},
|
|
2542
|
+
/**
|
|
2543
|
+
* THE PRESET THAT EXISTS TO MAKE A CAPABILITY REACHABLE, not to name a product
|
|
2544
|
+
* shape someone asked for.
|
|
2545
|
+
*
|
|
2546
|
+
* `kai-file-upload` and `kai-attachments` were registered elements that NO
|
|
2547
|
+
* preset composed, and `listCapabilityGroups` derives its answer from this
|
|
2548
|
+
* table — so the attachments capability reached neither `listSurfaceProbes`
|
|
2549
|
+
* nor `verify:scaffold`, and `renderSurface` had no branch that emitted either
|
|
2550
|
+
* tag. A caller could pass them in `components` (the axis takes any list) and
|
|
2551
|
+
* get two bare `<kai-file-upload></kai-file-upload>` / `<kai-attachments>`
|
|
2552
|
+
* siblings with nothing wired to them: on screen, inert, silent.
|
|
2553
|
+
*
|
|
2554
|
+
* The pair is ONE capability for the same reason `kai-artifact` +
|
|
2555
|
+
* `kai-resizable` are: a dropzone with no list is a black hole, and a list
|
|
2556
|
+
* with no dropzone can never fill. `hasAttachments` in scaffold.ts requires
|
|
2557
|
+
* both for exactly that reason.
|
|
2558
|
+
*
|
|
2559
|
+
* The components match the create-kai spec's own feature table
|
|
2560
|
+
* (`attachments` -> `kai-file-upload`, `kai-attachments`), so the CLI's
|
|
2561
|
+
* multi-select and this preset name the same surface rather than two.
|
|
2562
|
+
*/
|
|
2563
|
+
{
|
|
2564
|
+
id: "attachments",
|
|
2565
|
+
title: "File attachments",
|
|
2566
|
+
components: ["kai-chat", "kai-file-upload", "kai-attachments"],
|
|
2567
|
+
defaultPlacement: "full-page",
|
|
2568
|
+
docsSlug: "examples/attachments"
|
|
2304
2569
|
}
|
|
2305
2570
|
];
|
|
2306
2571
|
const integrations = [
|
|
@@ -2433,11 +2698,13 @@ function realStreamBody(opts) {
|
|
|
2433
2698
|
cards = false,
|
|
2434
2699
|
valueSource = "e.detail.value",
|
|
2435
2700
|
afterValue = [],
|
|
2436
|
-
mock: mock2 = false
|
|
2701
|
+
mock: mock2 = false,
|
|
2702
|
+
filesExpr
|
|
2437
2703
|
} = opts;
|
|
2438
2704
|
const asConst = strictRoles ? " as const" : "";
|
|
2439
2705
|
const historyType = strictRoles ? ": ChatMessage[]" : "";
|
|
2440
|
-
const
|
|
2706
|
+
const fileParts = filesExpr ? `...${filesExpr}.map((attachment) => ({ type: 'file'${asConst}, attachment })), ` : "";
|
|
2707
|
+
const userMessage = `{ id: crypto.randomUUID(), role: 'user'${asConst}, parts: [${fileParts}{ type: 'text', text: value }] }`;
|
|
2441
2708
|
const open = toolLoop ? thread.open({ pad, userMessage, typed: strictRoles }) : [
|
|
2442
2709
|
`${pad}const history${historyType} = [...${read}, ${userMessage}];`,
|
|
2443
2710
|
`${pad}${commitSet("history")}`
|
|
@@ -2585,12 +2852,13 @@ function assertCardToolFormat(integration) {
|
|
|
2585
2852
|
return provider;
|
|
2586
2853
|
}
|
|
2587
2854
|
function wireImportLines(opts) {
|
|
2588
|
-
const { pad = "", typed, toolLoop = false, setMessagesType = false, cards = false, cardTools: emitsCardTools = false, mock: mock2 = false } = opts;
|
|
2855
|
+
const { pad = "", typed, toolLoop = false, setMessagesType = false, cards = false, cardTools: emitsCardTools = false, mock: mock2 = false, attachments = false } = opts;
|
|
2589
2856
|
const stateNames = [
|
|
2590
2857
|
"createAssistantStream",
|
|
2591
2858
|
// The mock's canned reply comes from the kit, not from a copy pasted into
|
|
2592
2859
|
// this file. One implementation, shared with create-kai and the starters.
|
|
2593
2860
|
...mock2 ? ["createMockResponder"] : [],
|
|
2861
|
+
...attachments ? ["type AttachmentData"] : [],
|
|
2594
2862
|
...typed ? ["type ChatMessage"] : [],
|
|
2595
2863
|
...typed && setMessagesType ? ["type SetMessages"] : []
|
|
2596
2864
|
].join(", ");
|
|
@@ -2743,6 +3011,68 @@ const WORKSPACE_STRUCTURAL_TAGS = /* @__PURE__ */ new Set(["kai-resizable", "kai
|
|
|
2743
3011
|
function isWorkspace(components) {
|
|
2744
3012
|
return components.includes("kai-resizable") && components.includes("kai-artifact");
|
|
2745
3013
|
}
|
|
3014
|
+
const ATTACHMENT_TAGS = /* @__PURE__ */ new Set(["kai-file-upload", "kai-attachments"]);
|
|
3015
|
+
function hasAttachments(components) {
|
|
3016
|
+
return components.includes("kai-file-upload") && components.includes("kai-attachments");
|
|
3017
|
+
}
|
|
3018
|
+
const ATTACHMENT_WIRE_NOTE = [
|
|
3019
|
+
"// The staged files ride along on the message as `file` parts, so they RENDER",
|
|
3020
|
+
"// in the thread AND reach the model: toOpenAIMessages / toAnthropicMessages",
|
|
3021
|
+
"// encode each one into the content block its API takes.",
|
|
3022
|
+
"// Which is why toAttachment below stages a `data:` URI rather than a",
|
|
3023
|
+
"// URL.createObjectURL blob: an object URL previews fine and resolves ONLY",
|
|
3024
|
+
"// inside this tab, so the encoder rejects it rather than send an address the",
|
|
3025
|
+
"// provider cannot fetch.",
|
|
3026
|
+
"// What can be sent is not a list worth copying into a comment -- it moves, and",
|
|
3027
|
+
'// a copy cannot. `encodableMediaTypes()` from "@kitn.ai/ui/wire" IS the set,',
|
|
3028
|
+
"// and `resolveMediaPolicy().decide(mediaType)` answers for one file. Anything",
|
|
3029
|
+
"// outside it throws at encode time instead of going missing from the request.",
|
|
3030
|
+
"// A file the browser could not name (a .rs, a .toml) is settled by decoding",
|
|
3031
|
+
"// its bytes, never by its extension."
|
|
3032
|
+
];
|
|
3033
|
+
function fileToAttachmentLines(pad, typeName) {
|
|
3034
|
+
return [
|
|
3035
|
+
`${pad}/** <kai-file-upload> hands over File objects; <kai-attachments> renders these. */`,
|
|
3036
|
+
`${pad}async function toAttachment(file: File): Promise<${typeName}> {`,
|
|
3037
|
+
`${pad} // A data: URI, NOT URL.createObjectURL. Both preview identically, but an`,
|
|
3038
|
+
`${pad} // object URL resolves only inside this tab, so it can never reach a model —`,
|
|
3039
|
+
`${pad} // the encoder rejects one rather than send an address the provider cannot`,
|
|
3040
|
+
`${pad} // fetch. Reading is async, which is why this function is.`,
|
|
3041
|
+
`${pad} const url = await new Promise<string>((resolve, reject) => {`,
|
|
3042
|
+
`${pad} const reader = new FileReader();`,
|
|
3043
|
+
`${pad} reader.onload = () => resolve(String(reader.result));`,
|
|
3044
|
+
`${pad} reader.onerror = () => reject(reader.error ?? new Error('Could not read ' + file.name));`,
|
|
3045
|
+
`${pad} reader.readAsDataURL(file);`,
|
|
3046
|
+
`${pad} });`,
|
|
3047
|
+
`${pad} return {`,
|
|
3048
|
+
`${pad} id: crypto.randomUUID(),`,
|
|
3049
|
+
`${pad} type: 'file',`,
|
|
3050
|
+
`${pad} filename: file.name,`,
|
|
3051
|
+
`${pad} // '' for an extensionless file, and an empty mediaType would render as a`,
|
|
3052
|
+
`${pad} // blank detail line rather than falling back to the generic icon.`,
|
|
3053
|
+
`${pad} mediaType: file.type || undefined,`,
|
|
3054
|
+
`${pad} url,`,
|
|
3055
|
+
`${pad} };`,
|
|
3056
|
+
`${pad}}`
|
|
3057
|
+
];
|
|
3058
|
+
}
|
|
3059
|
+
const ATTACHMENT_MERGE_NOTE = [
|
|
3060
|
+
"// <kai-chat> renders its OWN paperclip (there is no prop that hides it), and its",
|
|
3061
|
+
"// staged files arrive on this event as detail.attachments. The dropzone above is",
|
|
3062
|
+
"// the second source. Both are folded in — reading either one alone silently",
|
|
3063
|
+
"// drops files the user really attached."
|
|
3064
|
+
];
|
|
3065
|
+
function attachmentTurnLines(opts) {
|
|
3066
|
+
const { stagedExpr, clear, fromSubmitEvent } = opts;
|
|
3067
|
+
return [
|
|
3068
|
+
...fromSubmitEvent ? ATTACHMENT_MERGE_NOTE : [
|
|
3069
|
+
"// This target renders PromptInput directly rather than <kai-chat>, so the",
|
|
3070
|
+
"// dropzone is the only source of staged files — there is no kai-submit detail."
|
|
3071
|
+
],
|
|
3072
|
+
fromSubmitEvent ? `const files = [...${stagedExpr}, ...e.detail.attachments];` : `const files = [...${stagedExpr}];`,
|
|
3073
|
+
...clear
|
|
3074
|
+
];
|
|
3075
|
+
}
|
|
2746
3076
|
const SAMPLE_AGENTIC_MESSAGE = {
|
|
2747
3077
|
id: "sample-assistant",
|
|
2748
3078
|
role: "assistant",
|
|
@@ -2779,11 +3109,22 @@ function sampleSeedComment(isMock, pad, decl) {
|
|
|
2779
3109
|
}
|
|
2780
3110
|
function componentTags(components, chatFill) {
|
|
2781
3111
|
const companionTags = components.filter(
|
|
2782
|
-
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t)
|
|
3112
|
+
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t) && !(hasAttachments(components) && ATTACHMENT_TAGS.has(t))
|
|
2783
3113
|
);
|
|
2784
3114
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
2785
3115
|
const hasStandaloneCompanions = companionTags.length > 0;
|
|
2786
3116
|
const lines = [];
|
|
3117
|
+
if (hasAttachments(components)) {
|
|
3118
|
+
lines.push(
|
|
3119
|
+
` <!-- Drop files here to stage them for the NEXT message. src/main.ts wires`,
|
|
3120
|
+
` kai-files-added -> the staged list -> kai-attachments' items property. -->`,
|
|
3121
|
+
` <div style="flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem;">`,
|
|
3122
|
+
` <kai-file-upload id="upload" accept="image/*,application/pdf"></kai-file-upload>`,
|
|
3123
|
+
` <!-- items is a JS PROPERTY (arrays can't be attributes); 'removable' fires kai-remove. -->`,
|
|
3124
|
+
` <kai-attachments id="attachments" variant="inline" removable></kai-attachments>`,
|
|
3125
|
+
` </div>`
|
|
3126
|
+
);
|
|
3127
|
+
}
|
|
2787
3128
|
if (isWorkspace(components)) {
|
|
2788
3129
|
lines.push(
|
|
2789
3130
|
` <!-- SCAF-14: workspace split — chat pane left, artifact preview right. -->`,
|
|
@@ -2826,6 +3167,40 @@ function componentTags(components, chatFill) {
|
|
|
2826
3167
|
function htmlModule(ctx, components) {
|
|
2827
3168
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
2828
3169
|
const hasSources = components.includes("kai-sources");
|
|
3170
|
+
const attachments = hasAttachments(components);
|
|
3171
|
+
const attachmentModuleLines = attachments ? [
|
|
3172
|
+
...ATTACHMENT_WIRE_NOTE,
|
|
3173
|
+
`//`,
|
|
3174
|
+
`// AttachmentData is the kit's own type for a staged file — the same one`,
|
|
3175
|
+
`// <kai-attachments>.items holds and the same one a 'file' message part holds.`,
|
|
3176
|
+
`let staged: AttachmentData[] = [];`,
|
|
3177
|
+
``,
|
|
3178
|
+
...fileToAttachmentLines("", "AttachmentData"),
|
|
3179
|
+
``
|
|
3180
|
+
] : [];
|
|
3181
|
+
const attachmentSetupLines = attachments ? [
|
|
3182
|
+
` const uploadEl = document.getElementById('upload') as HTMLElement;`,
|
|
3183
|
+
` const attachmentsEl = document.getElementById('attachments') as KaiAttachmentsElement;`,
|
|
3184
|
+
` // Same upgrade rule as <kai-chat>: 'items' is an array, so it is a PROPERTY,`,
|
|
3185
|
+
` // and a property set before the element upgrades is dropped on upgrade.`,
|
|
3186
|
+
` await customElements.whenDefined('kai-attachments');`,
|
|
3187
|
+
` // A NEW array per write — same contract as chat.messages.`,
|
|
3188
|
+
` const showStaged = () => { attachmentsEl.items = [...staged]; };`,
|
|
3189
|
+
``,
|
|
3190
|
+
` uploadEl.addEventListener('kai-files-added', async (event: Event) => {`,
|
|
3191
|
+
` const { files } = (event as CustomEvent<{ files: File[] }>).detail;`,
|
|
3192
|
+
` // Read every file BEFORE appending: a per-file append would order the`,
|
|
3193
|
+
` // list by whichever finished reading first.`,
|
|
3194
|
+
` staged = [...staged, ...(await Promise.all(files.map(toAttachment)))];`,
|
|
3195
|
+
` showStaged();`,
|
|
3196
|
+
` });`,
|
|
3197
|
+
` attachmentsEl.addEventListener('kai-remove', (event: Event) => {`,
|
|
3198
|
+
` const { id } = (event as CustomEvent<{ id: string }>).detail;`,
|
|
3199
|
+
` staged = staged.filter((a) => a.id !== id);`,
|
|
3200
|
+
` showStaged();`,
|
|
3201
|
+
` });`,
|
|
3202
|
+
``
|
|
3203
|
+
] : [];
|
|
2829
3204
|
const seedLines = hasEmbedded ? [...sampleSeedComment(ctx.isMock, ` `, (literal) => [`chat.messages = [${literal}];`]), ``] : [];
|
|
2830
3205
|
const sourcesSetupLines = hasSources ? [
|
|
2831
3206
|
` const sourcesEl = document.getElementById('sources') as KaiSourcesElement;`,
|
|
@@ -2851,7 +3226,11 @@ function htmlModule(ctx, components) {
|
|
|
2851
3226
|
` chat.cardSchemas = cards.validationSchemas;`,
|
|
2852
3227
|
``
|
|
2853
3228
|
] : [];
|
|
2854
|
-
const elementTypes =
|
|
3229
|
+
const elementTypes = [
|
|
3230
|
+
"KaiChatElement",
|
|
3231
|
+
...hasSources ? ["KaiSourcesElement"] : [],
|
|
3232
|
+
...attachments ? ["KaiAttachmentsElement"] : []
|
|
3233
|
+
].join(", ");
|
|
2855
3234
|
const annotatesChatMessage = !ctx.emitToolLoop;
|
|
2856
3235
|
const head = [
|
|
2857
3236
|
`// src/main.ts — the page's logic, in a module YOUR build type-checks.`,
|
|
@@ -2870,12 +3249,14 @@ function htmlModule(ctx, components) {
|
|
|
2870
3249
|
toolLoop: ctx.emitToolLoop,
|
|
2871
3250
|
cards: ctx.emitCards,
|
|
2872
3251
|
cardTools: ctx.cardProvider !== null,
|
|
2873
|
-
mock: ctx.isMock
|
|
3252
|
+
mock: ctx.isMock,
|
|
3253
|
+
attachments
|
|
2874
3254
|
}),
|
|
2875
3255
|
`import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
|
|
2876
3256
|
``,
|
|
2877
3257
|
...ctx.isMock ? [...mockResponderInit(), ``] : [],
|
|
2878
3258
|
...modelLines,
|
|
3259
|
+
...attachmentModuleLines,
|
|
2879
3260
|
...cardsLines,
|
|
2880
3261
|
...toolsLines,
|
|
2881
3262
|
...runnerLines,
|
|
@@ -2891,11 +3272,12 @@ function htmlModule(ctx, components) {
|
|
|
2891
3272
|
``,
|
|
2892
3273
|
...cardPropLines,
|
|
2893
3274
|
...seedLines,
|
|
2894
|
-
...sourcesSetupLines
|
|
3275
|
+
...sourcesSetupLines,
|
|
3276
|
+
...attachmentSetupLines
|
|
2895
3277
|
];
|
|
2896
3278
|
const listenerOpen = [
|
|
2897
3279
|
` chat.addEventListener('kai-submit', async (event: Event) => {`,
|
|
2898
|
-
` const e = event as CustomEvent<{ value: string }>;`
|
|
3280
|
+
attachments ? ` const e = event as CustomEvent<{ value: string; attachments: AttachmentData[] }>;` : ` const e = event as CustomEvent<{ value: string }>;`
|
|
2899
3281
|
];
|
|
2900
3282
|
const footer = [
|
|
2901
3283
|
` });`,
|
|
@@ -2923,7 +3305,15 @@ function htmlModule(ctx, components) {
|
|
|
2923
3305
|
"(fn) => { chat.messages = fn(chat.messages); }",
|
|
2924
3306
|
"chat.messages ?? []"
|
|
2925
3307
|
),
|
|
2926
|
-
mock: ctx.isMock
|
|
3308
|
+
mock: ctx.isMock,
|
|
3309
|
+
...attachments ? {
|
|
3310
|
+
filesExpr: "files",
|
|
3311
|
+
afterValue: attachmentTurnLines({
|
|
3312
|
+
stagedExpr: "staged",
|
|
3313
|
+
clear: ["staged = [];", "showStaged();"],
|
|
3314
|
+
fromSubmitEvent: true
|
|
3315
|
+
})
|
|
3316
|
+
} : {}
|
|
2927
3317
|
}),
|
|
2928
3318
|
...footer
|
|
2929
3319
|
].join("\n");
|
|
@@ -2966,8 +3356,9 @@ function renderJsx(components, ctx, framework) {
|
|
|
2966
3356
|
const importTags = workspace ? [.../* @__PURE__ */ new Set([...renderableTags.filter((t) => t !== "kai-resizable"), "kai-resizable", "kai-resizable-item"])] : renderableTags;
|
|
2967
3357
|
const wrapperNames = importTags.map(toPascalCase);
|
|
2968
3358
|
const importList = wrapperNames.join(", ");
|
|
3359
|
+
const attachments = hasAttachments(components);
|
|
2969
3360
|
const standaloneCompanionTags = components.filter(
|
|
2970
|
-
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t)
|
|
3361
|
+
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t) && !(attachments && ATTACHMENT_TAGS.has(t))
|
|
2971
3362
|
);
|
|
2972
3363
|
const companionJsxLines = [];
|
|
2973
3364
|
if (hasEmbedded) {
|
|
@@ -2988,6 +3379,41 @@ function renderJsx(components, ctx, framework) {
|
|
|
2988
3379
|
}
|
|
2989
3380
|
}
|
|
2990
3381
|
const companions = companionJsxLines.join("\n");
|
|
3382
|
+
const attachmentJsx = attachments ? [
|
|
3383
|
+
` {/* Drop files here to stage them for the NEXT message. */}`,
|
|
3384
|
+
` <div style={{ flex: '0 0 auto', display: 'flex', flexDirection: 'column', gap: '0.5rem', padding: '0.75rem' }}>`,
|
|
3385
|
+
` <FileUpload accept="image/*,application/pdf" onFilesAdded={onFilesAdded} />`,
|
|
3386
|
+
` {/* items is an ARRAY, so the wrapper sets it as a DOM property, never an attribute. */}`,
|
|
3387
|
+
` <Attachments items={staged} variant="inline" removable onRemove={onRemoveAttachment} />`,
|
|
3388
|
+
` </div>`
|
|
3389
|
+
].join("\n") : "";
|
|
3390
|
+
const attachmentModuleInit = attachments ? [
|
|
3391
|
+
...ATTACHMENT_WIRE_NOTE,
|
|
3392
|
+
`//`,
|
|
3393
|
+
`// AttachmentData is the kit's own type for a staged file — the same one`,
|
|
3394
|
+
`// <Attachments>.items holds and the same one a 'file' message part holds.`,
|
|
3395
|
+
...fileToAttachmentLines("", "AttachmentData")
|
|
3396
|
+
] : [];
|
|
3397
|
+
const attachmentStateInit = attachments ? [
|
|
3398
|
+
` const [staged, setStaged] = useState<AttachmentData[]>([]);`,
|
|
3399
|
+
` const onFilesAdded = async (e: CustomEvent<{ files: File[] }>) => {`,
|
|
3400
|
+
` // Read every file BEFORE appending: a per-file append would order the`,
|
|
3401
|
+
` // list by whichever finished reading first.`,
|
|
3402
|
+
` const added = await Promise.all(e.detail.files.map(toAttachment));`,
|
|
3403
|
+
` setStaged((prev) => [...prev, ...added]);`,
|
|
3404
|
+
` };`,
|
|
3405
|
+
` const onRemoveAttachment = (e: CustomEvent<{ id: string }>) =>`,
|
|
3406
|
+
` setStaged((prev) => prev.filter((a) => a.id !== e.detail.id));`
|
|
3407
|
+
].join("\n") : "";
|
|
3408
|
+
const attachmentSubmitOpts = attachments ? {
|
|
3409
|
+
filesExpr: "files",
|
|
3410
|
+
afterValue: attachmentTurnLines({
|
|
3411
|
+
stagedExpr: "staged",
|
|
3412
|
+
clear: ["setStaged([]);"],
|
|
3413
|
+
fromSubmitEvent: true
|
|
3414
|
+
})
|
|
3415
|
+
} : {};
|
|
3416
|
+
const submitParamType = attachments ? "CustomEvent<{ value: string; attachments: AttachmentData[] }>" : "CustomEvent<{ value: string }>";
|
|
2991
3417
|
const mockInit = isMock ? mockResponderInit() : [];
|
|
2992
3418
|
const sampleMessagesInit = [
|
|
2993
3419
|
...hasEmbedded ? sampleSeedComment(isMock, " ", (literal) => [
|
|
@@ -3022,7 +3448,8 @@ function renderJsx(components, ctx, framework) {
|
|
|
3022
3448
|
toolLoop: emitToolLoop,
|
|
3023
3449
|
cards: ctx.emitCards,
|
|
3024
3450
|
thread: REACT_THREAD,
|
|
3025
|
-
mock: isMock
|
|
3451
|
+
mock: isMock,
|
|
3452
|
+
...attachmentSubmitOpts
|
|
3026
3453
|
});
|
|
3027
3454
|
const useClientDirective = framework === "next" ? [`'use client';`, ``] : [];
|
|
3028
3455
|
if (framework === "next") {
|
|
@@ -3043,7 +3470,8 @@ function renderJsx(components, ctx, framework) {
|
|
|
3043
3470
|
setMessagesType: emitToolLoop,
|
|
3044
3471
|
cards: ctx.emitCards,
|
|
3045
3472
|
cardTools: ctx.cardProvider !== null,
|
|
3046
|
-
mock: isMock
|
|
3473
|
+
mock: isMock,
|
|
3474
|
+
attachments
|
|
3047
3475
|
}),
|
|
3048
3476
|
`import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
|
|
3049
3477
|
`// <kai-*> are client-only custom elements (the server has no customElements`,
|
|
@@ -3056,22 +3484,25 @@ function renderJsx(components, ctx, framework) {
|
|
|
3056
3484
|
...(p.altNote ?? []).map((l) => `// ${l}`),
|
|
3057
3485
|
...mockInit,
|
|
3058
3486
|
``,
|
|
3487
|
+
...attachmentModuleInit,
|
|
3059
3488
|
...cardsInit,
|
|
3060
3489
|
`export default function App() {`,
|
|
3061
3490
|
sampleMessagesInit,
|
|
3062
3491
|
` const [loading, setLoading] = useState(false);`,
|
|
3063
3492
|
` const suggestions = ${jsArray(suggestions)};`,
|
|
3493
|
+
...attachmentStateInit ? [attachmentStateInit] : [],
|
|
3064
3494
|
...sampleSourcesInit ? [sampleSourcesInit] : [],
|
|
3065
3495
|
...modelInit ? [modelInit] : [],
|
|
3066
3496
|
...toolsInit ? [toolsInit] : [],
|
|
3067
3497
|
...toolRunner ? [toolRunner] : [],
|
|
3068
3498
|
``,
|
|
3069
|
-
` async function onSubmit(e:
|
|
3499
|
+
` async function onSubmit(e: ${submitParamType}) {`,
|
|
3070
3500
|
onSubmitBody,
|
|
3071
3501
|
` }`,
|
|
3072
3502
|
``,
|
|
3073
3503
|
` return (`,
|
|
3074
3504
|
` <div style={{ ${jsxStyle(p.style)} }}>`,
|
|
3505
|
+
...attachmentJsx ? [attachmentJsx] : [],
|
|
3075
3506
|
...workspace ? [
|
|
3076
3507
|
` {/* SCAF-14: workspace split — chat pane left, artifact preview right. */}`,
|
|
3077
3508
|
` {/* Resizable needs ResizableItem children to render panels. */}`,
|
|
@@ -3131,7 +3562,8 @@ function renderJsx(components, ctx, framework) {
|
|
|
3131
3562
|
setMessagesType: emitToolLoop,
|
|
3132
3563
|
cards: ctx.emitCards,
|
|
3133
3564
|
cardTools: ctx.cardProvider !== null,
|
|
3134
|
-
mock: isMock
|
|
3565
|
+
mock: isMock,
|
|
3566
|
+
attachments
|
|
3135
3567
|
}),
|
|
3136
3568
|
`import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
|
|
3137
3569
|
``,
|
|
@@ -3140,22 +3572,25 @@ function renderJsx(components, ctx, framework) {
|
|
|
3140
3572
|
...(p.altNote ?? []).map((l) => `// ${l}`),
|
|
3141
3573
|
...mockInit,
|
|
3142
3574
|
``,
|
|
3575
|
+
...attachmentModuleInit,
|
|
3143
3576
|
...cardsInit,
|
|
3144
3577
|
`export default function App() {`,
|
|
3145
3578
|
sampleMessagesInit,
|
|
3146
3579
|
` const [loading, setLoading] = useState(false);`,
|
|
3147
3580
|
` const suggestions = ${jsArray(suggestions)};`,
|
|
3581
|
+
...attachmentStateInit ? [attachmentStateInit] : [],
|
|
3148
3582
|
...sampleSourcesInit ? [sampleSourcesInit] : [],
|
|
3149
3583
|
...modelInit ? [modelInit] : [],
|
|
3150
3584
|
...toolsInit ? [toolsInit] : [],
|
|
3151
3585
|
...toolRunner ? [toolRunner] : [],
|
|
3152
3586
|
``,
|
|
3153
|
-
` async function onSubmit(e:
|
|
3587
|
+
` async function onSubmit(e: ${submitParamType}) {`,
|
|
3154
3588
|
onSubmitBody,
|
|
3155
3589
|
` }`,
|
|
3156
3590
|
``,
|
|
3157
3591
|
` return (`,
|
|
3158
3592
|
` <div style={{ ${jsxStyle(p.style)} }}>`,
|
|
3593
|
+
...attachmentJsx ? [attachmentJsx] : [],
|
|
3159
3594
|
...workspace ? [
|
|
3160
3595
|
` {/* SCAF-14: workspace split — chat pane left, artifact preview right. */}`,
|
|
3161
3596
|
` {/* Resizable needs ResizableItem children to render panels. */}`,
|
|
@@ -3200,8 +3635,9 @@ function renderJsx(components, ctx, framework) {
|
|
|
3200
3635
|
function renderVue(components, ctx) {
|
|
3201
3636
|
const { p, emptyHint, suggestions, isMock, defaultModel, emitTools, emitToolLoop } = ctx;
|
|
3202
3637
|
const workspace = isWorkspace(components);
|
|
3638
|
+
const attachments = hasAttachments(components);
|
|
3203
3639
|
const standaloneCompanionTags = components.filter(
|
|
3204
|
-
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t)
|
|
3640
|
+
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t) && !(attachments && ATTACHMENT_TAGS.has(t))
|
|
3205
3641
|
);
|
|
3206
3642
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
3207
3643
|
const companionLines = [];
|
|
@@ -3220,6 +3656,42 @@ function renderVue(components, ctx) {
|
|
|
3220
3656
|
}
|
|
3221
3657
|
}
|
|
3222
3658
|
const companions = companionLines.join("\n");
|
|
3659
|
+
const attachmentTemplate = attachments ? [
|
|
3660
|
+
` <!-- Drop files here to stage them for the NEXT message. -->`,
|
|
3661
|
+
` <div style="flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem;">`,
|
|
3662
|
+
` <kai-file-upload accept="image/*,application/pdf" @kai-files-added="onFilesAdded" />`,
|
|
3663
|
+
` <kai-attachments :items.prop="staged" variant="inline" removable @kai-remove="onRemoveAttachment" />`,
|
|
3664
|
+
` </div>`
|
|
3665
|
+
].join("\n") : "";
|
|
3666
|
+
const attachmentScript = attachments ? [
|
|
3667
|
+
...ATTACHMENT_WIRE_NOTE,
|
|
3668
|
+
`//`,
|
|
3669
|
+
`// AttachmentData is the kit's own type for a staged file, and it is what`,
|
|
3670
|
+
`// <kai-attachments>.items and a 'file' MessagePart both hold.`,
|
|
3671
|
+
`const staged = ref<AttachmentData[]>([]);`,
|
|
3672
|
+
``,
|
|
3673
|
+
...fileToAttachmentLines("", "AttachmentData"),
|
|
3674
|
+
``,
|
|
3675
|
+
`async function onFilesAdded(event: Event) {`,
|
|
3676
|
+
` const { files } = (event as CustomEvent<{ files: File[] }>).detail;`,
|
|
3677
|
+
` // Read every file BEFORE appending: a per-file append would order the`,
|
|
3678
|
+
` // list by whichever finished reading first.`,
|
|
3679
|
+
` const added = await Promise.all(files.map(toAttachment));`,
|
|
3680
|
+
` staged.value = [...staged.value, ...added];`,
|
|
3681
|
+
`}`,
|
|
3682
|
+
`function onRemoveAttachment(event: Event) {`,
|
|
3683
|
+
` const { id } = (event as CustomEvent<{ id: string }>).detail;`,
|
|
3684
|
+
` staged.value = staged.value.filter((a) => a.id !== id);`,
|
|
3685
|
+
`}`
|
|
3686
|
+
] : [];
|
|
3687
|
+
const attachmentSubmitOpts = attachments ? {
|
|
3688
|
+
filesExpr: "files",
|
|
3689
|
+
afterValue: attachmentTurnLines({
|
|
3690
|
+
stagedExpr: "staged.value",
|
|
3691
|
+
clear: ["staged.value = [];"],
|
|
3692
|
+
fromSubmitEvent: true
|
|
3693
|
+
})
|
|
3694
|
+
} : {};
|
|
3223
3695
|
const onSubmitBody = realStreamBody({
|
|
3224
3696
|
pad: " ",
|
|
3225
3697
|
read: "messages.value",
|
|
@@ -3231,7 +3703,8 @@ function renderVue(components, ctx) {
|
|
|
3231
3703
|
toolLoop: emitToolLoop,
|
|
3232
3704
|
cards: ctx.emitCards,
|
|
3233
3705
|
thread: liveThreadBinding("messages.value", "(fn) => { messages.value = fn(messages.value); }"),
|
|
3234
|
-
mock: isMock
|
|
3706
|
+
mock: isMock,
|
|
3707
|
+
...attachmentSubmitOpts
|
|
3235
3708
|
});
|
|
3236
3709
|
const mockInit = isMock ? mockResponderInit() : [];
|
|
3237
3710
|
const modelInit = defaultModel ? [
|
|
@@ -3306,7 +3779,8 @@ function renderVue(components, ctx) {
|
|
|
3306
3779
|
toolLoop: emitToolLoop,
|
|
3307
3780
|
cards: ctx.emitCards,
|
|
3308
3781
|
cardTools: ctx.cardProvider !== null,
|
|
3309
|
-
mock: isMock
|
|
3782
|
+
mock: isMock,
|
|
3783
|
+
attachments
|
|
3310
3784
|
}),
|
|
3311
3785
|
`import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
|
|
3312
3786
|
vueImports,
|
|
@@ -3321,6 +3795,7 @@ function renderVue(components, ctx) {
|
|
|
3321
3795
|
...toolsLines,
|
|
3322
3796
|
...runnerLines,
|
|
3323
3797
|
...sourcesSeed,
|
|
3798
|
+
...attachmentScript,
|
|
3324
3799
|
``,
|
|
3325
3800
|
`// SCAF-15: kai-* register via an async dynamic import (SSR-safety). The .prop`,
|
|
3326
3801
|
`// bindings can apply before the element upgrades, which drops them — re-apply once`,
|
|
@@ -3332,13 +3807,14 @@ function renderVue(components, ctx) {
|
|
|
3332
3807
|
` if (el) Object.assign(el, { messages: messages.value, loading: loading.value, suggestions${cardPropAssign.length ? `, ${cardPropAssign.join(", ")}` : ""} });`,
|
|
3333
3808
|
`});`,
|
|
3334
3809
|
``,
|
|
3335
|
-
`async function onSubmit(e: CustomEvent<{ value: string }>) {`,
|
|
3810
|
+
`async function onSubmit(e: ${attachments ? "CustomEvent<{ value: string; attachments: AttachmentData[] }>" : "CustomEvent<{ value: string }>"}) {`,
|
|
3336
3811
|
onSubmitBody,
|
|
3337
3812
|
`}`,
|
|
3338
3813
|
`<\/script>`,
|
|
3339
3814
|
``,
|
|
3340
3815
|
`<template>`,
|
|
3341
3816
|
` <div style="${p.style}">`,
|
|
3817
|
+
...attachmentTemplate ? [attachmentTemplate] : [],
|
|
3342
3818
|
...workspaceTemplate,
|
|
3343
3819
|
` </div>`,
|
|
3344
3820
|
`</template>`
|
|
@@ -3347,8 +3823,9 @@ function renderVue(components, ctx) {
|
|
|
3347
3823
|
function renderSvelte(components, ctx) {
|
|
3348
3824
|
const { p, emptyHint, suggestions, isMock, defaultModel, emitTools, emitToolLoop } = ctx;
|
|
3349
3825
|
const workspace = isWorkspace(components);
|
|
3826
|
+
const attachments = hasAttachments(components);
|
|
3350
3827
|
const standaloneCompanionTags = components.filter(
|
|
3351
|
-
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t)
|
|
3828
|
+
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t) && !(attachments && ATTACHMENT_TAGS.has(t))
|
|
3352
3829
|
);
|
|
3353
3830
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
3354
3831
|
const hasSourcesCompanion = standaloneCompanionTags.includes("kai-sources");
|
|
@@ -3368,6 +3845,14 @@ function renderSvelte(components, ctx) {
|
|
|
3368
3845
|
}
|
|
3369
3846
|
}
|
|
3370
3847
|
const companionLines = companionLinesList.join("\n");
|
|
3848
|
+
const attachmentSubmitOpts = attachments ? {
|
|
3849
|
+
filesExpr: "files",
|
|
3850
|
+
afterValue: attachmentTurnLines({
|
|
3851
|
+
stagedExpr: "staged",
|
|
3852
|
+
clear: ["staged = [];"],
|
|
3853
|
+
fromSubmitEvent: true
|
|
3854
|
+
})
|
|
3855
|
+
} : {};
|
|
3371
3856
|
const onSubmitBody = realStreamBody({
|
|
3372
3857
|
pad: " ",
|
|
3373
3858
|
read: "messages",
|
|
@@ -3379,7 +3864,8 @@ function renderSvelte(components, ctx) {
|
|
|
3379
3864
|
toolLoop: emitToolLoop,
|
|
3380
3865
|
cards: ctx.emitCards,
|
|
3381
3866
|
thread: liveThreadBinding("messages", "(fn) => { messages = fn(messages); }"),
|
|
3382
|
-
mock: isMock
|
|
3867
|
+
mock: isMock,
|
|
3868
|
+
...attachmentSubmitOpts
|
|
3383
3869
|
});
|
|
3384
3870
|
const mockInit = isMock ? mockResponderInit(" ") : [];
|
|
3385
3871
|
const modelInit = defaultModel ? [
|
|
@@ -3405,6 +3891,33 @@ function renderSvelte(components, ctx) {
|
|
|
3405
3891
|
` ];`,
|
|
3406
3892
|
` $effect(() => { if (sourcesEl) { sourcesEl.sources = sampleSources; } });`
|
|
3407
3893
|
] : [];
|
|
3894
|
+
const attachmentScript = attachments ? [
|
|
3895
|
+
...ATTACHMENT_WIRE_NOTE.map((l) => ` ${l}`),
|
|
3896
|
+
` //`,
|
|
3897
|
+
` // AttachmentData is the kit's own type for a staged file, and it is what`,
|
|
3898
|
+
` // <kai-attachments>.items and a 'file' MessagePart both hold.`,
|
|
3899
|
+
` let attachmentsEl = $state<KaiAttachmentsElement | undefined>(undefined);`,
|
|
3900
|
+
` let staged = $state.raw<AttachmentData[]>([]);`,
|
|
3901
|
+
` $effect(() => { if (attachmentsEl && defined) { attachmentsEl.items = staged; } });`,
|
|
3902
|
+
``,
|
|
3903
|
+
...fileToAttachmentLines(" ", "AttachmentData"),
|
|
3904
|
+
` async function onFilesAdded(e: CustomEvent<{ files: File[] }>) {`,
|
|
3905
|
+
` // Read every file BEFORE appending: a per-file append would order the`,
|
|
3906
|
+
` // list by whichever finished reading first.`,
|
|
3907
|
+
` const added = await Promise.all(e.detail.files.map(toAttachment));`,
|
|
3908
|
+
` staged = [...staged, ...added];`,
|
|
3909
|
+
` }`,
|
|
3910
|
+
` function onRemoveAttachment(e: CustomEvent<{ id: string }>) {`,
|
|
3911
|
+
` staged = staged.filter((a) => a.id !== e.detail.id);`,
|
|
3912
|
+
` }`
|
|
3913
|
+
] : [];
|
|
3914
|
+
const attachmentMarkup = attachments ? [
|
|
3915
|
+
` <!-- Drop files here to stage them for the NEXT message. -->`,
|
|
3916
|
+
` <div style="flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem;">`,
|
|
3917
|
+
` <kai-file-upload accept="image/*,application/pdf" onkai-files-added={onFilesAdded}></kai-file-upload>`,
|
|
3918
|
+
` <kai-attachments bind:this={attachmentsEl} variant="inline" removable onkai-remove={onRemoveAttachment}></kai-attachments>`,
|
|
3919
|
+
` </div>`
|
|
3920
|
+
] : [];
|
|
3408
3921
|
const workspaceMarkup = workspace ? [
|
|
3409
3922
|
` <!-- SCAF-14: workspace split — chat pane left, artifact preview right. -->`,
|
|
3410
3923
|
` <!-- kai-resizable needs kai-resizable-item children to render panels. -->`,
|
|
@@ -3435,14 +3948,19 @@ function renderSvelte(components, ctx) {
|
|
|
3435
3948
|
// KaiSourcesElement is only imported when a kai-sources companion is actually
|
|
3436
3949
|
// declared below: an always-on import would be unused (and fail noUnusedLocals)
|
|
3437
3950
|
// on every archetype without kai-sources.
|
|
3438
|
-
` import type { ${
|
|
3951
|
+
` import type { ${[
|
|
3952
|
+
"KaiChatElement",
|
|
3953
|
+
...hasSourcesCompanion ? ["KaiSourcesElement"] : [],
|
|
3954
|
+
...attachments ? ["KaiAttachmentsElement"] : []
|
|
3955
|
+
].join(", ")} } from '@kitn.ai/ui/elements';`,
|
|
3439
3956
|
...wireImportLines({
|
|
3440
3957
|
pad: " ",
|
|
3441
3958
|
typed: true,
|
|
3442
3959
|
toolLoop: emitToolLoop,
|
|
3443
3960
|
cards: ctx.emitCards,
|
|
3444
3961
|
cardTools: ctx.cardProvider !== null,
|
|
3445
|
-
mock: isMock
|
|
3962
|
+
mock: isMock,
|
|
3963
|
+
attachments
|
|
3446
3964
|
}),
|
|
3447
3965
|
` import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
|
|
3448
3966
|
` import { onMount } from 'svelte';`,
|
|
@@ -3468,13 +3986,15 @@ function renderSvelte(components, ctx) {
|
|
|
3468
3986
|
` if (chatEl && defined) { chatEl.messages = messages; chatEl.loading = loading; chatEl.suggestions = suggestions;${cardPropEffect} }`,
|
|
3469
3987
|
` });`,
|
|
3470
3988
|
...sourcesReactive,
|
|
3989
|
+
...attachmentScript,
|
|
3471
3990
|
``,
|
|
3472
|
-
` async function onSubmit(e: CustomEvent<{ value: string }>) {`,
|
|
3991
|
+
` async function onSubmit(e: ${attachments ? "CustomEvent<{ value: string; attachments: AttachmentData[] }>" : "CustomEvent<{ value: string }>"}) {`,
|
|
3473
3992
|
onSubmitBody,
|
|
3474
3993
|
` }`,
|
|
3475
3994
|
`<\/script>`,
|
|
3476
3995
|
``,
|
|
3477
3996
|
`<div style="${p.style}">`,
|
|
3997
|
+
...attachmentMarkup,
|
|
3478
3998
|
...workspaceMarkup,
|
|
3479
3999
|
`</div>`
|
|
3480
4000
|
].filter((l, i, arr) => !(l === "" && arr[i - 1] === "" && i === arr.length - 1)).join("\n");
|
|
@@ -3487,8 +4007,9 @@ function renderTanstackStart(components, ctx) {
|
|
|
3487
4007
|
const importTags = workspace ? [.../* @__PURE__ */ new Set([...renderableTags.filter((t) => t !== "kai-resizable"), "kai-resizable", "kai-resizable-item"])] : renderableTags;
|
|
3488
4008
|
const wrapperNames = importTags.map(toPascalCase);
|
|
3489
4009
|
const importList = wrapperNames.join(", ");
|
|
4010
|
+
const attachments = hasAttachments(components);
|
|
3490
4011
|
const standaloneCompanionTags = components.filter(
|
|
3491
|
-
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t)
|
|
4012
|
+
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t) && !(attachments && ATTACHMENT_TAGS.has(t))
|
|
3492
4013
|
);
|
|
3493
4014
|
const companionJsxLines = [];
|
|
3494
4015
|
if (hasEmbedded) {
|
|
@@ -3509,6 +4030,41 @@ function renderTanstackStart(components, ctx) {
|
|
|
3509
4030
|
}
|
|
3510
4031
|
}
|
|
3511
4032
|
const companions = companionJsxLines.join("\n");
|
|
4033
|
+
const attachmentJsx = attachments ? [
|
|
4034
|
+
` {/* Drop files here to stage them for the NEXT message. */}`,
|
|
4035
|
+
` <div style={{ flex: '0 0 auto', display: 'flex', flexDirection: 'column', gap: '0.5rem', padding: '0.75rem' }}>`,
|
|
4036
|
+
` <FileUpload accept="image/*,application/pdf" onFilesAdded={onFilesAdded} />`,
|
|
4037
|
+
` {/* items is an ARRAY, so the wrapper sets it as a DOM property, never an attribute. */}`,
|
|
4038
|
+
` <Attachments items={staged} variant="inline" removable onRemove={onRemoveAttachment} />`,
|
|
4039
|
+
` </div>`
|
|
4040
|
+
].join("\n") : "";
|
|
4041
|
+
const attachmentModuleInit = attachments ? [
|
|
4042
|
+
...ATTACHMENT_WIRE_NOTE,
|
|
4043
|
+
`//`,
|
|
4044
|
+
`// AttachmentData is the kit's own type for a staged file — the same one`,
|
|
4045
|
+
`// <Attachments>.items holds and the same one a 'file' message part holds.`,
|
|
4046
|
+
...fileToAttachmentLines("", "AttachmentData")
|
|
4047
|
+
] : [];
|
|
4048
|
+
const attachmentStateInit = attachments ? [
|
|
4049
|
+
` const [staged, setStaged] = useState<AttachmentData[]>([]);`,
|
|
4050
|
+
` const onFilesAdded = async (e: CustomEvent<{ files: File[] }>) => {`,
|
|
4051
|
+
` // Read every file BEFORE appending: a per-file append would order the`,
|
|
4052
|
+
` // list by whichever finished reading first.`,
|
|
4053
|
+
` const added = await Promise.all(e.detail.files.map(toAttachment));`,
|
|
4054
|
+
` setStaged((prev) => [...prev, ...added]);`,
|
|
4055
|
+
` };`,
|
|
4056
|
+
` const onRemoveAttachment = (e: CustomEvent<{ id: string }>) =>`,
|
|
4057
|
+
` setStaged((prev) => prev.filter((a) => a.id !== e.detail.id));`
|
|
4058
|
+
].join("\n") : "";
|
|
4059
|
+
const attachmentSubmitOpts = attachments ? {
|
|
4060
|
+
filesExpr: "files",
|
|
4061
|
+
afterValue: attachmentTurnLines({
|
|
4062
|
+
stagedExpr: "staged",
|
|
4063
|
+
clear: ["setStaged([]);"],
|
|
4064
|
+
fromSubmitEvent: true
|
|
4065
|
+
})
|
|
4066
|
+
} : {};
|
|
4067
|
+
const submitParamType = attachments ? "CustomEvent<{ value: string; attachments: AttachmentData[] }>" : "CustomEvent<{ value: string }>";
|
|
3512
4068
|
const mockInit = isMock ? mockResponderInit() : [];
|
|
3513
4069
|
const sampleMessagesInit = [
|
|
3514
4070
|
...hasEmbedded ? sampleSeedComment(isMock, " ", (literal) => [
|
|
@@ -3543,7 +4099,8 @@ function renderTanstackStart(components, ctx) {
|
|
|
3543
4099
|
toolLoop: emitToolLoop,
|
|
3544
4100
|
cards: ctx.emitCards,
|
|
3545
4101
|
thread: REACT_THREAD,
|
|
3546
|
-
mock: isMock
|
|
4102
|
+
mock: isMock,
|
|
4103
|
+
...attachmentSubmitOpts
|
|
3547
4104
|
});
|
|
3548
4105
|
const filePathNote = [
|
|
3549
4106
|
`// TanStack Start route file — save as: src/routes/chat.tsx`,
|
|
@@ -3573,7 +4130,8 @@ function renderTanstackStart(components, ctx) {
|
|
|
3573
4130
|
setMessagesType: emitToolLoop,
|
|
3574
4131
|
cards: ctx.emitCards,
|
|
3575
4132
|
cardTools: ctx.cardProvider !== null,
|
|
3576
|
-
mock: isMock
|
|
4133
|
+
mock: isMock,
|
|
4134
|
+
attachments
|
|
3577
4135
|
}),
|
|
3578
4136
|
`import '@kitn.ai/ui/theme.tokens.css' // compiled token defaults`,
|
|
3579
4137
|
``,
|
|
@@ -3581,6 +4139,7 @@ function renderTanstackStart(components, ctx) {
|
|
|
3581
4139
|
...(p.altNote ?? []).map((l) => `// ${l}`),
|
|
3582
4140
|
...mockInit,
|
|
3583
4141
|
``,
|
|
4142
|
+
...attachmentModuleInit,
|
|
3584
4143
|
...cardsInit,
|
|
3585
4144
|
`// ssr: false keeps the Solid-based web component client-only.`,
|
|
3586
4145
|
`// Server HTML for /chat omits <kai-chat> → no hydration mismatch.`,
|
|
@@ -3593,17 +4152,19 @@ function renderTanstackStart(components, ctx) {
|
|
|
3593
4152
|
sampleMessagesInit,
|
|
3594
4153
|
` const [loading, setLoading] = useState(false);`,
|
|
3595
4154
|
` const suggestions = ${jsArray(suggestions)};`,
|
|
4155
|
+
...attachmentStateInit ? [attachmentStateInit] : [],
|
|
3596
4156
|
...sampleSourcesInit ? [sampleSourcesInit] : [],
|
|
3597
4157
|
...modelInit ? [modelInit] : [],
|
|
3598
4158
|
...toolsInit ? [toolsInit] : [],
|
|
3599
4159
|
...toolRunner ? [toolRunner] : [],
|
|
3600
4160
|
``,
|
|
3601
|
-
` async function onSubmit(e:
|
|
4161
|
+
` async function onSubmit(e: ${submitParamType}) {`,
|
|
3602
4162
|
onSubmitBody,
|
|
3603
4163
|
` }`,
|
|
3604
4164
|
``,
|
|
3605
4165
|
` return (`,
|
|
3606
4166
|
` <main style={{ ${jsxStyle(p.style)} }}>`,
|
|
4167
|
+
...attachmentJsx ? [attachmentJsx] : [],
|
|
3607
4168
|
...workspace ? [
|
|
3608
4169
|
` {/* SCAF-14: workspace split — chat pane left, artifact preview right. */}`,
|
|
3609
4170
|
` {/* Resizable needs ResizableItem children to render panels. */}`,
|
|
@@ -3648,8 +4209,9 @@ function renderTanstackStart(components, ctx) {
|
|
|
3648
4209
|
function renderAngular(components, ctx) {
|
|
3649
4210
|
const { p, emptyHint, suggestions, isMock, defaultModel, emitTools, emitToolLoop } = ctx;
|
|
3650
4211
|
const workspace = isWorkspace(components);
|
|
4212
|
+
const attachments = hasAttachments(components);
|
|
3651
4213
|
const standaloneCompanionTags = components.filter(
|
|
3652
|
-
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t)
|
|
4214
|
+
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t) && !(attachments && ATTACHMENT_TAGS.has(t))
|
|
3653
4215
|
);
|
|
3654
4216
|
const hasEmbedded = components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
|
|
3655
4217
|
const hasSourcesCompanion = standaloneCompanionTags.includes("kai-sources");
|
|
@@ -3670,6 +4232,46 @@ function renderAngular(components, ctx) {
|
|
|
3670
4232
|
companionLines.push(` <${t}></${t}>`);
|
|
3671
4233
|
}
|
|
3672
4234
|
}
|
|
4235
|
+
const attachmentTemplate = attachments ? [
|
|
4236
|
+
` <!-- Drop files here to stage them for the NEXT message. -->`,
|
|
4237
|
+
` <div style="flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem;">`,
|
|
4238
|
+
` <kai-file-upload accept="image/*,application/pdf" (kai-files-added)="onFilesAdded($event)"></kai-file-upload>`,
|
|
4239
|
+
` <kai-attachments [items]="staged()" variant="inline" removable (kai-remove)="onRemoveAttachment($event)"></kai-attachments>`,
|
|
4240
|
+
` </div>`
|
|
4241
|
+
] : [];
|
|
4242
|
+
const attachmentModuleInit = attachments ? [
|
|
4243
|
+
...ATTACHMENT_WIRE_NOTE,
|
|
4244
|
+
`//`,
|
|
4245
|
+
`// AttachmentData is the kit's own type for a staged file, and it is what`,
|
|
4246
|
+
`// <kai-attachments>.items and a 'file' MessagePart both hold.`,
|
|
4247
|
+
...fileToAttachmentLines("", "AttachmentData"),
|
|
4248
|
+
``
|
|
4249
|
+
] : [];
|
|
4250
|
+
const attachmentFields = attachments ? [
|
|
4251
|
+
` readonly staged = signal<AttachmentData[]>([]);`,
|
|
4252
|
+
``,
|
|
4253
|
+
` // Same Event-not-CustomEvent rule as onSubmit below: strictTemplates types`,
|
|
4254
|
+
` // $event on an unknown custom-element event as a plain Event.`,
|
|
4255
|
+
` async onFilesAdded(event: Event) {`,
|
|
4256
|
+
` const { files } = (event as CustomEvent<{ files: File[] }>).detail;`,
|
|
4257
|
+
` // Read every file BEFORE appending: a per-file append would order the`,
|
|
4258
|
+
` // list by whichever finished reading first.`,
|
|
4259
|
+
` const added = await Promise.all(files.map(toAttachment));`,
|
|
4260
|
+
` this.staged.set([...this.staged(), ...added]);`,
|
|
4261
|
+
` }`,
|
|
4262
|
+
` onRemoveAttachment(event: Event) {`,
|
|
4263
|
+
` const { id } = (event as CustomEvent<{ id: string }>).detail;`,
|
|
4264
|
+
` this.staged.set(this.staged().filter((a) => a.id !== id));`,
|
|
4265
|
+
` }`
|
|
4266
|
+
] : [];
|
|
4267
|
+
const attachmentSubmitOpts = attachments ? {
|
|
4268
|
+
filesExpr: "files",
|
|
4269
|
+
afterValue: attachmentTurnLines({
|
|
4270
|
+
stagedExpr: "this.staged()",
|
|
4271
|
+
clear: ["this.staged.set([]);"],
|
|
4272
|
+
fromSubmitEvent: true
|
|
4273
|
+
})
|
|
4274
|
+
} : {};
|
|
3673
4275
|
const read = "this.messages()";
|
|
3674
4276
|
const commit = (value) => `this.messages.set(${value});`;
|
|
3675
4277
|
const setter = "(fn) => this.messages.set(fn(this.messages()))";
|
|
@@ -3684,7 +4286,8 @@ function renderAngular(components, ctx) {
|
|
|
3684
4286
|
toolLoop: emitToolLoop,
|
|
3685
4287
|
cards: ctx.emitCards,
|
|
3686
4288
|
thread: accessorThreadBinding(read, commit, setter),
|
|
3687
|
-
mock: isMock
|
|
4289
|
+
mock: isMock,
|
|
4290
|
+
...attachmentSubmitOpts
|
|
3688
4291
|
});
|
|
3689
4292
|
const modelInit = defaultModel ? [
|
|
3690
4293
|
`// SCAF-8: change this model id to another id THIS PROVIDER accepts.`,
|
|
@@ -3760,7 +4363,8 @@ function renderAngular(components, ctx) {
|
|
|
3760
4363
|
toolLoop: emitToolLoop,
|
|
3761
4364
|
cards: ctx.emitCards,
|
|
3762
4365
|
cardTools: ctx.cardProvider !== null,
|
|
3763
|
-
mock: isMock
|
|
4366
|
+
mock: isMock,
|
|
4367
|
+
attachments
|
|
3764
4368
|
}),
|
|
3765
4369
|
``,
|
|
3766
4370
|
`// ${ctx.label} — ${p.note}. empty-state hint: ${emptyHint}`,
|
|
@@ -3768,6 +4372,7 @@ function renderAngular(components, ctx) {
|
|
|
3768
4372
|
...isMock ? mockResponderInit() : [],
|
|
3769
4373
|
``,
|
|
3770
4374
|
...modelInit,
|
|
4375
|
+
...attachmentModuleInit,
|
|
3771
4376
|
...cardsInit,
|
|
3772
4377
|
...toolsLines,
|
|
3773
4378
|
...runnerLines,
|
|
@@ -3779,6 +4384,7 @@ function renderAngular(components, ctx) {
|
|
|
3779
4384
|
` schemas: [CUSTOM_ELEMENTS_SCHEMA],`,
|
|
3780
4385
|
` template: \``,
|
|
3781
4386
|
` <div style="${p.style}">`,
|
|
4387
|
+
...attachmentTemplate,
|
|
3782
4388
|
...templateBody,
|
|
3783
4389
|
` </div>`,
|
|
3784
4390
|
` \`,`,
|
|
@@ -3811,11 +4417,13 @@ function renderAngular(components, ctx) {
|
|
|
3811
4417
|
` });`,
|
|
3812
4418
|
` }`,
|
|
3813
4419
|
``,
|
|
4420
|
+
...attachmentFields,
|
|
4421
|
+
``,
|
|
3814
4422
|
` // \`Event\`, not \`CustomEvent\`: under strictTemplates Angular types \`$event\` on`,
|
|
3815
4423
|
` // an unknown custom-element event as a plain Event, so the narrowing happens`,
|
|
3816
4424
|
` // here rather than in the signature.`,
|
|
3817
4425
|
` async onSubmit(event: Event) {`,
|
|
3818
|
-
` const e = event as CustomEvent<{ value: string }>;`,
|
|
4426
|
+
` const e = event as CustomEvent<{ value: string${attachments ? "; attachments: AttachmentData[]" : ""} }>;`,
|
|
3819
4427
|
onSubmitBody,
|
|
3820
4428
|
` }`,
|
|
3821
4429
|
`}`
|
|
@@ -3824,8 +4432,9 @@ function renderAngular(components, ctx) {
|
|
|
3824
4432
|
function renderSolid(components, ctx) {
|
|
3825
4433
|
const { p, emptyHint, suggestions, isMock, defaultModel, emitTools, emitToolLoop } = ctx;
|
|
3826
4434
|
const workspace = isWorkspace(components);
|
|
4435
|
+
const attachments = hasAttachments(components);
|
|
3827
4436
|
const standaloneCompanionTags = components.filter(
|
|
3828
|
-
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t)
|
|
4437
|
+
(t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t) && !(attachments && ATTACHMENT_TAGS.has(t))
|
|
3829
4438
|
);
|
|
3830
4439
|
const hasSources = standaloneCompanionTags.includes("kai-sources");
|
|
3831
4440
|
const hasVoice = standaloneCompanionTags.includes("kai-voice-input");
|
|
@@ -3856,7 +4465,13 @@ function renderSolid(components, ctx) {
|
|
|
3856
4465
|
"SourceTrigger",
|
|
3857
4466
|
"Tool",
|
|
3858
4467
|
...workspace ? ["Artifact", "ResizableHandle", "ResizablePanel", "ResizablePanelGroup"] : [],
|
|
3859
|
-
...hasVoice ? ["VoiceInput"] : []
|
|
4468
|
+
...hasVoice ? ["VoiceInput"] : [],
|
|
4469
|
+
// `Attachments`/`Attachment`/`AttachmentPreview`/`AttachmentInfo` are already
|
|
4470
|
+
// unconditional above — `renderPart` draws `file` PARTS with them whatever the
|
|
4471
|
+
// surface asked for. These three are the STAGING half, which only this
|
|
4472
|
+
// capability composes: the dropzone, its trigger, and the per-chip remove
|
|
4473
|
+
// button `renderPart` has no use for (a sent file is not removable).
|
|
4474
|
+
...attachments ? ["AttachmentRemove", "FileUpload", "FileUploadTrigger"] : []
|
|
3860
4475
|
].sort();
|
|
3861
4476
|
const read = "messages()";
|
|
3862
4477
|
const commit = (value) => `setMessages(${value});`;
|
|
@@ -3873,7 +4488,17 @@ function renderSolid(components, ctx) {
|
|
|
3873
4488
|
cards: ctx.emitCards,
|
|
3874
4489
|
thread: accessorThreadBinding(read, commit, setter),
|
|
3875
4490
|
valueSource: "input()",
|
|
3876
|
-
|
|
4491
|
+
// The staged files are captured BEFORE either list is cleared. `setInput('')`
|
|
4492
|
+
// stays last so the order reads the way it executes.
|
|
4493
|
+
afterValue: [
|
|
4494
|
+
...attachments ? attachmentTurnLines({
|
|
4495
|
+
stagedExpr: "staged()",
|
|
4496
|
+
clear: ["setStaged([]);"],
|
|
4497
|
+
fromSubmitEvent: false
|
|
4498
|
+
}) : [],
|
|
4499
|
+
`setInput('');`
|
|
4500
|
+
],
|
|
4501
|
+
...attachments ? { filesExpr: "files" } : {},
|
|
3877
4502
|
mock: isMock
|
|
3878
4503
|
});
|
|
3879
4504
|
const modelInit = defaultModel ? [
|
|
@@ -3887,6 +4512,11 @@ function renderSolid(components, ctx) {
|
|
|
3887
4512
|
` types={cards.components}`,
|
|
3888
4513
|
` schemas={cards.validationSchemas}`
|
|
3889
4514
|
] : [];
|
|
4515
|
+
const attachmentInit = attachments ? [
|
|
4516
|
+
...ATTACHMENT_WIRE_NOTE.map((l) => ` ${l}`),
|
|
4517
|
+
` const [staged, setStaged] = createSignal<AttachmentData[]>([]);`,
|
|
4518
|
+
...fileToAttachmentLines(" ", "AttachmentData")
|
|
4519
|
+
] : [];
|
|
3890
4520
|
const sourcesInit = hasSources ? [
|
|
3891
4521
|
` // Replace sampleSources with your real source data.`,
|
|
3892
4522
|
` const sampleSources = [`,
|
|
@@ -3962,6 +4592,35 @@ function renderSolid(components, ctx) {
|
|
|
3962
4592
|
` </For>`,
|
|
3963
4593
|
` </div>`,
|
|
3964
4594
|
` </Show>`,
|
|
4595
|
+
...attachments ? [
|
|
4596
|
+
` {/* Drop files here to stage them for the NEXT message. */}`,
|
|
4597
|
+
` <div class="flex flex-col gap-2 pb-3">`,
|
|
4598
|
+
` {/* Read every file BEFORE appending: a per-file append would order`,
|
|
4599
|
+
` the list by whichever finished reading first. */}`,
|
|
4600
|
+
` <FileUpload`,
|
|
4601
|
+
` accept="image/*,application/pdf"`,
|
|
4602
|
+
` onFilesAdded={async (files) => setStaged([...staged(), ...(await Promise.all(files.map(toAttachment)))])}`,
|
|
4603
|
+
` >`,
|
|
4604
|
+
` <FileUploadTrigger class="border-border text-muted-foreground w-full rounded-xl border border-dashed px-4 py-3 text-center text-sm">`,
|
|
4605
|
+
` Click or drop files to attach`,
|
|
4606
|
+
` </FileUploadTrigger>`,
|
|
4607
|
+
` </FileUpload>`,
|
|
4608
|
+
` {/* The staged chips. onRemove is what makes <AttachmentRemove> draw its button. */}`,
|
|
4609
|
+
` <Show when={staged().length > 0}>`,
|
|
4610
|
+
` <Attachments variant="inline">`,
|
|
4611
|
+
` <For each={staged()}>`,
|
|
4612
|
+
` {(a) => (`,
|
|
4613
|
+
` <Attachment data={a} onRemove={() => setStaged(staged().filter((x) => x.id !== a.id))}>`,
|
|
4614
|
+
` <AttachmentPreview />`,
|
|
4615
|
+
` <AttachmentInfo />`,
|
|
4616
|
+
` <AttachmentRemove />`,
|
|
4617
|
+
` </Attachment>`,
|
|
4618
|
+
` )}`,
|
|
4619
|
+
` </For>`,
|
|
4620
|
+
` </Attachments>`,
|
|
4621
|
+
` </Show>`,
|
|
4622
|
+
` </div>`
|
|
4623
|
+
] : [],
|
|
3965
4624
|
` <PromptInput value={input()} onValueChange={setInput} onSubmit={onSubmit} isLoading={loading()}>`,
|
|
3966
4625
|
` <div class="flex flex-col">`,
|
|
3967
4626
|
` <PromptInputTextarea placeholder="Send a message…" class="min-h-[44px] pt-3 pl-4" />`,
|
|
@@ -4026,11 +4685,25 @@ function renderSolid(components, ctx) {
|
|
|
4026
4685
|
`// every kit utility class as unused, and the whole UI renders unstyled.`,
|
|
4027
4686
|
`// (theme.css here, not theme.tokens.css: this app compiles Tailwind itself.)`,
|
|
4028
4687
|
`import { For, Index, Match, Show, Switch, createMemo, createSignal } from 'solid-js';`,
|
|
4688
|
+
// '@kitn.ai/ui/solid', NOT the root '@kitn.ai/ui', and the difference is
|
|
4689
|
+
// invisible to every compiler on this repo's critical path: src/solid.ts is
|
|
4690
|
+
// `export * from './index'`, so ./solid is a strict SUPERSET of the root and
|
|
4691
|
+
// both specifiers typecheck identically. verify:scaffold's solid project
|
|
4692
|
+
// compiled clean the whole time this was wrong.
|
|
4693
|
+
//
|
|
4694
|
+
// The root is the shared layer EVERY consumer resolves — React, Vue, Svelte
|
|
4695
|
+
// and vanilla included — so it deliberately carries only the chat components,
|
|
4696
|
+
// types and helpers those frameworks can use. The rest of the Solid catalog
|
|
4697
|
+
// lives on ./solid, built as its own bundle so a React app never pays for it.
|
|
4698
|
+
// A Solid app pointed at the root therefore gets the smaller surface and finds
|
|
4699
|
+
// out the hard way, one missing export at a time. The published guide says so
|
|
4700
|
+
// outright ("Import Solid components from @kitn.ai/ui/solid, not from the root
|
|
4701
|
+
// @kitn.ai/ui"), and this generator contradicted it for every Solid scaffold.
|
|
4029
4702
|
`import {`,
|
|
4030
4703
|
...componentImports.map((n) => ` ${n},`),
|
|
4031
|
-
`} from '@kitn.ai/ui';`,
|
|
4704
|
+
`} from '@kitn.ai/ui/solid';`,
|
|
4032
4705
|
`// The kit's own types, from the same entry the components come from.`,
|
|
4033
|
-
`import type { ChatMessage, MessagePart, MessageSource } from '@kitn.ai/ui';`,
|
|
4706
|
+
`import type { ${attachments ? "AttachmentData, ChatMessage, MessagePart, MessageSource" : "ChatMessage, MessagePart, MessageSource"} } from '@kitn.ai/ui/solid';`,
|
|
4034
4707
|
...wireImportLines({
|
|
4035
4708
|
typed: false,
|
|
4036
4709
|
toolLoop: emitToolLoop,
|
|
@@ -4230,6 +4903,7 @@ function renderSolid(components, ctx) {
|
|
|
4230
4903
|
` // value, no row moves, and the content updates through accessors instead.`,
|
|
4231
4904
|
` const messageKeys = createMemo(() => messages().map((m) => m.id));`,
|
|
4232
4905
|
` const suggestions = ${jsArray(suggestions)};`,
|
|
4906
|
+
...attachmentInit,
|
|
4233
4907
|
...sourcesInit,
|
|
4234
4908
|
...modelInit,
|
|
4235
4909
|
...toolsLines,
|
|
@@ -4676,12 +5350,72 @@ const CHAT_REQUEST_BODY_DECL = [
|
|
|
4676
5350
|
` return (await request.json()) as ChatRequestBody;`,
|
|
4677
5351
|
`}`
|
|
4678
5352
|
];
|
|
5353
|
+
const CONTENT_PARTS_DECL = [
|
|
5354
|
+
`/** Where an attachment's bytes are: inline base64, or an address the PROVIDER`,
|
|
5355
|
+
` * fetches. Never both. */`,
|
|
5356
|
+
`type WireFileSource = { type: 'data'; data: string } | { type: 'url'; url: string };`,
|
|
5357
|
+
``,
|
|
5358
|
+
`/** One piece of a turn, with the string and array content forms flattened into`,
|
|
5359
|
+
` * a single shape. */`,
|
|
5360
|
+
`type WirePart =`,
|
|
5361
|
+
` | { kind: 'text'; text: string }`,
|
|
5362
|
+
` | { kind: 'file'; mediaType: string; filename?: string; source: WireFileSource };`,
|
|
5363
|
+
``,
|
|
5364
|
+
`const DATA_URI = /^data:([^;,]+);base64,([\\s\\S]*)$/;`,
|
|
5365
|
+
``,
|
|
5366
|
+
`/**`,
|
|
5367
|
+
` * Flatten a wire message's content into parts.`,
|
|
5368
|
+
` *`,
|
|
5369
|
+
` * An image sent by URL has no media type here — \`image_url\` carries only the`,
|
|
5370
|
+
` * address — so it reports the top-level segment \`'image'\`, which is all a URL`,
|
|
5371
|
+
` * source needs. Only images can reach that branch: the kit refuses to encode a`,
|
|
5372
|
+
` * remote PDF rather than guess at one.`,
|
|
5373
|
+
` */`,
|
|
5374
|
+
`function wireParts(content: OpenAIWireMessage['content']): WirePart[] {`,
|
|
5375
|
+
` if (content == null) return [];`,
|
|
5376
|
+
` if (typeof content === 'string') return content === '' ? [] : [{ kind: 'text', text: content }];`,
|
|
5377
|
+
` return content.map((part): WirePart => {`,
|
|
5378
|
+
` if (part.type === 'text') return { kind: 'text', text: part.text };`,
|
|
5379
|
+
` if (part.type === 'image_url') {`,
|
|
5380
|
+
` const asData = DATA_URI.exec(part.image_url.url);`,
|
|
5381
|
+
` return asData`,
|
|
5382
|
+
` ? { kind: 'file', mediaType: asData[1], source: { type: 'data', data: asData[2] } }`,
|
|
5383
|
+
` : { kind: 'file', mediaType: 'image', source: { type: 'url', url: part.image_url.url } };`,
|
|
5384
|
+
` }`,
|
|
5385
|
+
` const asData = DATA_URI.exec(part.file.file_data);`,
|
|
5386
|
+
` if (!asData) {`,
|
|
5387
|
+
` // LOUD on purpose. \`file_data\` is a data URI on this wire; anything else`,
|
|
5388
|
+
` // cannot be turned into bytes without fetching it, and forwarding a turn`,
|
|
5389
|
+
` // with the attachment quietly missing is the bug this whole path exists`,
|
|
5390
|
+
` // to prevent.`,
|
|
5391
|
+
` throw new Error(`,
|
|
5392
|
+
` 'Unsupported file content part: file_data must be a data: URI of the form data:<media type>;base64,<data>.',`,
|
|
5393
|
+
` );`,
|
|
5394
|
+
` }`,
|
|
5395
|
+
` return {`,
|
|
5396
|
+
` kind: 'file',`,
|
|
5397
|
+
` mediaType: asData[1],`,
|
|
5398
|
+
` filename: part.file.filename,`,
|
|
5399
|
+
` source: { type: 'data', data: asData[2] },`,
|
|
5400
|
+
` };`,
|
|
5401
|
+
` });`,
|
|
5402
|
+
`}`,
|
|
5403
|
+
``,
|
|
5404
|
+
`/** Just the text of a turn. System, assistant and tool messages are text-only`,
|
|
5405
|
+
` * on this wire, so this collapses the array form for them. */`,
|
|
5406
|
+
`function wireText(content: OpenAIWireMessage['content']): string {`,
|
|
5407
|
+
` return wireParts(content)`,
|
|
5408
|
+
` .map((p) => (p.kind === 'text' ? p.text : ''))`,
|
|
5409
|
+
` .join('');`,
|
|
5410
|
+
`}`
|
|
5411
|
+
];
|
|
4679
5412
|
function withChatRequestBody(fragment) {
|
|
5413
|
+
const decl = /\bwire(?:Parts|Text)\s*\(/.test(fragment) ? [...CHAT_REQUEST_BODY_DECL, ``, ...CONTENT_PARTS_DECL] : CHAT_REQUEST_BODY_DECL;
|
|
4680
5414
|
const lines = fragment.split("\n");
|
|
4681
5415
|
let at = lines.findIndex((l) => /^(?:export\s+)?async function chatHandler\b/.test(l));
|
|
4682
|
-
if (at < 0) return [...
|
|
5416
|
+
if (at < 0) return [...decl, ``, ...lines].join("\n");
|
|
4683
5417
|
while (at > 0 && /^\s*(?:\/\/|\/\*|\*)/.test(lines[at - 1])) at -= 1;
|
|
4684
|
-
return [...lines.slice(0, at), ...
|
|
5418
|
+
return [...lines.slice(0, at), ...decl, ``, ...lines.slice(at)].join("\n");
|
|
4685
5419
|
}
|
|
4686
5420
|
function webRouteFor(integration, framework) {
|
|
4687
5421
|
const fragment = integration.webRoute;
|
|
@@ -5067,7 +5801,7 @@ function rejectUseCase(id) {
|
|
|
5067
5801
|
``,
|
|
5068
5802
|
`Valid useCases (presets): ${valid}.`,
|
|
5069
5803
|
``,
|
|
5070
|
-
// The presets are
|
|
5804
|
+
// The presets are seven points, not the space. A harness that only ever learns
|
|
5071
5805
|
// the six ids will ask for the nearest one instead of the surface it wants,
|
|
5072
5806
|
// so the rejection that teaches the id list is the right place to say so.
|
|
5073
5807
|
`These are PRESETS over the real axis, which is \`components\`. To compose a surface no`,
|
|
@@ -5086,14 +5820,14 @@ const scaffold = {
|
|
|
5086
5820
|
// catalogs to discover valid ids.
|
|
5087
5821
|
//
|
|
5088
5822
|
// `useCase` is OPTIONAL because `components` can carry the surface instead —
|
|
5089
|
-
// the archetypes are
|
|
5823
|
+
// the archetypes are seven points in the components space, not the space. A
|
|
5090
5824
|
// request must still name one of the two, and the handler says so when it
|
|
5091
5825
|
// names neither.
|
|
5092
5826
|
useCase: z$1.string().optional().describe(
|
|
5093
|
-
'Archetype PRESET id, e.g. "drop-in-chat", "support-widget", "knowledge-base", "agentic", "workspace", "voice". Shorthand for the preset\'s `components`. Omit it and pass `components` to compose a surface no preset names.'
|
|
5827
|
+
'Archetype PRESET id, e.g. "drop-in-chat", "support-widget", "knowledge-base", "agentic", "workspace", "voice", "attachments". Shorthand for the preset\'s `components`. Omit it and pass `components` to compose a surface no preset names.'
|
|
5094
5828
|
),
|
|
5095
5829
|
components: z$1.array(z$1.string()).optional().describe(
|
|
5096
|
-
'The kai-* components this surface composes, e.g. ["kai-chat", "kai-tool", "kai-reasoning", "kai-artifact", "kai-resizable"]. The real axis: any combination is renderable, not just the
|
|
5830
|
+
'The kai-* components this surface composes, e.g. ["kai-chat", "kai-tool", "kai-reasoning", "kai-artifact", "kai-resizable"]. The real axis: any combination is renderable, not just the seven presets. Include "kai-chat". Wins over `useCase` when both are given.'
|
|
5097
5831
|
),
|
|
5098
5832
|
integration: z$1.string().describe(
|
|
5099
5833
|
'Backend integration id, e.g. "openrouter", "vercel-ai-sdk", "langgraph", "cloudflare", "ollama", "mastra", "pi", "pydantic-ai".'
|