@xmesh/system-design 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +472 -0
- package/assets/brand-lockup-dark.svg +9 -0
- package/assets/brand-lockup-light.svg +9 -0
- package/assets/brand-mark.svg +9 -0
- package/colors_and_type.css +11 -0
- package/dist/lit/components/alert/index.css +201 -0
- package/dist/lit/components/alert/index.d.ts +25 -0
- package/dist/lit/components/alert/index.js +191 -0
- package/dist/lit/components/app-bar/index.css +80 -0
- package/dist/lit/components/app-bar/index.d.ts +19 -0
- package/dist/lit/components/app-bar/index.js +120 -0
- package/dist/lit/components/artifact/index.css +166 -0
- package/dist/lit/components/artifact/index.d.ts +37 -0
- package/dist/lit/components/artifact/index.js +294 -0
- package/dist/lit/components/autocomplete/index.css +171 -0
- package/dist/lit/components/autocomplete/index.d.ts +47 -0
- package/dist/lit/components/autocomplete/index.js +404 -0
- package/dist/lit/components/avatar/index.css +62 -0
- package/dist/lit/components/avatar/index.d.ts +19 -0
- package/dist/lit/components/avatar/index.js +112 -0
- package/dist/lit/components/avatar-group/index.css +60 -0
- package/dist/lit/components/avatar-group/index.d.ts +19 -0
- package/dist/lit/components/avatar-group/index.js +97 -0
- package/dist/lit/components/badge/index.css +72 -0
- package/dist/lit/components/badge/index.d.ts +18 -0
- package/dist/lit/components/badge/index.js +115 -0
- package/dist/lit/components/brand-mark/index.css +109 -0
- package/dist/lit/components/brand-mark/index.d.ts +24 -0
- package/dist/lit/components/brand-mark/index.js +116 -0
- package/dist/lit/components/breadcrumbs/index.css +91 -0
- package/dist/lit/components/breadcrumbs/index.d.ts +19 -0
- package/dist/lit/components/breadcrumbs/index.js +104 -0
- package/dist/lit/components/bubble/index.css +182 -0
- package/dist/lit/components/bubble/index.d.ts +72 -0
- package/dist/lit/components/bubble/index.js +617 -0
- package/dist/lit/components/button/index.css +342 -0
- package/dist/lit/components/button/index.d.ts +32 -0
- package/dist/lit/components/button/index.js +202 -0
- package/dist/lit/components/card/index.css +99 -0
- package/dist/lit/components/card/index.d.ts +20 -0
- package/dist/lit/components/card/index.js +133 -0
- package/dist/lit/components/chat/index.css +292 -0
- package/dist/lit/components/chat/index.d.ts +74 -0
- package/dist/lit/components/chat/index.js +589 -0
- package/dist/lit/components/checkbox/index.css +126 -0
- package/dist/lit/components/checkbox/index.d.ts +21 -0
- package/dist/lit/components/checkbox/index.js +138 -0
- package/dist/lit/components/chip/index.css +145 -0
- package/dist/lit/components/chip/index.d.ts +30 -0
- package/dist/lit/components/chip/index.js +230 -0
- package/dist/lit/components/chip-group/index.css +19 -0
- package/dist/lit/components/chip-group/index.d.ts +24 -0
- package/dist/lit/components/chip-group/index.js +171 -0
- package/dist/lit/components/code/index.css +42 -0
- package/dist/lit/components/code/index.d.ts +12 -0
- package/dist/lit/components/code/index.js +68 -0
- package/dist/lit/components/composer/index.css +548 -0
- package/dist/lit/components/composer/index.d.ts +67 -0
- package/dist/lit/components/composer/index.js +713 -0
- package/dist/lit/components/data-table/index.css +166 -0
- package/dist/lit/components/data-table/index.d.ts +55 -0
- package/dist/lit/components/data-table/index.js +390 -0
- package/dist/lit/components/dialog/index.css +124 -0
- package/dist/lit/components/dialog/index.d.ts +24 -0
- package/dist/lit/components/dialog/index.js +199 -0
- package/dist/lit/components/divider/index.css +27 -0
- package/dist/lit/components/divider/index.d.ts +13 -0
- package/dist/lit/components/divider/index.js +67 -0
- package/dist/lit/components/empty-state/index.css +69 -0
- package/dist/lit/components/empty-state/index.d.ts +21 -0
- package/dist/lit/components/empty-state/index.js +123 -0
- package/dist/lit/components/expansion-panel/index.css +120 -0
- package/dist/lit/components/expansion-panel/index.d.ts +22 -0
- package/dist/lit/components/expansion-panel/index.js +174 -0
- package/dist/lit/components/field/index.css +223 -0
- package/dist/lit/components/field/index.d.ts +106 -0
- package/dist/lit/components/field/index.js +388 -0
- package/dist/lit/components/file-input/index.css +257 -0
- package/dist/lit/components/file-input/index.d.ts +30 -0
- package/dist/lit/components/file-input/index.js +298 -0
- package/dist/lit/components/form/index.css +29 -0
- package/dist/lit/components/form/index.d.ts +38 -0
- package/dist/lit/components/form/index.js +192 -0
- package/dist/lit/components/grid/index.css +53 -0
- package/dist/lit/components/grid/index.d.ts +14 -0
- package/dist/lit/components/grid/index.js +82 -0
- package/dist/lit/components/kbd/index.css +35 -0
- package/dist/lit/components/kbd/index.d.ts +11 -0
- package/dist/lit/components/kbd/index.js +43 -0
- package/dist/lit/components/list/index.css +15 -0
- package/dist/lit/components/list/index.d.ts +28 -0
- package/dist/lit/components/list/index.js +188 -0
- package/dist/lit/components/list-item/index.css +119 -0
- package/dist/lit/components/list-item/index.d.ts +20 -0
- package/dist/lit/components/list-item/index.js +127 -0
- package/dist/lit/components/menu/index.css +94 -0
- package/dist/lit/components/menu/index.d.ts +47 -0
- package/dist/lit/components/menu/index.js +386 -0
- package/dist/lit/components/navigation-drawer/index.css +114 -0
- package/dist/lit/components/navigation-drawer/index.d.ts +29 -0
- package/dist/lit/components/navigation-drawer/index.js +218 -0
- package/dist/lit/components/overlay/index.css +171 -0
- package/dist/lit/components/overlay/index.d.ts +65 -0
- package/dist/lit/components/overlay/index.js +566 -0
- package/dist/lit/components/pagination/index.css +102 -0
- package/dist/lit/components/pagination/index.d.ts +22 -0
- package/dist/lit/components/pagination/index.js +184 -0
- package/dist/lit/components/primitives/index.css +504 -0
- package/dist/lit/components/primitives/index.d.ts +25 -0
- package/dist/lit/components/primitives/index.js +283 -0
- package/dist/lit/components/progress/index.css +143 -0
- package/dist/lit/components/progress/index.d.ts +23 -0
- package/dist/lit/components/progress/index.js +180 -0
- package/dist/lit/components/radio-group/index.css +178 -0
- package/dist/lit/components/radio-group/index.d.ts +35 -0
- package/dist/lit/components/radio-group/index.js +292 -0
- package/dist/lit/components/select/index.css +151 -0
- package/dist/lit/components/select/index.d.ts +50 -0
- package/dist/lit/components/select/index.js +390 -0
- package/dist/lit/components/sidebar-item/index.css +133 -0
- package/dist/lit/components/sidebar-item/index.d.ts +20 -0
- package/dist/lit/components/sidebar-item/index.js +105 -0
- package/dist/lit/components/skeleton/index.css +81 -0
- package/dist/lit/components/skeleton/index.d.ts +19 -0
- package/dist/lit/components/skeleton/index.js +119 -0
- package/dist/lit/components/slider/index.css +171 -0
- package/dist/lit/components/slider/index.d.ts +36 -0
- package/dist/lit/components/slider/index.js +302 -0
- package/dist/lit/components/snackbar/index.css +279 -0
- package/dist/lit/components/snackbar/index.d.ts +33 -0
- package/dist/lit/components/snackbar/index.js +195 -0
- package/dist/lit/components/stack/index.css +41 -0
- package/dist/lit/components/stack/index.d.ts +20 -0
- package/dist/lit/components/stack/index.js +103 -0
- package/dist/lit/components/switch/index.css +126 -0
- package/dist/lit/components/switch/index.d.ts +17 -0
- package/dist/lit/components/switch/index.js +116 -0
- package/dist/lit/components/table/index.css +85 -0
- package/dist/lit/components/table/index.d.ts +25 -0
- package/dist/lit/components/table/index.js +139 -0
- package/dist/lit/components/tabs/index.css +116 -0
- package/dist/lit/components/tabs/index.d.ts +49 -0
- package/dist/lit/components/tabs/index.js +320 -0
- package/dist/lit/components/text-field/index.css +90 -0
- package/dist/lit/components/text-field/index.d.ts +17 -0
- package/dist/lit/components/text-field/index.js +101 -0
- package/dist/lit/components/textarea/index.css +55 -0
- package/dist/lit/components/textarea/index.d.ts +26 -0
- package/dist/lit/components/textarea/index.js +124 -0
- package/dist/lit/components/tooltip/index.css +37 -0
- package/dist/lit/components/tooltip/index.d.ts +31 -0
- package/dist/lit/components/tooltip/index.js +196 -0
- package/dist/lit/components/validation/index.css +386 -0
- package/dist/lit/components/validation/index.d.ts +45 -0
- package/dist/lit/components/validation/index.js +318 -0
- package/dist/lit/index.d.ts +50 -0
- package/dist/lit/index.js +59 -0
- package/package.json +81 -0
- package/styles/README.md +346 -0
- package/styles/_elevation.css +24 -0
- package/styles/_fonts.css +6 -0
- package/styles/_layout.css +37 -0
- package/styles/_primitives.css +154 -0
- package/styles/_scroll.css +75 -0
- package/styles/_semantic.css +146 -0
- package/styles/_space.css +61 -0
- package/styles/_type.css +139 -0
- package/styles/_xmesh-extensions.css +232 -0
- package/styles/index.css +44 -0
- package/styles/md3/_color.css +102 -0
- package/styles/md3/_elevation.css +26 -0
- package/styles/md3/_motion.css +35 -0
- package/styles/md3/_shape.css +22 -0
- package/styles/md3/_state.css +22 -0
- package/styles/md3/_type.css +111 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
/*
|
|
2
|
+
chat/index.ts — Lit port of components/chat/index.jsx.
|
|
3
|
+
|
|
4
|
+
<xm-chat-shell> — full-viewport chat layout (sidebar + top + thread +
|
|
5
|
+
composer + optional artifact panel).
|
|
6
|
+
|
|
7
|
+
Properties (all set via JS — they're nested objects):
|
|
8
|
+
conversations Array<{ group: string, items: [{ id, title, active? }] }>
|
|
9
|
+
thread Array<{ from: 'user'|'bot', text?, html?, ts,
|
|
10
|
+
atts?, artifact? }>
|
|
11
|
+
`html` is a trusted HTML string (rendered via
|
|
12
|
+
unsafeHTML); `text` is plain text.
|
|
13
|
+
processing boolean — forwarded to the inner <xm-composer>
|
|
14
|
+
thinkingLabel string — forwarded to the inner <xm-composer>
|
|
15
|
+
topTitle string override (default "New Conversation")
|
|
16
|
+
|
|
17
|
+
Slots (host-level):
|
|
18
|
+
artifact-body rendered inside <xm-artifact> as the preview view
|
|
19
|
+
when an artifact is open
|
|
20
|
+
artifact-code rendered inside <xm-artifact> as the code view; the
|
|
21
|
+
panel's segmented eye/code toggle swaps between this
|
|
22
|
+
and artifact-body. Optional — omit for preview-only.
|
|
23
|
+
empty replaces the thread + composer area when set
|
|
24
|
+
(host owns its own composer in that case)
|
|
25
|
+
|
|
26
|
+
Events (all bubble, composed):
|
|
27
|
+
send detail: { text, attachments } — proxy from xm-composer
|
|
28
|
+
cancel
|
|
29
|
+
artifact-open detail: { artifact } — when a bubble's artifact chip
|
|
30
|
+
is clicked. The shell also handles this internally
|
|
31
|
+
(opens the artifact slot).
|
|
32
|
+
artifact-close
|
|
33
|
+
|
|
34
|
+
Light DOM. Loads the entire palette of dependent stylesheets at the
|
|
35
|
+
preview level. Composes the previously-ported xm-* elements.
|
|
36
|
+
*/
|
|
37
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
38
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
39
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
40
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
41
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
42
|
+
};
|
|
43
|
+
import { LitElement, html, svg, nothing } from "lit";
|
|
44
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
45
|
+
// Pull in dependencies (side-effect imports; each defines its own custom
|
|
46
|
+
// elements). The preview HTML can also `<script type="module">` them
|
|
47
|
+
// individually if it wants finer control over load order.
|
|
48
|
+
import "../primitives/index.js";
|
|
49
|
+
import "../brand-mark/index.js";
|
|
50
|
+
import "../button/index.js";
|
|
51
|
+
import "../sidebar-item/index.js";
|
|
52
|
+
import "../bubble/index.js";
|
|
53
|
+
import "../composer/index.js";
|
|
54
|
+
import "../artifact/index.js";
|
|
55
|
+
/* ---------- Inline icon SVG fragments (used in topbar / sidebar chrome) */
|
|
56
|
+
const I_PANEL = (size = 16) => svg `
|
|
57
|
+
<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
|
|
58
|
+
stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
|
|
59
|
+
stroke-linejoin="round" class="ds-icon">
|
|
60
|
+
<rect x="3" y="4" width="18" height="16" rx="2" />
|
|
61
|
+
<path d="M9 4v16" />
|
|
62
|
+
</svg>
|
|
63
|
+
`;
|
|
64
|
+
const I_PLUS = (size = 14) => svg `
|
|
65
|
+
<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
|
|
66
|
+
stroke="currentColor" stroke-width="2.2" stroke-linecap="round"
|
|
67
|
+
stroke-linejoin="round" class="ds-icon">
|
|
68
|
+
<path d="M12 5v14M5 12h14" />
|
|
69
|
+
</svg>
|
|
70
|
+
`;
|
|
71
|
+
const I_SEARCH = (size = 14) => svg `
|
|
72
|
+
<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
|
|
73
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
74
|
+
stroke-linejoin="round" class="ds-icon">
|
|
75
|
+
<circle cx="11" cy="11" r="7" />
|
|
76
|
+
<path d="M21 21l-4.3-4.3" />
|
|
77
|
+
</svg>
|
|
78
|
+
`;
|
|
79
|
+
const I_TRACE = (size = 17) => svg `
|
|
80
|
+
<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
|
|
81
|
+
stroke="currentColor" stroke-width="1.6" stroke-linecap="round"
|
|
82
|
+
stroke-linejoin="round" class="ds-icon">
|
|
83
|
+
<path d="M8 6V4a4 4 0 0 1 8 0v2" />
|
|
84
|
+
<rect x="6" y="6" width="12" height="13" rx="6" />
|
|
85
|
+
<path d="M12 11v8" />
|
|
86
|
+
<path d="M3 11h3" /><path d="M18 11h3" />
|
|
87
|
+
<path d="M3 18h3" /><path d="M18 18h3" />
|
|
88
|
+
<path d="M3 14h3" /><path d="M18 14h3" />
|
|
89
|
+
</svg>
|
|
90
|
+
`;
|
|
91
|
+
const I_SETTINGS = (size = 17) => svg `
|
|
92
|
+
<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
|
|
93
|
+
stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
|
|
94
|
+
stroke-linejoin="round" class="ds-icon">
|
|
95
|
+
<circle cx="12" cy="12" r="3" />
|
|
96
|
+
<path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3h.1a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8v.1a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z" />
|
|
97
|
+
</svg>
|
|
98
|
+
`;
|
|
99
|
+
const I_REGEN = (size = 16) => svg `
|
|
100
|
+
<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
|
|
101
|
+
stroke="currentColor" stroke-width="1.6" stroke-linecap="round"
|
|
102
|
+
stroke-linejoin="round" class="ds-icon">
|
|
103
|
+
<path d="M3 12a9 9 0 1 0 3-6.7" />
|
|
104
|
+
<path d="M3 4v5h5" />
|
|
105
|
+
</svg>
|
|
106
|
+
`;
|
|
107
|
+
const I_COPY = (size = 16) => svg `
|
|
108
|
+
<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
|
|
109
|
+
stroke="currentColor" stroke-width="1.6" stroke-linecap="round"
|
|
110
|
+
stroke-linejoin="round" class="ds-icon">
|
|
111
|
+
<rect x="9" y="9" width="11" height="11" rx="2" />
|
|
112
|
+
<path d="M5 15V5a2 2 0 0 1 2-2h10" />
|
|
113
|
+
</svg>
|
|
114
|
+
`;
|
|
115
|
+
const I_EDIT = (size = 16) => svg `
|
|
116
|
+
<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
|
|
117
|
+
stroke="currentColor" stroke-width="1.6" stroke-linecap="round"
|
|
118
|
+
stroke-linejoin="round" class="ds-icon">
|
|
119
|
+
<path d="M12 20h9" />
|
|
120
|
+
<path d="M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4 12.5-12.5z" />
|
|
121
|
+
</svg>
|
|
122
|
+
`;
|
|
123
|
+
const I_CHECK = (size = 16) => svg `
|
|
124
|
+
<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
|
|
125
|
+
stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
|
|
126
|
+
stroke-linejoin="round" class="ds-icon">
|
|
127
|
+
<polyline points="20 6 9 17 4 12" />
|
|
128
|
+
</svg>
|
|
129
|
+
`;
|
|
130
|
+
// Best-effort clipboard write. Async API first; falls back to a hidden
|
|
131
|
+
// textarea + execCommand for headless / non-secure-context environments
|
|
132
|
+
// (notably the screenshot harness).
|
|
133
|
+
const writeClipboard = async (text) => {
|
|
134
|
+
try {
|
|
135
|
+
if (navigator.clipboard?.writeText) {
|
|
136
|
+
await navigator.clipboard.writeText(text);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (_) { /* fall through to legacy path */ }
|
|
141
|
+
try {
|
|
142
|
+
const ta = document.createElement("textarea");
|
|
143
|
+
ta.value = text;
|
|
144
|
+
ta.setAttribute("readonly", "");
|
|
145
|
+
ta.style.position = "fixed";
|
|
146
|
+
ta.style.opacity = "0";
|
|
147
|
+
document.body.appendChild(ta);
|
|
148
|
+
ta.select();
|
|
149
|
+
const ok = document.execCommand("copy");
|
|
150
|
+
document.body.removeChild(ta);
|
|
151
|
+
return ok;
|
|
152
|
+
}
|
|
153
|
+
catch (_) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
// Strip HTML to plain text by parsing into a detached div. Used for
|
|
158
|
+
// thread messages whose body is authored as `html` — pasting raw HTML
|
|
159
|
+
// is rarely what the user wants.
|
|
160
|
+
const htmlToText = (html) => {
|
|
161
|
+
const div = document.createElement("div");
|
|
162
|
+
div.innerHTML = html;
|
|
163
|
+
return (div.textContent || "").trim();
|
|
164
|
+
};
|
|
165
|
+
let XmChatShell = class XmChatShell extends LitElement {
|
|
166
|
+
constructor() {
|
|
167
|
+
super(...arguments);
|
|
168
|
+
this.conversations = [];
|
|
169
|
+
this.thread = [];
|
|
170
|
+
this.processing = false;
|
|
171
|
+
this.thinkingLabel = "";
|
|
172
|
+
this.topTitle = "New Conversation";
|
|
173
|
+
/** Pin the shell to the viewport (height: 100vh). Default is height: 100%
|
|
174
|
+
so the shell sizes to its container — embedded hosts get correct
|
|
175
|
+
sizing for free. Full-bleed previews opt in via the `full-viewport`
|
|
176
|
+
attribute. */
|
|
177
|
+
this.fullViewport = false;
|
|
178
|
+
this._collapsed = false;
|
|
179
|
+
this._activeId = null;
|
|
180
|
+
this._openArtifact = null;
|
|
181
|
+
this._hasEmpty = false;
|
|
182
|
+
this._searchValue = "";
|
|
183
|
+
this._copiedIdx = null;
|
|
184
|
+
this._copiedTimer = null;
|
|
185
|
+
this._artifactBodyNodes = [];
|
|
186
|
+
this._artifactCodeNodes = [];
|
|
187
|
+
this._emptyNodes = [];
|
|
188
|
+
this._toggleCollapsed = () => { this._collapsed = !this._collapsed; };
|
|
189
|
+
this._selectConv = (id) => { this._activeId = id; };
|
|
190
|
+
this._onSearchInput = (e) => {
|
|
191
|
+
this._searchValue = e.target.value;
|
|
192
|
+
};
|
|
193
|
+
this._onSearchClear = () => { this._searchValue = ""; };
|
|
194
|
+
this._onArtifactOpen = (e) => {
|
|
195
|
+
const detail = e.detail;
|
|
196
|
+
this._openArtifact = detail?.artifact ?? null;
|
|
197
|
+
this.dispatchEvent(new CustomEvent("artifact-open", {
|
|
198
|
+
detail: { artifact: this._openArtifact },
|
|
199
|
+
bubbles: true, composed: true,
|
|
200
|
+
}));
|
|
201
|
+
};
|
|
202
|
+
this._onArtifactClose = () => {
|
|
203
|
+
this._openArtifact = null;
|
|
204
|
+
this.dispatchEvent(new CustomEvent("artifact-close", {
|
|
205
|
+
bubbles: true, composed: true,
|
|
206
|
+
}));
|
|
207
|
+
};
|
|
208
|
+
this._onComposerSend = (e) => {
|
|
209
|
+
const detail = e.detail;
|
|
210
|
+
this.dispatchEvent(new CustomEvent("send", {
|
|
211
|
+
detail, bubbles: true, composed: true,
|
|
212
|
+
}));
|
|
213
|
+
};
|
|
214
|
+
this._onComposerCancel = () => {
|
|
215
|
+
this.dispatchEvent(new CustomEvent("cancel", {
|
|
216
|
+
bubbles: true, composed: true,
|
|
217
|
+
}));
|
|
218
|
+
};
|
|
219
|
+
this._onCopyMessage = async (e, idx) => {
|
|
220
|
+
// Stop the bubble-actions delegated listener (added by xm-bubble-actions
|
|
221
|
+
// for the static-preview path) from also firing.
|
|
222
|
+
e.stopPropagation();
|
|
223
|
+
const m = (this.thread || [])[idx];
|
|
224
|
+
if (!m)
|
|
225
|
+
return;
|
|
226
|
+
const text = m.text ? m.text : (m.html ? htmlToText(m.html) : "");
|
|
227
|
+
if (!text)
|
|
228
|
+
return;
|
|
229
|
+
const ok = await writeClipboard(text);
|
|
230
|
+
if (!ok)
|
|
231
|
+
return;
|
|
232
|
+
this._copiedIdx = idx;
|
|
233
|
+
if (this._copiedTimer != null)
|
|
234
|
+
window.clearTimeout(this._copiedTimer);
|
|
235
|
+
this._copiedTimer = window.setTimeout(() => {
|
|
236
|
+
this._copiedIdx = null;
|
|
237
|
+
this._copiedTimer = null;
|
|
238
|
+
}, 1200);
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
createRenderRoot() {
|
|
242
|
+
return this;
|
|
243
|
+
}
|
|
244
|
+
connectedCallback() {
|
|
245
|
+
super.connectedCallback();
|
|
246
|
+
// Snapshot any authored slot="artifact-body" / slot="artifact-code" /
|
|
247
|
+
// slot="empty" content so lit's render doesn't replace them. We mount
|
|
248
|
+
// them imperatively in updated().
|
|
249
|
+
const ab = [];
|
|
250
|
+
const ac = [];
|
|
251
|
+
const em = [];
|
|
252
|
+
for (const child of Array.from(this.childNodes)) {
|
|
253
|
+
if (child.nodeType !== Node.ELEMENT_NODE) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const slot = child.getAttribute("slot");
|
|
257
|
+
if (slot === "artifact-body")
|
|
258
|
+
ab.push(child);
|
|
259
|
+
else if (slot === "artifact-code")
|
|
260
|
+
ac.push(child);
|
|
261
|
+
else if (slot === "empty")
|
|
262
|
+
em.push(child);
|
|
263
|
+
}
|
|
264
|
+
for (const n of [...ab, ...ac, ...em])
|
|
265
|
+
n.remove();
|
|
266
|
+
this._artifactBodyNodes = ab;
|
|
267
|
+
this._artifactCodeNodes = ac;
|
|
268
|
+
this._emptyNodes = em;
|
|
269
|
+
this._hasEmpty = em.length > 0;
|
|
270
|
+
// Initialize active conversation from .active flag
|
|
271
|
+
if (Array.isArray(this.conversations)) {
|
|
272
|
+
for (const g of this.conversations) {
|
|
273
|
+
const match = g.items?.find((i) => i.active);
|
|
274
|
+
if (match) {
|
|
275
|
+
this._activeId = match.id;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
willUpdate(changed) {
|
|
282
|
+
// Re-init active id if conversations were re-set after upgrade.
|
|
283
|
+
if (changed.has("conversations") && this._activeId == null && Array.isArray(this.conversations)) {
|
|
284
|
+
for (const g of this.conversations) {
|
|
285
|
+
const match = g.items?.find((i) => i.active);
|
|
286
|
+
if (match) {
|
|
287
|
+
this._activeId = match.id;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
updated() {
|
|
294
|
+
// Pin thread to bottom on render (mirrors useEffect on [thread, processing]).
|
|
295
|
+
const threadEl = this.querySelector(".chat-shell__thread");
|
|
296
|
+
if (threadEl)
|
|
297
|
+
threadEl.scrollTop = threadEl.scrollHeight;
|
|
298
|
+
// Mount empty content
|
|
299
|
+
const emptyMount = this.querySelector('[data-mount="empty"]');
|
|
300
|
+
if (emptyMount && this._emptyNodes.length && emptyMount.childNodes.length === 0) {
|
|
301
|
+
for (const n of this._emptyNodes)
|
|
302
|
+
emptyMount.appendChild(n);
|
|
303
|
+
}
|
|
304
|
+
// Hand the artifact panel its preview/code body nodes via the public
|
|
305
|
+
// setBodies() API. This replaces any self-authored bodies and triggers
|
|
306
|
+
// a re-render so the active view mounts. We re-call on every chat-shell
|
|
307
|
+
// update so a freshly-mounted <xm-artifact> (after open/close/reopen)
|
|
308
|
+
// always lands with the right content.
|
|
309
|
+
if (this._openArtifact) {
|
|
310
|
+
const art = this.querySelector("xm-artifact");
|
|
311
|
+
if (art?.setBodies && (this._artifactBodyNodes.length || this._artifactCodeNodes.length)) {
|
|
312
|
+
art.setBodies(this._artifactBodyNodes, this._artifactCodeNodes);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/* ---------- sidebar render ---------- */
|
|
317
|
+
_renderSidebar() {
|
|
318
|
+
const collapsed = this._collapsed;
|
|
319
|
+
const groups = this.conversations || [];
|
|
320
|
+
return html `
|
|
321
|
+
<aside class="chat-sidebar" aria-label="Conversations">
|
|
322
|
+
${collapsed
|
|
323
|
+
? html `<xm-button
|
|
324
|
+
class="chat-sidebar__top"
|
|
325
|
+
variant="ghost"
|
|
326
|
+
icon-only
|
|
327
|
+
aria-label="Expand sidebar"
|
|
328
|
+
title="Expand sidebar"
|
|
329
|
+
@click=${this._toggleCollapsed}
|
|
330
|
+
>${I_PANEL()}</xm-button>`
|
|
331
|
+
: html `<xm-button
|
|
332
|
+
class="chat-sidebar__top"
|
|
333
|
+
variant="ghost"
|
|
334
|
+
full-width
|
|
335
|
+
aria-label="Collapse sidebar"
|
|
336
|
+
title="Collapse sidebar"
|
|
337
|
+
@click=${this._toggleCollapsed}
|
|
338
|
+
>
|
|
339
|
+
<xm-brand-mark size="sm" show-wordmark></xm-brand-mark>
|
|
340
|
+
<span slot="icon-right">${I_PANEL()}</span>
|
|
341
|
+
</xm-button>`}
|
|
342
|
+
|
|
343
|
+
${collapsed
|
|
344
|
+
? html `<xm-button
|
|
345
|
+
class="chat-sidebar__new"
|
|
346
|
+
variant="ghost"
|
|
347
|
+
icon-only
|
|
348
|
+
aria-label="New chat"
|
|
349
|
+
title="New chat"
|
|
350
|
+
>${I_PLUS()}</xm-button>`
|
|
351
|
+
: html `<xm-button
|
|
352
|
+
class="chat-sidebar__new"
|
|
353
|
+
variant="ghost"
|
|
354
|
+
full-width
|
|
355
|
+
aria-label="New chat"
|
|
356
|
+
>
|
|
357
|
+
<span slot="icon-left">${I_PLUS()}</span>
|
|
358
|
+
New chat
|
|
359
|
+
<span slot="icon-right" class="ds-kbd">⌘N</span>
|
|
360
|
+
</xm-button>`}
|
|
361
|
+
|
|
362
|
+
${!collapsed
|
|
363
|
+
? html `
|
|
364
|
+
<div class="chat-sidebar__search chat-sidebar__search--full">
|
|
365
|
+
<span class="ds-search ds-search--on-canvas">
|
|
366
|
+
<span class="ds-search__icon" aria-hidden="true">${I_SEARCH(13)}</span>
|
|
367
|
+
<input
|
|
368
|
+
type="text"
|
|
369
|
+
class="ds-search__input"
|
|
370
|
+
.value=${this._searchValue}
|
|
371
|
+
placeholder="Search conversations"
|
|
372
|
+
spellcheck="false"
|
|
373
|
+
@input=${this._onSearchInput}
|
|
374
|
+
/>
|
|
375
|
+
${this._searchValue
|
|
376
|
+
? html `<button
|
|
377
|
+
type="button"
|
|
378
|
+
class="ds-search__clear"
|
|
379
|
+
aria-label="Clear search"
|
|
380
|
+
title="Clear search"
|
|
381
|
+
@click=${this._onSearchClear}
|
|
382
|
+
>${svg `
|
|
383
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none"
|
|
384
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
385
|
+
stroke-linejoin="round" class="ds-icon">
|
|
386
|
+
<path d="M6 6l12 12M18 6L6 18" />
|
|
387
|
+
</svg>`}</button>`
|
|
388
|
+
: nothing}
|
|
389
|
+
</span>
|
|
390
|
+
</div>
|
|
391
|
+
`
|
|
392
|
+
: html `
|
|
393
|
+
<xm-button
|
|
394
|
+
variant="ghost"
|
|
395
|
+
icon-only
|
|
396
|
+
class="chat-sidebar__search--icon-only"
|
|
397
|
+
aria-label="Search conversations"
|
|
398
|
+
title="Search conversations"
|
|
399
|
+
>${I_SEARCH(14)}</xm-button>
|
|
400
|
+
`}
|
|
401
|
+
|
|
402
|
+
<nav class="chat-sidebar__list scroll-canvas" aria-label="Recent chats">
|
|
403
|
+
${collapsed
|
|
404
|
+
? this._renderCollapsedList(groups)
|
|
405
|
+
: this._renderExpandedList(groups)}
|
|
406
|
+
</nav>
|
|
407
|
+
</aside>
|
|
408
|
+
`;
|
|
409
|
+
}
|
|
410
|
+
_renderCollapsedList(groups) {
|
|
411
|
+
const all = groups.flatMap((g) => g.items || []);
|
|
412
|
+
return all.map((c, i) => html `
|
|
413
|
+
<xm-sidebar-item
|
|
414
|
+
collapsed
|
|
415
|
+
?active=${this._activeId === c.id}
|
|
416
|
+
title="${c.title}"
|
|
417
|
+
@click=${() => this._selectConv(c.id)}
|
|
418
|
+
></xm-sidebar-item>
|
|
419
|
+
${i === 2 ? html `<div class="chat-sidebar__divider" aria-hidden="true"></div>` : nothing}
|
|
420
|
+
`);
|
|
421
|
+
}
|
|
422
|
+
_renderExpandedList(groups) {
|
|
423
|
+
return groups.map((group) => html `
|
|
424
|
+
<div class="sidebar-item sidebar-item--row sidebar-item--eyebrow">${group.group}</div>
|
|
425
|
+
${(group.items || []).map((c) => html `
|
|
426
|
+
<xm-sidebar-item
|
|
427
|
+
layout="stacked"
|
|
428
|
+
?active=${this._activeId === c.id}
|
|
429
|
+
title="${c.title}"
|
|
430
|
+
@click=${() => this._selectConv(c.id)}
|
|
431
|
+
></xm-sidebar-item>
|
|
432
|
+
`)}
|
|
433
|
+
`);
|
|
434
|
+
}
|
|
435
|
+
/* ---------- topbar render ---------- */
|
|
436
|
+
_renderTopBar() {
|
|
437
|
+
const showSubtitle = !this._hasEmpty;
|
|
438
|
+
return html `
|
|
439
|
+
<header class="chat-shell__top">
|
|
440
|
+
<div class="chat-shell__top-left">
|
|
441
|
+
<span class="chat-shell__top-title">${this.topTitle || "New Conversation"}</span>
|
|
442
|
+
${showSubtitle
|
|
443
|
+
? html `
|
|
444
|
+
<span class="chat-shell__top-sub">
|
|
445
|
+
<span>${(this.thread || []).length} messages</span>
|
|
446
|
+
<span class="ds-sep">·</span>
|
|
447
|
+
<span>updated now</span>
|
|
448
|
+
</span>
|
|
449
|
+
`
|
|
450
|
+
: nothing}
|
|
451
|
+
</div>
|
|
452
|
+
<div class="chat-shell__top-right">
|
|
453
|
+
<xm-button variant="ghost" icon-only aria-label="Open debug panel" title="Open debug panel">${I_TRACE()}</xm-button>
|
|
454
|
+
<xm-button variant="ghost" icon-only aria-label="Conversation settings" title="Conversation settings">${I_SETTINGS()}</xm-button>
|
|
455
|
+
</div>
|
|
456
|
+
</header>
|
|
457
|
+
`;
|
|
458
|
+
}
|
|
459
|
+
/* ---------- bubble row render ----------
|
|
460
|
+
Bubbles receive their body via `.text=` or `.bodyHtml=` properties.
|
|
461
|
+
This avoids the lit-marker-vs-snapshot race that would happen if
|
|
462
|
+
we put text content between <xm-bubble>...</xm-bubble> in the
|
|
463
|
+
parent template — the bubble's own template controls its body
|
|
464
|
+
so lit's diffing stays consistent across re-renders. */
|
|
465
|
+
_renderBubbleRow(m, idx) {
|
|
466
|
+
const isUser = m.from === "user";
|
|
467
|
+
const variant = isUser ? "user" : "bot";
|
|
468
|
+
const copied = this._copiedIdx === idx;
|
|
469
|
+
return html `
|
|
470
|
+
<div class="chat-shell__row chat-shell__row--${variant}">
|
|
471
|
+
<xm-bubble-group variant="${variant}">
|
|
472
|
+
<xm-bubble
|
|
473
|
+
variant="${variant}"
|
|
474
|
+
.attachments=${m.atts || []}
|
|
475
|
+
.artifact=${m.artifact || null}
|
|
476
|
+
?download=${!!m.artifact}
|
|
477
|
+
.text=${m.html ? "" : (m.text || "")}
|
|
478
|
+
.bodyHtml=${m.html || ""}
|
|
479
|
+
@artifact-open=${this._onArtifactOpen}
|
|
480
|
+
></xm-bubble>
|
|
481
|
+
<xm-bubble-actions ts="${m.ts || ""}">
|
|
482
|
+
<xm-button variant="ghost-canvas" icon-only size="sm" aria-label="Regenerate">${I_REGEN()}</xm-button>
|
|
483
|
+
${!isUser
|
|
484
|
+
? html `<xm-button variant="ghost-canvas" icon-only size="sm" aria-label="View trace">${I_TRACE(16)}</xm-button>`
|
|
485
|
+
: html `<xm-button variant="ghost-canvas" icon-only size="sm" aria-label="Edit message">${I_EDIT()}</xm-button>`}
|
|
486
|
+
<xm-button
|
|
487
|
+
variant="ghost-canvas"
|
|
488
|
+
icon-only
|
|
489
|
+
size="sm"
|
|
490
|
+
aria-label="${copied ? "Copied" : "Copy message"}"
|
|
491
|
+
title="${copied ? "Copied" : "Copy message"}"
|
|
492
|
+
@click=${(e) => this._onCopyMessage(e, idx)}
|
|
493
|
+
>${copied ? I_CHECK() : I_COPY()}</xm-button>
|
|
494
|
+
</xm-bubble-actions>
|
|
495
|
+
</xm-bubble-group>
|
|
496
|
+
</div>
|
|
497
|
+
`;
|
|
498
|
+
}
|
|
499
|
+
/* ---------- shell render ---------- */
|
|
500
|
+
render() {
|
|
501
|
+
const cls = [
|
|
502
|
+
"chat-shell",
|
|
503
|
+
this.fullViewport ? "chat-shell--full-viewport" : "",
|
|
504
|
+
this._collapsed ? "chat-shell--collapsed" : "",
|
|
505
|
+
this._openArtifact ? "chat-shell--has-artifact" : "",
|
|
506
|
+
].filter(Boolean).join(" ");
|
|
507
|
+
const showEmpty = this._hasEmpty;
|
|
508
|
+
const thread = this.thread || [];
|
|
509
|
+
return html `
|
|
510
|
+
<div class="${cls}">
|
|
511
|
+
${this._renderSidebar()}
|
|
512
|
+
${this._renderTopBar()}
|
|
513
|
+
<div class="chat-shell__center">
|
|
514
|
+
${showEmpty
|
|
515
|
+
? html `<div data-mount="empty" style="display:contents"></div>`
|
|
516
|
+
: html `
|
|
517
|
+
<div class="chat-shell__thread scroll-canvas">
|
|
518
|
+
<div class="chat-shell__thread-inner">
|
|
519
|
+
${thread.map((m, i) => this._renderBubbleRow(m, i))}
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
<div class="chat-shell__composer">
|
|
523
|
+
<xm-composer
|
|
524
|
+
?processing=${this.processing}
|
|
525
|
+
thinking-label="${this.thinkingLabel}"
|
|
526
|
+
show-attachments
|
|
527
|
+
.initialFocus=${false}
|
|
528
|
+
@send=${this._onComposerSend}
|
|
529
|
+
@cancel=${this._onComposerCancel}
|
|
530
|
+
></xm-composer>
|
|
531
|
+
</div>
|
|
532
|
+
`}
|
|
533
|
+
</div>
|
|
534
|
+
${this._openArtifact
|
|
535
|
+
? html `
|
|
536
|
+
<aside class="chat-shell__artifact-slot" aria-label="Artifact viewer">
|
|
537
|
+
<xm-artifact
|
|
538
|
+
title="${this._openArtifact.title || ""}"
|
|
539
|
+
.sub=${this._openArtifact.sub || []}
|
|
540
|
+
show-refresh
|
|
541
|
+
show-close
|
|
542
|
+
@close=${this._onArtifactClose}
|
|
543
|
+
></xm-artifact>
|
|
544
|
+
</aside>
|
|
545
|
+
`
|
|
546
|
+
: nothing}
|
|
547
|
+
</div>
|
|
548
|
+
`;
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
__decorate([
|
|
552
|
+
property({ attribute: false })
|
|
553
|
+
], XmChatShell.prototype, "conversations", void 0);
|
|
554
|
+
__decorate([
|
|
555
|
+
property({ attribute: false })
|
|
556
|
+
], XmChatShell.prototype, "thread", void 0);
|
|
557
|
+
__decorate([
|
|
558
|
+
property({ type: Boolean })
|
|
559
|
+
], XmChatShell.prototype, "processing", void 0);
|
|
560
|
+
__decorate([
|
|
561
|
+
property({ type: String, attribute: "thinking-label" })
|
|
562
|
+
], XmChatShell.prototype, "thinkingLabel", void 0);
|
|
563
|
+
__decorate([
|
|
564
|
+
property({ type: String, attribute: "top-title" })
|
|
565
|
+
], XmChatShell.prototype, "topTitle", void 0);
|
|
566
|
+
__decorate([
|
|
567
|
+
property({ type: Boolean, attribute: "full-viewport" })
|
|
568
|
+
], XmChatShell.prototype, "fullViewport", void 0);
|
|
569
|
+
__decorate([
|
|
570
|
+
state()
|
|
571
|
+
], XmChatShell.prototype, "_collapsed", void 0);
|
|
572
|
+
__decorate([
|
|
573
|
+
state()
|
|
574
|
+
], XmChatShell.prototype, "_activeId", void 0);
|
|
575
|
+
__decorate([
|
|
576
|
+
state()
|
|
577
|
+
], XmChatShell.prototype, "_openArtifact", void 0);
|
|
578
|
+
__decorate([
|
|
579
|
+
state()
|
|
580
|
+
], XmChatShell.prototype, "_hasEmpty", void 0);
|
|
581
|
+
__decorate([
|
|
582
|
+
state()
|
|
583
|
+
], XmChatShell.prototype, "_searchValue", void 0);
|
|
584
|
+
__decorate([
|
|
585
|
+
state()
|
|
586
|
+
], XmChatShell.prototype, "_copiedIdx", void 0);
|
|
587
|
+
XmChatShell = __decorate([
|
|
588
|
+
customElement("xm-chat-shell")
|
|
589
|
+
], XmChatShell);
|