@kitn.ai/ui 0.27.0 → 0.28.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/README.md +10 -2
- package/dist/{Icon-BnFKqqtY.js → Icon-DObhsRtm.js} +1 -1
- package/dist/{action-icons-CBskfBMz.js → action-icons-CcgNN2oT.js} +1 -1
- package/dist/arrow-down-CjRU3JgV.js +1 -0
- package/dist/arrow-left-Bnpb1UEW.js +1 -0
- package/dist/{artifact-CvXEm1lS.js → artifact-Bwt7OgcR.js} +1 -1
- package/dist/{attachments-C43ZegEq.js → attachments-Qr4kg-2b.js} +1 -1
- package/dist/{audio-visualizer-B-79y1Tj.js → audio-visualizer-DToYdfSc.js} +1 -1
- package/dist/{badge-BpUbq8c0.js → badge-CdEu_jn2.js} +1 -1
- package/dist/{bell-sn17Ip9v.js → bell-DQAAOpDz.js} +1 -1
- package/dist/{button-BH6rN0U-.js → button-DkuOnGLy.js} +1 -1
- package/dist/{card-renderer-Cb1gQmth.js → card-renderer-Jx70m6_l.js} +1 -1
- package/dist/{card-routing-DifQgS7n.js → card-routing-0YidER_3.js} +1 -1
- package/dist/check-njkVdZu2.js +1 -0
- package/dist/{checkbox-Dc9Yov_B.js → checkbox-D7iRB3nL.js} +1 -1
- package/dist/{checkbox-group-B1kDLLYk.js → checkbox-group-B2_rpoPM.js} +1 -1
- package/dist/chevron-down-B72s6PXR.js +1 -0
- package/dist/chevron-right-BatuHzUm.js +1 -0
- package/dist/{choice-card-BT3c1S8g.js → choice-card-m0BYqqOK.js} +1 -1
- package/dist/circle-check-B62pODB3.js +1 -0
- package/dist/{circle-x-wobWkoSx.js → circle-x-BPG0iPst.js} +1 -1
- package/dist/circle-zx0ByzY9.js +1 -0
- package/dist/code-block-ClMLKb7C.js +1 -0
- package/dist/{collapsible-CPJbf6FM.js → collapsible-B4DlWicL.js} +1 -1
- package/dist/components/chat-thread.d.ts +89 -1
- package/dist/components/conversation-item.d.ts +16 -0
- package/dist/components/conversation-panel.d.ts +29 -0
- package/dist/components/home-panel.d.ts +34 -0
- package/dist/components/widget-tab-bar.d.ts +16 -0
- package/dist/{composer-CjngLzal.js → composer-sQ0HcvyG.js} +3 -3
- package/dist/{confirm-card-DCx_A4bU.js → confirm-card-rQWDQsnC.js} +1 -1
- package/dist/construct-cli.es.js +182 -14
- package/dist/{context-BY8V-Nlp.js → context-0kg4ZX1J.js} +1 -1
- package/dist/controllable-CnGpMF6r.js +1 -0
- package/dist/conversation-item-BxwQLaFJ.js +1 -0
- package/dist/conversation-list-BCfGN1JU.js +1 -0
- package/dist/{copy-B6DO4PqB.js → copy-n9u9rqpi.js} +1 -1
- package/dist/{create-tween-CnMHviKB.js → create-tween-C-q-JCM-.js} +1 -1
- package/dist/{create-tween-D1aiGZZh.js → create-tween-CRrJXxBd.js} +1 -1
- package/dist/{create-tween-BPzaefTp.js → create-tween-DZ3RRDHD.js} +1 -1
- package/dist/{create-tween-DUSQUKpr.js → create-tween-uPkArjIG.js} +1 -1
- package/dist/custom-elements.json +51 -2
- package/dist/{default-input-CucQEOqH.js → default-input-CaMrJkgH.js} +1 -1
- package/dist/define-By-uL588.js +1 -0
- package/dist/define.js +2 -2
- package/dist/define.server.js +2 -2
- package/dist/{disclosure-CbLtajUQ.js → disclosure-WUImBbDv.js} +1 -1
- package/dist/{dropdown-BwhViJFd.js → dropdown-wZVeJOMd.js} +1 -1
- 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/checkbox-group.js +1 -1
- package/dist/elements/checkbox.js +1 -1
- package/dist/elements/checkpoint.js +1 -1
- package/dist/elements/choice.js +1 -1
- 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-item.js +1 -1
- package/dist/elements/conversation-list.js +1 -1
- package/dist/elements/dialog.js +1 -1
- package/dist/elements/dock.js +3 -3
- package/dist/elements/dropdown.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-grid.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/radio-group.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 +1 -1
- 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/select.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/slider.js +1 -1
- 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 +20 -4
- package/dist/{ellipsis-DFr4Vqr3.js → ellipsis-CbOpJnU3.js} +1 -1
- package/dist/{embed-5s_vU4J0.js → embed-CrfjkbUG.js} +1 -1
- package/dist/{external-link-u9d85ORQ.js → external-link-ByXAwBBQ.js} +1 -1
- package/dist/{file-text-CX7_x42o.js → file-text-DMib2qwt.js} +1 -1
- package/dist/{file-tree-D3kBjXI5.js → file-tree-CJqM2aP7.js} +1 -1
- package/dist/{folder-V-YdIL8f.js → folder-_BAb929C.js} +1 -1
- package/dist/{form-BRNUMCN2.js → form-DSZgZQZk.js} +1 -1
- package/dist/{hover-card-DaeiYrTA.js → hover-card-BQbul9eq.js} +1 -1
- package/dist/{icon-BpixdZNq.js → icon-CdcfRhYI.js} +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2842 -2703
- package/dist/index.server.js +2820 -2681
- package/dist/{info-8mFsIjS3.js → info-DsFjGuI_.js} +1 -1
- package/dist/{input-d1Yuu-hz.js → input-DKbmkMos.js} +1 -1
- package/dist/kai.es.js +1 -1
- package/dist/{kbd-961zMPFB.js → kbd-PvdGhX4s.js} +1 -1
- package/dist/{link-preview-DIIbV2Sd.js → link-preview-DbM1866N.js} +1 -1
- package/dist/{link-DlZZBGaR.js → link-zF8PzWfF.js} +1 -1
- package/dist/{loader-YQszHFaW.js → loader-BM4CEz4G.js} +1 -1
- package/dist/{markdown-CVk1MPsj.js → markdown-LMvmAyBk.js} +7 -7
- package/dist/mcp.es.js +78 -4
- package/dist/{message-CwKVEdZg.js → message-BSsgvHvf.js} +1 -1
- package/dist/{message-DS8lzHc1.js → message-BYZECyek.js} +1 -1
- package/dist/message-circle-DruwObfX.js +1 -0
- package/dist/message-square-BG6q6mlm.js +1 -0
- package/dist/{minimize-2-UEZmP2QT.js → minimize-2-BQRoBV1V.js} +1 -1
- package/dist/{model-switcher-Kk1jEQ5t.js → model-switcher-CMpCOYcB.js} +1 -1
- package/dist/{overlay-C1od6LOz.js → overlay-Cb-MkyDa.js} +1 -1
- package/dist/{panel-right-ryET0yyn.js → panel-right-i8warFxT.js} +1 -1
- package/dist/{paperclip-BDNoZFJP.js → paperclip-ByY3UVO6.js} +1 -1
- package/dist/play-BvKKq3fX.js +1 -0
- package/dist/primitives/conversation-store.d.ts +54 -0
- package/dist/{progress-bar-BvYX0aoy.js → progress-bar-jVAHvMUf.js} +1 -1
- package/dist/{prompt-suggestion-BRFu2lcu.js → prompt-suggestion-CD-qtNgC.js} +1 -1
- package/dist/{radio-DcL23cNJ.js → radio-BsoBMhkF.js} +1 -1
- package/dist/react/index.d.ts +393 -0
- package/dist/react.js +20 -20
- package/dist/{reasoning-Cackq4Rh.js → reasoning-4kBPtlTx.js} +1 -1
- package/dist/register-impl-B1hLkrI8.js +424 -0
- package/dist/{resizable-Cp9HUYih.js → resizable-uPmY9VY-.js} +1 -1
- package/dist/{rotate-ccw-0DLRDHH5.js → rotate-ccw-B2xn93py.js} +1 -1
- package/dist/{rotate-cw-DOB9IGcU.js → rotate-cw-CMVLoCyH.js} +1 -1
- package/dist/{scroll-area-emVmWP4K.js → scroll-area-BqLUo8ZZ.js} +1 -1
- package/dist/{scroll-button-CeP_gv1x.js → scroll-button-DslXiJyL.js} +1 -1
- package/dist/{select-CWmu00tS.js → select-2cLRtPGT.js} +1 -1
- package/dist/{separator-CE6vly6z.js → separator-BWQtNJcM.js} +1 -1
- package/dist/{settings-CE-78SvZ.js → settings-BBR63jLg.js} +1 -1
- package/dist/{settings-group-ZFWNxFQR.js → settings-group-B2ciy-Iw.js} +1 -1
- package/dist/{skeleton-BebdP4mf.js → skeleton-CkHI-HgC.js} +1 -1
- package/dist/{slider-BAKRYUlz.js → slider-DDR9U-i0.js} +1 -1
- package/dist/{solid-BE5ui6sy.js → solid-BzCFdL25.js} +7720 -7058
- package/dist/{solid-DFhsce3O.js → solid-DruVOXnf.js} +5864 -5283
- package/dist/solid.d.ts +6 -2
- package/dist/solid.js +217 -210
- package/dist/solid.server.js +217 -210
- package/dist/{source-B0FlguSx.js → source-GEnFF528.js} +1 -1
- package/dist/{star-CBMZvLuN.js → star-CilHq8GP.js} +1 -1
- package/dist/store-Ciyk6eZG.js +1 -0
- package/dist/{switch-CViARZCy.js → switch-KkeQIbuH.js} +1 -1
- package/dist/{tasks-card-MjCcOhR_.js → tasks-card-BrN4bggJ.js} +1 -1
- package/dist/{text-shimmer-DKlwRF2G.js → text-shimmer-Dcj1MHuq.js} +1 -1
- package/dist/{textarea-3ay2Me2-.js → textarea-Dpvbngbg.js} +1 -1
- package/dist/theme.tokens.css +28 -0
- package/dist/{thumbs-up-DQcu2HMO.js → thumbs-up-CsT9Wzb9.js} +1 -1
- package/dist/{toast-store-C6yKOhbk.js → toast-store-tgK6MuMv.js} +1 -1
- package/dist/{tool-BCYNlfKy.js → tool-DzUjIBT8.js} +1 -1
- package/dist/{tooltip-05zI4hjs.js → tooltip-Bkkg_4t0.js} +1 -1
- package/dist/{trash-2-BR1RxGky.js → trash-2-jBKxjGGQ.js} +1 -1
- package/dist/{triangle-alert-DHohDOCy.js → triangle-alert-DlQ68ppf.js} +1 -1
- package/dist/types.d.ts +48 -3
- package/dist/ui/dock.d.ts +29 -0
- package/dist/{upload-BbGW6A_u.js → upload-TcAHXv9W.js} +1 -1
- package/dist/{url-scheme-policy-DHpoTJwB.js → url-scheme-policy-DR5DgvkM.js} +1 -1
- package/dist/{use-card-resolution-CJW4grFa.js → use-card-resolution-DaZqAdWf.js} +1 -1
- package/dist/{variant-aurora-xiMPty8f.js → variant-aurora-BhLuYxJR.js} +1 -1
- package/dist/{variant-aurora-BUOVgPOB.js → variant-aurora-CNmfHhcx.js} +2 -2
- package/dist/{variant-aurora-CUsUJvFI.js → variant-aurora-D6GsdTPA.js} +2 -2
- package/dist/{variant-aurora-CW2i4bfr.js → variant-aurora-DbW_OWL2.js} +1 -1
- package/dist/{variant-custom-D91HjAKz.js → variant-custom-BRvaNHSi.js} +1 -1
- package/dist/{variant-custom-BQtOnmeO.js → variant-custom-BvYHR62b.js} +1 -1
- package/dist/{variant-custom-CAnzzppW.js → variant-custom-DMTrEO5w.js} +2 -2
- package/dist/{variant-custom-CAm9L-E5.js → variant-custom-DV2x254s.js} +2 -2
- package/dist/{variant-wave-Ca1Ls4_y.js → variant-wave-BNMGrDfj.js} +2 -2
- package/dist/{variant-wave-uY5crU_K.js → variant-wave-BpDd8t3Z.js} +1 -1
- package/dist/{variant-wave-CxX39DKd.js → variant-wave-CyBueXj9.js} +2 -2
- package/dist/{variant-wave-DYyzIIZv.js → variant-wave-sp-qwsdI.js} +1 -1
- package/dist/x-Z5UntkIJ.js +1 -0
- package/frameworks/react/index.tsx +14 -4
- package/llms-full.txt +7 -2
- package/package.json +1 -1
- package/src/agent-tooling/README.md +192 -0
- package/src/agent-tooling/catalog/invariants.ts +2 -2
- package/src/agent-tooling/construct/cli.ts +16 -0
- package/src/agent-tooling/construct/codegen.ts +309 -27
- package/src/agent-tooling/construct/construct.v1.schema.json +69 -0
- package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +11 -2
- package/src/agent-tooling/construct/schema.ts +81 -0
- package/src/components/chat-thread.tsx +532 -36
- package/src/components/conversation-item.tsx +23 -0
- package/src/components/conversation-panel.tsx +118 -0
- package/src/components/home-panel.tsx +159 -0
- package/src/components/widget-tab-bar.tsx +73 -0
- package/src/elements/chat.tsx +94 -2
- package/src/elements/compiled.css +1 -1
- package/src/elements/element-meta.json +49 -6
- package/src/elements/element-nonscalar.json +2 -0
- package/src/elements/element-types.d.ts +20 -4
- package/src/index.ts +3 -1
- package/src/primitives/conversation-store.ts +248 -0
- package/src/solid.ts +10 -2
- package/src/types.ts +45 -3
- package/src/ui/dock.tsx +47 -1
- package/theme.css +28 -0
- package/dist/arrow-down-Br0qtpsz.js +0 -1
- package/dist/arrow-left-Dj7xgE12.js +0 -1
- package/dist/check-DPeeMUYx.js +0 -1
- package/dist/chevron-down-CHSWk7ZT.js +0 -1
- package/dist/chevron-right-B7l_fMgx.js +0 -1
- package/dist/circle-Bb0yOpqM.js +0 -1
- package/dist/circle-check-BGZpCiz7.js +0 -1
- package/dist/code-block-CUQ5mtv0.js +0 -1
- package/dist/controllable-D43BKmtX.js +0 -1
- package/dist/conversation-list-D4lLU_yS.js +0 -1
- package/dist/define-CGGX-7Vg.js +0 -1
- package/dist/message-circle-Xf8Q-vZp.js +0 -1
- package/dist/message-square-Cxnwgkns.js +0 -1
- package/dist/play-OFIvYXUQ.js +0 -1
- package/dist/register-impl-C7rENvPK.js +0 -424
- package/dist/store-7eoadydQ.js +0 -1
- package/dist/x-C5m1JFgi.js +0 -1
|
@@ -31,6 +31,29 @@ export function relativeTimeShort(iso?: string, now: number = Date.now()): strin
|
|
|
31
31
|
return `${Math.floor(hours / 24)}d ago`;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Whether a conversation should show an unread indicator (owner round,
|
|
36
|
+
* 2026-08-26). `lastReadAt`'s own doc (`types.ts`) has the full contract;
|
|
37
|
+
* this is the one place that reads it, so both list surfaces (this file's
|
|
38
|
+
* `ConversationItem` and the widget-box `ConversationPanel`) derive it
|
|
39
|
+
* identically rather than each restating the comparison.
|
|
40
|
+
*
|
|
41
|
+
* Absent `lastReadAt` reads as NOT unread — the decide-loudly default for a
|
|
42
|
+
* store that never implements `ConversationStore.markRead` at all (every
|
|
43
|
+
* summary it returns leaves the field undefined forever, so this always
|
|
44
|
+
* returns `false` for it) rather than guessing "probably unread" from a
|
|
45
|
+
* signal the store never actually provided. Defensive `Date.parse`, same
|
|
46
|
+
* pattern as `relativeTimeShort` above: an unparsable date reads as not
|
|
47
|
+
* unread rather than throwing.
|
|
48
|
+
*/
|
|
49
|
+
export function isConversationUnread(conv: Pick<ConversationSummary, 'updatedAt' | 'lastReadAt'>): boolean {
|
|
50
|
+
if (!conv.lastReadAt) return false;
|
|
51
|
+
const updated = Date.parse(conv.updatedAt);
|
|
52
|
+
const read = Date.parse(conv.lastReadAt);
|
|
53
|
+
if (Number.isNaN(updated) || Number.isNaN(read)) return false;
|
|
54
|
+
return updated > read;
|
|
55
|
+
}
|
|
56
|
+
|
|
34
57
|
/**
|
|
35
58
|
* The slotted-item shape rendered by `<kai-conversation-item>` — the composed
|
|
36
59
|
* row of the consumer-owned loop. Distinct from the
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { For, Show, createMemo } from 'solid-js';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
import { ScrollArea } from '../ui/scroll-area';
|
|
4
|
+
import { relativeTimeShort, isConversationUnread } from './conversation-item';
|
|
5
|
+
import { byRecency } from '../primitives/conversation-store';
|
|
6
|
+
import type { ConversationSummary } from '../types';
|
|
7
|
+
|
|
8
|
+
export interface ConversationPanelProps {
|
|
9
|
+
conversations: ConversationSummary[];
|
|
10
|
+
activeId?: string;
|
|
11
|
+
onSelect: (id: string) => void;
|
|
12
|
+
onNewChat: () => void;
|
|
13
|
+
/** Wording for the floating new-conversation pill. Defaults to "New conversation". */
|
|
14
|
+
newChatLabel?: string;
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The widget-box list view (owner rework, 2026-08-26 — the earlier retrofit
|
|
20
|
+
* of the desktop `ConversationList` into this box was rejected at the live
|
|
21
|
+
* demo). NOT `ConversationList`, which stays the desktop-sidebar surface
|
|
22
|
+
* unchanged: no search box, no group headers, no per-row menu, no full-width
|
|
23
|
+
* footer bar, no second "+" beside the header toggle. A row is a
|
|
24
|
+
* conversation, full stop — bold title, right-aligned relative time, one
|
|
25
|
+
* truncated line of the last message. The ONE way to start a new
|
|
26
|
+
* conversation is the floating pill near the bottom.
|
|
27
|
+
*
|
|
28
|
+
* Modeled directly on Intercom's Messenger "Messages" tab (first-hand
|
|
29
|
+
* research: `.superpowers/sdd/2026-08-26-conversations/
|
|
30
|
+
* research-intercom-messages-view.md`) — a box this size gets one job at a
|
|
31
|
+
* time: browsing conversations OR having one, never both, and the list
|
|
32
|
+
* replaces the ENTIRE content area (`ChatThread` hides the thread,
|
|
33
|
+
* suggestions and composer while this renders — see its `view() === 'list'`
|
|
34
|
+
* branch).
|
|
35
|
+
*/
|
|
36
|
+
export function ConversationPanel(props: ConversationPanelProps) {
|
|
37
|
+
// Most-recently-updated first — the same defensive sort ChatThread's own
|
|
38
|
+
// auto-restore uses (an unparsable/missing date sorts last, never throws).
|
|
39
|
+
const ordered = createMemo(() =>
|
|
40
|
+
[...props.conversations].sort(byRecency),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div class={cn('relative flex h-full flex-col', props.class)}>
|
|
45
|
+
<Show
|
|
46
|
+
when={props.conversations.length > 0}
|
|
47
|
+
fallback={
|
|
48
|
+
<div class="flex flex-1 flex-col items-center justify-center gap-1 p-6 text-center text-sm text-muted-foreground">
|
|
49
|
+
No conversations yet
|
|
50
|
+
</div>
|
|
51
|
+
}
|
|
52
|
+
>
|
|
53
|
+
<ScrollArea class="flex-1 px-2 pt-2">
|
|
54
|
+
{/* Bottom padding clears the floating pill so the last row is never
|
|
55
|
+
hidden behind it. */}
|
|
56
|
+
<div role="list" aria-label="Conversations" class="space-y-0.5 pb-16">
|
|
57
|
+
<For each={ordered()}>
|
|
58
|
+
{(conv) => {
|
|
59
|
+
const isActive = () => conv.id === props.activeId;
|
|
60
|
+
const time = () => relativeTimeShort(conv.updatedAt ?? conv.lastMessageAt);
|
|
61
|
+
const unread = () => isConversationUnread(conv);
|
|
62
|
+
return (
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
role="listitem"
|
|
66
|
+
data-conversation-id={conv.id}
|
|
67
|
+
data-active={isActive() ? '' : undefined}
|
|
68
|
+
data-unread={unread() ? '' : undefined}
|
|
69
|
+
aria-current={isActive() ? 'true' : undefined}
|
|
70
|
+
onClick={() => props.onSelect(conv.id)}
|
|
71
|
+
class={cn(
|
|
72
|
+
'block w-full rounded-lg px-3 py-2.5 text-left transition-colors',
|
|
73
|
+
isActive() ? 'bg-muted' : 'hover:bg-muted/50',
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
<div class="flex items-baseline gap-2">
|
|
77
|
+
<span class={cn('min-w-0 flex-1 truncate text-sm font-semibold', isActive() ? 'text-foreground' : 'text-foreground/90')}>
|
|
78
|
+
{conv.title}
|
|
79
|
+
</span>
|
|
80
|
+
<Show when={time()}>
|
|
81
|
+
<span class="shrink-0 text-xs text-muted-foreground">{time()}</span>
|
|
82
|
+
</Show>
|
|
83
|
+
</div>
|
|
84
|
+
{/* Trailing line: the last-message preview, plus — Intercom's own
|
|
85
|
+
placement (research doc §2) — a small unread dot at its end. No
|
|
86
|
+
preview text and no unread still renders nothing, same as before. */}
|
|
87
|
+
<Show when={conv.trailing || unread()}>
|
|
88
|
+
<div class="mt-0.5 flex items-center gap-1.5">
|
|
89
|
+
<span class="min-w-0 flex-1 truncate text-xs text-muted-foreground">{conv.trailing}</span>
|
|
90
|
+
<Show when={unread()}>
|
|
91
|
+
<span aria-hidden="true" class="size-1.5 shrink-0 rounded-full bg-unread" />
|
|
92
|
+
<span class="sr-only">Unread</span>
|
|
93
|
+
</Show>
|
|
94
|
+
</div>
|
|
95
|
+
</Show>
|
|
96
|
+
</button>
|
|
97
|
+
);
|
|
98
|
+
}}
|
|
99
|
+
</For>
|
|
100
|
+
</div>
|
|
101
|
+
</ScrollArea>
|
|
102
|
+
</Show>
|
|
103
|
+
{/* The ONE new-conversation control: a compact centered floating pill,
|
|
104
|
+
never a header "+" AND a footer bar at once (the retrofit's
|
|
105
|
+
rejected shape). */}
|
|
106
|
+
<div class="pointer-events-none absolute inset-x-0 bottom-3 flex justify-center">
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
data-kai-new-conversation
|
|
110
|
+
onClick={props.onNewChat}
|
|
111
|
+
class="pointer-events-auto rounded-full border border-border bg-background px-4 py-2 text-sm font-medium text-foreground shadow-md transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
112
|
+
>
|
|
113
|
+
{props.newChatLabel ?? 'New conversation'}
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { For, Show } from 'solid-js';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
import { Button } from '../ui/button';
|
|
4
|
+
import { renderIcon } from '../ui/icon';
|
|
5
|
+
import { isSafeUrl } from '../primitives/url-scheme-policy';
|
|
6
|
+
import { relativeTimeShort, isConversationUnread } from './conversation-item';
|
|
7
|
+
import type { ConversationSummary, HomeLinkEntry } from '../types';
|
|
8
|
+
|
|
9
|
+
export interface HomePanelProps {
|
|
10
|
+
greeting?: { title?: string; subtitle?: string };
|
|
11
|
+
/** The most-recent conversation, host-derived. `undefined` hides the card
|
|
12
|
+
* entirely — this component never fetches or picks one itself. */
|
|
13
|
+
recent?: ConversationSummary;
|
|
14
|
+
/** Defaults to `true`. */
|
|
15
|
+
showNewConversation?: boolean;
|
|
16
|
+
/** Defaults to `'Send us a message'`. */
|
|
17
|
+
newChatLabel?: string;
|
|
18
|
+
links?: HomeLinkEntry[];
|
|
19
|
+
onSelectRecent?: (id: string) => void;
|
|
20
|
+
onNewChat: () => void;
|
|
21
|
+
/** Fired only for href-less link entries — an entry with a safe `href`
|
|
22
|
+
* navigates as a real anchor instead. */
|
|
23
|
+
onLink?: (entry: HomeLinkEntry) => void;
|
|
24
|
+
class?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The widget home screen (Intercom-pattern, H-1): greeting, the most-recent
|
|
29
|
+
* conversation, a "start a new conversation" CTA, and a list of host-defined
|
|
30
|
+
* links. Pure props in, events out — no fetching, no routing; a later task
|
|
31
|
+
* wires this behind `ChatThread`'s Home/Messages tab bar.
|
|
32
|
+
*
|
|
33
|
+
* Unsafe-href rule: a `links` entry with an `href` that fails `isSafeUrl`
|
|
34
|
+
* (e.g. `javascript:`) is NOT silently promoted into a button/event-emitter —
|
|
35
|
+
* that would let a host-authored-but-attacker-influenced config still fire a
|
|
36
|
+
* handler off a scheme SAFE_SCHEMES rejects. It renders as a plain,
|
|
37
|
+
* non-interactive row: label visible, no anchor, no button, no click handler.
|
|
38
|
+
*/
|
|
39
|
+
export function HomePanel(props: HomePanelProps) {
|
|
40
|
+
const title = () => props.greeting?.title ?? 'Hi there 👋';
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
data-kai-home-panel
|
|
45
|
+
class={cn('flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-5', props.class)}
|
|
46
|
+
>
|
|
47
|
+
<div class="flex flex-col gap-1">
|
|
48
|
+
<h2 class="text-xl font-semibold text-foreground">{title()}</h2>
|
|
49
|
+
<Show when={props.greeting?.subtitle}>
|
|
50
|
+
<p class="text-sm text-muted-foreground">{props.greeting!.subtitle}</p>
|
|
51
|
+
</Show>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<Show when={props.recent}>
|
|
55
|
+
{(recent) => {
|
|
56
|
+
const time = () => relativeTimeShort(recent().updatedAt ?? recent().lastMessageAt);
|
|
57
|
+
const unread = () => isConversationUnread(recent());
|
|
58
|
+
return (
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
data-kai-home-recent
|
|
62
|
+
onClick={() => props.onSelectRecent?.(recent().id)}
|
|
63
|
+
class="flex flex-col gap-1 rounded-xl border border-border p-4 text-left transition-colors hover:bg-accent"
|
|
64
|
+
>
|
|
65
|
+
<span class="flex items-center gap-2 text-sm font-semibold text-foreground">
|
|
66
|
+
<span class="min-w-0 flex-1 truncate">{recent().title}</span>
|
|
67
|
+
<Show when={unread()}>
|
|
68
|
+
<span aria-hidden="true" class="size-1.5 shrink-0 rounded-full bg-unread" />
|
|
69
|
+
</Show>
|
|
70
|
+
<Show when={time()}>
|
|
71
|
+
<span class="shrink-0 text-xs font-normal text-muted-foreground">{time()}</span>
|
|
72
|
+
</Show>
|
|
73
|
+
</span>
|
|
74
|
+
<Show when={recent().trailing}>
|
|
75
|
+
<span class="line-clamp-1 text-sm text-muted-foreground">{recent().trailing}</span>
|
|
76
|
+
</Show>
|
|
77
|
+
</button>
|
|
78
|
+
);
|
|
79
|
+
}}
|
|
80
|
+
</Show>
|
|
81
|
+
|
|
82
|
+
<Show when={props.showNewConversation !== false}>
|
|
83
|
+
<Button
|
|
84
|
+
type="button"
|
|
85
|
+
data-kai-home-new
|
|
86
|
+
full
|
|
87
|
+
align="start"
|
|
88
|
+
onClick={() => props.onNewChat()}
|
|
89
|
+
class="justify-between"
|
|
90
|
+
>
|
|
91
|
+
<span>{props.newChatLabel ?? 'Send us a message'}</span>
|
|
92
|
+
{renderIcon('arrow-right', { class: 'size-4 shrink-0' })}
|
|
93
|
+
</Button>
|
|
94
|
+
</Show>
|
|
95
|
+
|
|
96
|
+
<Show when={props.links && props.links.length > 0}>
|
|
97
|
+
<div class="flex flex-col overflow-hidden rounded-xl border border-border">
|
|
98
|
+
<For each={props.links}>
|
|
99
|
+
{(entry) => {
|
|
100
|
+
const safeHref = () => (entry.href && isSafeUrl(entry.href) ? entry.href : undefined);
|
|
101
|
+
const inner = (
|
|
102
|
+
<>
|
|
103
|
+
<span class="flex min-w-0 items-center gap-2">
|
|
104
|
+
<Show when={entry.icon}>{renderIcon(entry.icon, { class: 'size-4 shrink-0 text-muted-foreground' })}</Show>
|
|
105
|
+
<span class="flex min-w-0 flex-col text-left">
|
|
106
|
+
<span class="truncate text-sm font-medium text-foreground">{entry.label}</span>
|
|
107
|
+
<Show when={entry.description}>
|
|
108
|
+
<span class="truncate text-xs text-muted-foreground">{entry.description}</span>
|
|
109
|
+
</Show>
|
|
110
|
+
</span>
|
|
111
|
+
</span>
|
|
112
|
+
{renderIcon('chevron-right', { class: 'size-4 shrink-0 text-muted-foreground' })}
|
|
113
|
+
</>
|
|
114
|
+
);
|
|
115
|
+
const rowClass = 'flex items-center justify-between gap-2 border-b border-border p-3 last:border-b-0';
|
|
116
|
+
return (
|
|
117
|
+
<Show
|
|
118
|
+
when={!entry.href}
|
|
119
|
+
fallback={
|
|
120
|
+
<Show
|
|
121
|
+
when={safeHref()}
|
|
122
|
+
fallback={
|
|
123
|
+
// Unsafe href: a non-interactive row. No anchor, no
|
|
124
|
+
// button, no handler — the label stays visible, it
|
|
125
|
+
// just never becomes clickable.
|
|
126
|
+
<div data-kai-home-link class={rowClass}>
|
|
127
|
+
{inner}
|
|
128
|
+
</div>
|
|
129
|
+
}
|
|
130
|
+
>
|
|
131
|
+
<a
|
|
132
|
+
data-kai-home-link
|
|
133
|
+
href={safeHref()}
|
|
134
|
+
target="_blank"
|
|
135
|
+
rel="noreferrer noopener"
|
|
136
|
+
class={cn(rowClass, 'transition-colors hover:bg-accent')}
|
|
137
|
+
>
|
|
138
|
+
{inner}
|
|
139
|
+
</a>
|
|
140
|
+
</Show>
|
|
141
|
+
}
|
|
142
|
+
>
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
data-kai-home-link
|
|
146
|
+
onClick={() => props.onLink?.(entry)}
|
|
147
|
+
class={cn(rowClass, 'w-full text-left transition-colors hover:bg-accent')}
|
|
148
|
+
>
|
|
149
|
+
{inner}
|
|
150
|
+
</button>
|
|
151
|
+
</Show>
|
|
152
|
+
);
|
|
153
|
+
}}
|
|
154
|
+
</For>
|
|
155
|
+
</div>
|
|
156
|
+
</Show>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Show } from 'solid-js';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
import { renderIcon } from '../ui/icon';
|
|
4
|
+
|
|
5
|
+
export interface WidgetTabBarProps {
|
|
6
|
+
active: 'home' | 'messages';
|
|
7
|
+
onChange: (tab: 'home' | 'messages') => void;
|
|
8
|
+
/** Whether the Messages tab has unread activity. Reaches BOTH the visible
|
|
9
|
+
* dot and the tab's accessible name (`aria-label`) — a dot alone is
|
|
10
|
+
* invisible to assistive tech, the #336 lesson this test pins. */
|
|
11
|
+
unread?: boolean;
|
|
12
|
+
homeLabel?: string;
|
|
13
|
+
messagesLabel?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The widget's Home/Messages tab bar (Intercom-pattern chrome, H-2/H-6). Real
|
|
18
|
+
* `tablist`/`tab` semantics — this is a persistent view switch within the
|
|
19
|
+
* widget, not page navigation, so it is not `<Nav>`'s `role="page"` dialect.
|
|
20
|
+
*/
|
|
21
|
+
export function WidgetTabBar(props: WidgetTabBarProps) {
|
|
22
|
+
const home = () => props.homeLabel ?? 'Home';
|
|
23
|
+
const messages = () => props.messagesLabel ?? 'Messages';
|
|
24
|
+
const messagesLabel = () => (props.unread ? `${messages()} (unread)` : messages());
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<nav
|
|
28
|
+
role="tablist"
|
|
29
|
+
aria-label="Widget navigation"
|
|
30
|
+
class="flex h-14 shrink-0 items-stretch border-t border-border bg-background"
|
|
31
|
+
>
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
role="tab"
|
|
35
|
+
data-kai-tab-home
|
|
36
|
+
aria-selected={props.active === 'home'}
|
|
37
|
+
aria-label={home()}
|
|
38
|
+
onClick={() => props.onChange('home')}
|
|
39
|
+
class={cn(
|
|
40
|
+
'flex flex-1 flex-col items-center justify-center gap-0.5 text-xs transition-colors',
|
|
41
|
+
props.active === 'home' ? 'text-primary' : 'text-muted-foreground hover:text-foreground',
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
{renderIcon('home', { class: 'size-5' })}
|
|
45
|
+
<span>{home()}</span>
|
|
46
|
+
</button>
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
role="tab"
|
|
50
|
+
data-kai-tab-messages
|
|
51
|
+
aria-selected={props.active === 'messages'}
|
|
52
|
+
aria-label={messagesLabel()}
|
|
53
|
+
onClick={() => props.onChange('messages')}
|
|
54
|
+
class={cn(
|
|
55
|
+
'flex flex-1 flex-col items-center justify-center gap-0.5 text-xs transition-colors',
|
|
56
|
+
props.active === 'messages' ? 'text-primary' : 'text-muted-foreground hover:text-foreground',
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
<span class="relative">
|
|
60
|
+
{renderIcon('message-square', { class: 'size-5' })}
|
|
61
|
+
<Show when={props.unread}>
|
|
62
|
+
<span
|
|
63
|
+
data-kai-tab-unread
|
|
64
|
+
aria-hidden="true"
|
|
65
|
+
class="absolute -right-0.5 -top-0.5 size-1.5 rounded-full bg-unread"
|
|
66
|
+
/>
|
|
67
|
+
</Show>
|
|
68
|
+
</span>
|
|
69
|
+
<span>{messages()}</span>
|
|
70
|
+
</button>
|
|
71
|
+
</nav>
|
|
72
|
+
);
|
|
73
|
+
}
|
package/src/elements/chat.tsx
CHANGED
|
@@ -10,12 +10,51 @@ import type { ChatMessage } from './chat-types';
|
|
|
10
10
|
import type { TriggerDef } from '../components/composer';
|
|
11
11
|
import type { ComposerDoc } from '../primitives/composer-model';
|
|
12
12
|
import type { ProseSize } from '../primitives/chat-config';
|
|
13
|
-
import type { ModelOption } from '../types';
|
|
13
|
+
import type { ModelOption, HomeConfig, HomeLinkEntry } from '../types';
|
|
14
|
+
import type { ConversationStore } from '../primitives/conversation-store';
|
|
14
15
|
|
|
15
16
|
type Props = Omit<ChatThreadProps,
|
|
16
17
|
'class' | 'onValueChange' | 'onSubmit' | 'onAttachmentsChange' | 'onSuggestionClick' | 'onModelChange'
|
|
17
18
|
| 'onMessageAction' | 'onWebSearch' | 'onVoice' | 'controllerRef' | 'cardTypes' | 'cardSchemas' | 'cardHostElement' | 'messages'
|
|
18
19
|
| 'accept' | 'onAttachmentsRejected'
|
|
20
|
+
// `conversations`/`store` are re-declared below (own doc comments, matching
|
|
21
|
+
// this element's own attribute/property conventions) rather than left to
|
|
22
|
+
// flow through `Omit`'s pass-through — same reason `messages` is excluded
|
|
23
|
+
// above. Left unexcluded, the intersection carries TWO declarations of the
|
|
24
|
+
// same property (the inherited `ChatThreadProps` one plus the re-declared
|
|
25
|
+
// one below) and `gen-element-api.mjs` concatenates both JSDoc comments into
|
|
26
|
+
// one duplicated, em-dash-laden description.
|
|
27
|
+
| 'conversations' | 'store'
|
|
28
|
+
// `home` is re-declared below for the same reason as `conversations`/`store`
|
|
29
|
+
// above (own element-facing doc comment rather than the ChatThread-level one
|
|
30
|
+
// flowing through `Omit`'s pass-through). `onHomeLink` is wired internally
|
|
31
|
+
// (JSX prop on `<ChatThread>` below) as a dispatched `kai-home-link` event,
|
|
32
|
+
// matching every other ChatThread callback on this element — same reasoning
|
|
33
|
+
// as `onConversationLoad` just below.
|
|
34
|
+
| 'home' | 'onHomeLink'
|
|
35
|
+
// `onConversationLoad` is wired internally (below, JSX prop on
|
|
36
|
+
// `<ChatThread>`) as a dispatched `kai-conversation-load` event — matching
|
|
37
|
+
// every other ChatThread callback on this element — rather than left as a
|
|
38
|
+
// settable JS property: a `JSX.Element`-shaped callback prop has no HTML-
|
|
39
|
+
// consumer analogue the way `store`/`messages` do, and the kai- contract's
|
|
40
|
+
// idiom for "this thread wants to tell you something" is already an event.
|
|
41
|
+
// Excluded from `Props` for the same reason `onValueChange`/`onSubmit`/etc
|
|
42
|
+
// are excluded above: it is not a property a consumer of `<kai-chat>` sets.
|
|
43
|
+
| 'onConversationLoad'
|
|
44
|
+
// `hostOpen`/`onUnreadChange` (unread indicators, 2026-08-26) are the SAME
|
|
45
|
+
// "types without forwarding" trap as `onConversationLoad` above, for the
|
|
46
|
+
// same reason: this facade does not read either off the host element and
|
|
47
|
+
// forward it onto `<ChatThread>` below. Both exist for a caller composing
|
|
48
|
+
// `ChatThread` directly as a Solid component with its OWN show/hide chrome
|
|
49
|
+
// to report through `hostOpen` and its own sibling control (a `Dock`'s
|
|
50
|
+
// `unread` badge) to mirror `onUnreadChange` onto — the construct engine's
|
|
51
|
+
// emitted App is the motivating case (codegen.ts's `emitChatThreadUnreadProps`),
|
|
52
|
+
// exactly like `headerEndContent`/`emptyContent` below. `<kai-chat>` has no
|
|
53
|
+
// such sibling chrome of its own to report to or read from, so there is
|
|
54
|
+
// nothing here for either prop to DO — left unexcluded, `gen-element-api.mjs`
|
|
55
|
+
// would still type them into the public API and docs as settable properties
|
|
56
|
+
// that silently do nothing when set.
|
|
57
|
+
| 'hostOpen' | 'onUnreadChange'
|
|
19
58
|
// `headerEndContent`/`emptyContent` are JSX.Element escape hatches for a caller
|
|
20
59
|
// composing `ChatThread` directly as a Solid component (see their doc comments in
|
|
21
60
|
// chat-thread.tsx — the construct-engine's emitted App is the motivating case).
|
|
@@ -75,6 +114,40 @@ type Props = Omit<ChatThreadProps,
|
|
|
75
114
|
* authored one carries `$schema`/`title`/`description`/`additionalProperties`,
|
|
76
115
|
* so the tighter type would reject both of the normal ways to supply one. */
|
|
77
116
|
cardSchemas?: Record<string, object>;
|
|
117
|
+
/** Turns on the prior-conversations list (a list-toggle button in the
|
|
118
|
+
* header, plus a second list view sharing the panel, C-1). Attribute-
|
|
119
|
+
* settable like every other boolean flag on this element:
|
|
120
|
+
* `<kai-chat conversations>`. Requires `store`. A row select, "new
|
|
121
|
+
* conversation," or the visitor's mount-time auto-restore all deliver
|
|
122
|
+
* their messages the same way: listen for `kai-conversation-load` and
|
|
123
|
+
* set `el.messages` from `event.detail.messages` (a fresh array): this
|
|
124
|
+
* element does not update `messages` for you. Set with no `store`, the
|
|
125
|
+
* underlying `ChatThread` decides loudly (one console.error) and stays
|
|
126
|
+
* visually off; this facade always supplies its own internal load
|
|
127
|
+
* handler (the `kai-conversation-load` dispatch below), so the second
|
|
128
|
+
* ChatThread guard, missing `onConversationLoad`, never trips here,
|
|
129
|
+
* even for a consumer who never listens for the event. Default false. */
|
|
130
|
+
conversations?: boolean;
|
|
131
|
+
/** The adapter this thread persists conversations through: an object of
|
|
132
|
+
* three functions (`list`/`load`/`save`; `ConversationStore`, exported
|
|
133
|
+
* from `@kitn.ai/ui`'s `primitives/conversation-store`). A JS PROPERTY
|
|
134
|
+
* ONLY: `el.store = myAdapter`. It can never be an attribute, since a
|
|
135
|
+
* function-bearing object has no HTML string form, the same reasoning
|
|
136
|
+
* that keeps `messages`/`cardSchemas` property-only (the kai- contract:
|
|
137
|
+
* array/object props are JS properties, never attributes). Two built-ins
|
|
138
|
+
* ship: `localStorageStore(name, userId?)` and `fetchStore(url, userId?)`. */
|
|
139
|
+
store?: ConversationStore;
|
|
140
|
+
/** Turns on the widget home screen (Intercom-pattern): the panel boots into
|
|
141
|
+
* a `home` view, with a greeting, most-recent-conversation card, a "new
|
|
142
|
+
* conversation" CTA, and host-defined links, plus a Home/Messages tab bar
|
|
143
|
+
* for switching back to the thread. An OBJECT, so it is a JS property only:
|
|
144
|
+
* `el.home = { greeting: { title: 'Hey' }, links: [...] }`, never an
|
|
145
|
+
* attribute (the kai- contract: array/object props are JS properties).
|
|
146
|
+
* A `links` entry with no `href` fires `kai-home-link` with that entry when
|
|
147
|
+
* tapped, rather than navigating; one WITH `href` opens it directly
|
|
148
|
+
* (only when the URL passes the kit's own scheme allowlist). Omit for the
|
|
149
|
+
* no-home widget (chat view only, unchanged). */
|
|
150
|
+
home?: HomeConfig;
|
|
78
151
|
};
|
|
79
152
|
|
|
80
153
|
interface Events {
|
|
@@ -102,6 +175,19 @@ interface Events {
|
|
|
102
175
|
'kai-web-search': Record<string, never>;
|
|
103
176
|
/** The Mic / voice button was clicked. */
|
|
104
177
|
'kai-voice': Record<string, never>;
|
|
178
|
+
/** A conversation's history loaded: a row tap in the list, "new
|
|
179
|
+
* conversation," or the visitor's own mount-time auto-restore of their
|
|
180
|
+
* most recent thread (only fires when `conversations` is on and a `store`
|
|
181
|
+
* is set). `detail.id` is that conversation's id, `undefined` for the
|
|
182
|
+
* "new conversation" case (no id exists until the first message mints
|
|
183
|
+
* one, C-6). Set `el.messages = event.detail.messages` (already a fresh
|
|
184
|
+
* array) to actually render it, since this element does not do that for
|
|
185
|
+
* you; `messages` stays your own state like everywhere else on this
|
|
186
|
+
* element. */
|
|
187
|
+
'kai-conversation-load': { id: string | undefined; messages: ChatMessage[] };
|
|
188
|
+
/** A `home.links` entry with no `href` was activated (tapped/clicked/Enter).
|
|
189
|
+
* Meaningful only when `home` is set. */
|
|
190
|
+
'kai-home-link': { entry: HomeLinkEntry };
|
|
105
191
|
}
|
|
106
192
|
|
|
107
193
|
defineWebComponent<Props, Events>('kai-chat', {
|
|
@@ -111,7 +197,8 @@ defineWebComponent<Props, Events>('kai-chat', {
|
|
|
111
197
|
models: undefined, currentModel: undefined, context: undefined, scrollButton: true,
|
|
112
198
|
attach: true, webSearch: false, voice: false, triggers: undefined, kindIcons: undefined,
|
|
113
199
|
actionsReveal: 'always', cardTypes: undefined, cardSchemas: undefined, accept: undefined,
|
|
114
|
-
reasoning: undefined, reasoningOpen: undefined,
|
|
200
|
+
reasoning: undefined, reasoningOpen: undefined, conversations: false, store: undefined,
|
|
201
|
+
home: undefined,
|
|
115
202
|
}, (props, { dispatch, flag, reflectFlag, element, expose }) => {
|
|
116
203
|
// `messages` is an untyped boundary: a consumer can hand it anything at
|
|
117
204
|
// runtime (a pre-0.20.0 `{ id, role, content }` array, in particular). Skip
|
|
@@ -185,6 +272,11 @@ defineWebComponent<Props, Events>('kai-chat', {
|
|
|
185
272
|
actionsReveal={props.actionsReveal as 'always' | 'hover'}
|
|
186
273
|
cardTypes={cardComponentsFromTags(props.cardTypes as Record<string, string> | undefined, (props as { theme?: string }).theme)}
|
|
187
274
|
cardSchemas={props.cardSchemas as Record<string, object> | undefined}
|
|
275
|
+
conversations={flag('conversations')}
|
|
276
|
+
store={props.store as ConversationStore | undefined}
|
|
277
|
+
onConversationLoad={(messages, id) => dispatch('kai-conversation-load', { id, messages })}
|
|
278
|
+
home={props.home as HomeConfig | undefined}
|
|
279
|
+
onHomeLink={(entry) => dispatch('kai-home-link', { entry })}
|
|
188
280
|
/* F-26: card parts emit off THIS element as the bubbling `kai-card` event,
|
|
189
281
|
so `listenForCardEvents(el)` / addEventListener('kai-card') work. */
|
|
190
282
|
cardHostElement={element}
|