@kodaris/krubble-app-components 1.0.124 → 1.0.125
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/chatbot.d.ts +70 -8
- package/dist/chatbot.d.ts.map +1 -1
- package/dist/chatbot.js +496 -40
- package/dist/chatbot.js.map +1 -1
- package/dist/krubble-app.bundled.js +496 -40
- package/dist/krubble-app.bundled.js.map +1 -1
- package/dist/krubble-app.bundled.min.js +249 -48
- package/dist/krubble-app.bundled.min.js.map +1 -1
- package/dist/krubble-app.umd.js +496 -40
- package/dist/krubble-app.umd.js.map +1 -1
- package/dist/krubble-app.umd.min.js +235 -34
- package/dist/krubble-app.umd.min.js.map +1 -1
- package/package.json +1 -1
package/dist/chatbot.js
CHANGED
|
@@ -296,6 +296,14 @@ export class KRChatbot extends LitElement {
|
|
|
296
296
|
this._inputValue = '';
|
|
297
297
|
this._isStreaming = false;
|
|
298
298
|
this._error = '';
|
|
299
|
+
/**
|
|
300
|
+
* The current conversation's title. In the floating widget it replaces the
|
|
301
|
+
* static header label so the user sees which conversation they're in. Set from
|
|
302
|
+
* the loaded/switched conversation and from the live `CONVERSATION_TITLE` event
|
|
303
|
+
* (the server generates a title at the end of a new conversation's first turn);
|
|
304
|
+
* empty for a brand-new conversation, where the header falls back to `label`.
|
|
305
|
+
*/
|
|
306
|
+
this._conversationTitle = '';
|
|
299
307
|
/**
|
|
300
308
|
* User-facing context warning text (`CONVERSATION_TOKEN_WARNING`). Shown as a
|
|
301
309
|
* dismissible banner in all variants. Carries only friendly guidance, no numbers.
|
|
@@ -355,12 +363,23 @@ export class KRChatbot extends LitElement {
|
|
|
355
363
|
*/
|
|
356
364
|
this._drawerOpen = false;
|
|
357
365
|
/**
|
|
358
|
-
* Which view the drawer shows — settings, debug, or a single tool
|
|
359
|
-
* detail. One drawer holds all
|
|
360
|
-
* and clicking a tool line switches to `toolDetail`. The
|
|
361
|
-
* inside. Never null, so the content stays visible while the
|
|
366
|
+
* Which view the drawer shows — settings, debug, history, or a single tool
|
|
367
|
+
* call's full detail. One drawer holds them all; header buttons switch
|
|
368
|
+
* settings/debug/history and clicking a tool line switches to `toolDetail`. The
|
|
369
|
+
* content/width swap inside. Never null, so the content stays visible while the
|
|
370
|
+
* drawer slides shut. `history` is floating-only (the previous-conversations
|
|
371
|
+
* picker); in floating the drawer fills the panel over the transcript.
|
|
362
372
|
*/
|
|
363
373
|
this._drawerView = 'settings';
|
|
374
|
+
/**
|
|
375
|
+
* The user's previous conversations, loaded from `/conversation/list` when the
|
|
376
|
+
* history view opens. Each is the raw ConversationViewDto (we read conversationID,
|
|
377
|
+
* title, lastModified). Empty until the first open; reloaded on each open so a
|
|
378
|
+
* freshly-created or renamed conversation shows.
|
|
379
|
+
*/
|
|
380
|
+
this._conversations = [];
|
|
381
|
+
/** True while the history list is being fetched (drives a loading state). */
|
|
382
|
+
this._conversationsLoading = false;
|
|
364
383
|
/**
|
|
365
384
|
* The tool call whose full input/output the `toolDetail` drawer view shows —
|
|
366
385
|
* the clicked `tool_use` message and its paired `tool_result` message, stored
|
|
@@ -604,6 +623,11 @@ export class KRChatbot extends LitElement {
|
|
|
604
623
|
// --- Lifecycle ---
|
|
605
624
|
connectedCallback() {
|
|
606
625
|
super.connectedCallback();
|
|
626
|
+
// Capture the consumer's targeting BEFORE we default `load` below: a custom
|
|
627
|
+
// `load` or an explicitly-set `conversation-id` means the consumer owns which
|
|
628
|
+
// conversation shows, so the tab-scoped resume must not override it.
|
|
629
|
+
const usesBuiltInLoad = !this.load;
|
|
630
|
+
const consumerSetConversationId = this.conversationId != null;
|
|
607
631
|
// Default to loading a conversation's messages when the consumer hasn't
|
|
608
632
|
// provided their own `load`. With `conversationId` set we rebuild that
|
|
609
633
|
// conversation; with none set the user wants a NEW conversation, so we start
|
|
@@ -686,7 +710,54 @@ export class KRChatbot extends LitElement {
|
|
|
686
710
|
})));
|
|
687
711
|
};
|
|
688
712
|
}
|
|
689
|
-
|
|
713
|
+
// Tab-scoped resume (floating only): a reload should redisplay the exact
|
|
714
|
+
// conversation this tab was on, and a brand-new tab should reopen the user's
|
|
715
|
+
// most-recent one. The pinned id lives in sessionStorage (per-tab — unlike
|
|
716
|
+
// localStorage it isn't shared across tabs, so two tabs can sit on different
|
|
717
|
+
// conversations). We resolve the id FIRST, then run the built-in load so it
|
|
718
|
+
// rebuilds the right transcript. Skipped when the consumer owns targeting (a
|
|
719
|
+
// custom `load` or an explicit `conversation-id`) or in inline mode (embedded
|
|
720
|
+
// as a page — the host route, not the tab, decides the conversation).
|
|
721
|
+
const resolveConversationId = () => {
|
|
722
|
+
if (this.variant !== 'floating' || !usesBuiltInLoad || consumerSetConversationId) {
|
|
723
|
+
return Promise.resolve();
|
|
724
|
+
}
|
|
725
|
+
const stored = Number(sessionStorage.getItem('kr-chatbot-conversation-id'));
|
|
726
|
+
if (stored > 0) {
|
|
727
|
+
this.conversationId = stored;
|
|
728
|
+
// Fetch the conversation so the header shows its title on reload (the
|
|
729
|
+
// transcript load doesn't carry it). Best-effort — the header falls back
|
|
730
|
+
// to the default label if this fails.
|
|
731
|
+
return KRHttp.fetch({ url: `/api/system/conversation/${stored}` })
|
|
732
|
+
.then((r) => r.json())
|
|
733
|
+
.then((res) => {
|
|
734
|
+
this._conversationTitle = res?.data?.title || '';
|
|
735
|
+
})
|
|
736
|
+
.catch(() => { });
|
|
737
|
+
}
|
|
738
|
+
// Fresh tab, nothing pinned yet — resume the user's most recent conversation
|
|
739
|
+
// (404 when they have none, which leaves us blank for a new one).
|
|
740
|
+
return KRHttp.fetch({ url: '/api/system/conversation/last' })
|
|
741
|
+
.then((r) => {
|
|
742
|
+
if (!r.ok) {
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
return r.json().then((res) => {
|
|
746
|
+
const id = res?.data?.conversationID;
|
|
747
|
+
if (typeof id === 'number') {
|
|
748
|
+
this.conversationId = id;
|
|
749
|
+
this._conversationTitle = res?.data?.title || '';
|
|
750
|
+
this._persistConversationId();
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
})
|
|
754
|
+
.catch(() => { });
|
|
755
|
+
};
|
|
756
|
+
// `this.load` is always set by here (defaulted above when the consumer didn't
|
|
757
|
+
// supply one), so capture it for the async chain — TS can't narrow the nullable
|
|
758
|
+
// property across the promise boundary.
|
|
759
|
+
const load = this.load;
|
|
760
|
+
resolveConversationId().then(() => load()).then((messages) => {
|
|
690
761
|
if (messages && messages.length) {
|
|
691
762
|
this.messages = messages;
|
|
692
763
|
}
|
|
@@ -928,12 +999,31 @@ export class KRChatbot extends LitElement {
|
|
|
928
999
|
}
|
|
929
1000
|
this._saveState();
|
|
930
1001
|
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Persist the pinned conversation id per-tab so a reload redisplays this exact
|
|
1004
|
+
* conversation. sessionStorage (not localStorage) is deliberate: it's scoped to
|
|
1005
|
+
* the tab, so two tabs can sit on different conversations without clobbering
|
|
1006
|
+
* each other. Only meaningful for the floating widget's built-in flow — inline
|
|
1007
|
+
* embeds and custom-`load` consumers own their own targeting.
|
|
1008
|
+
*/
|
|
1009
|
+
_persistConversationId() {
|
|
1010
|
+
try {
|
|
1011
|
+
if (this.conversationId != null) {
|
|
1012
|
+
sessionStorage.setItem('kr-chatbot-conversation-id', String(this.conversationId));
|
|
1013
|
+
}
|
|
1014
|
+
else {
|
|
1015
|
+
sessionStorage.removeItem('kr-chatbot-conversation-id');
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
catch { /* sessionStorage not available */ }
|
|
1019
|
+
}
|
|
931
1020
|
_saveState() {
|
|
932
1021
|
try {
|
|
933
|
-
// `toolDetail`
|
|
934
|
-
// real preference views so a reload never reopens a stale tool
|
|
1022
|
+
// `toolDetail` and `history` are transient views, not saved ones — persist
|
|
1023
|
+
// only the real preference views so a reload never reopens a stale tool or a
|
|
1024
|
+
// stale conversation list.
|
|
935
1025
|
let persistedDrawerView = this._drawerView;
|
|
936
|
-
if (persistedDrawerView === 'toolDetail') {
|
|
1026
|
+
if (persistedDrawerView === 'toolDetail' || persistedDrawerView === 'history') {
|
|
937
1027
|
persistedDrawerView = 'settings';
|
|
938
1028
|
}
|
|
939
1029
|
const state = {
|
|
@@ -1938,6 +2028,8 @@ export class KRChatbot extends LitElement {
|
|
|
1938
2028
|
const id = conversation?.conversationID;
|
|
1939
2029
|
if (typeof id === 'number') {
|
|
1940
2030
|
this.conversationId = id;
|
|
2031
|
+
// Pin it to this tab so a reload redisplays this new conversation.
|
|
2032
|
+
this._persistConversationId();
|
|
1941
2033
|
// Announce the newly-created conversation so consumers can react
|
|
1942
2034
|
// (e.g. update the route from /chat/new to /chat/{id}). The full
|
|
1943
2035
|
// conversation object is passed, not just the id, so consumers
|
|
@@ -2135,9 +2227,111 @@ export class KRChatbot extends LitElement {
|
|
|
2135
2227
|
else {
|
|
2136
2228
|
this._drawerOpen = true;
|
|
2137
2229
|
this._drawerView = view;
|
|
2230
|
+
// Opening the history view refetches the user's previous conversations
|
|
2231
|
+
// (most-recently modified first) so a just-created or renamed conversation
|
|
2232
|
+
// shows without a reload. Ownership is enforced server-side (the endpoint
|
|
2233
|
+
// scopes to the current administrator), so we only filter to active ones.
|
|
2234
|
+
if (view === 'history') {
|
|
2235
|
+
this._conversationsLoading = true;
|
|
2236
|
+
KRHttp.fetch({
|
|
2237
|
+
url: '/api/system/conversation/list',
|
|
2238
|
+
method: 'POST',
|
|
2239
|
+
body: {
|
|
2240
|
+
page: 0,
|
|
2241
|
+
size: 50,
|
|
2242
|
+
sortBy: 'lastModified',
|
|
2243
|
+
sortDirection: 'desc',
|
|
2244
|
+
filterFields: [{
|
|
2245
|
+
name: 'active',
|
|
2246
|
+
operation: 'IS',
|
|
2247
|
+
value: 'true',
|
|
2248
|
+
}],
|
|
2249
|
+
},
|
|
2250
|
+
})
|
|
2251
|
+
.then((r) => r.json())
|
|
2252
|
+
.then((res) => {
|
|
2253
|
+
this._conversations = res?.data?.content || [];
|
|
2254
|
+
})
|
|
2255
|
+
.catch(() => {
|
|
2256
|
+
this._conversations = [];
|
|
2257
|
+
})
|
|
2258
|
+
.then(() => {
|
|
2259
|
+
this._conversationsLoading = false;
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
this._saveState();
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2266
|
+
* Switch the floating widget to a previously-created conversation (clicked in the
|
|
2267
|
+
* history list): stop any live turn, reset the transient turn/context state, pin
|
|
2268
|
+
* the new id (persisted per-tab so a reload stays here), then rebuild its
|
|
2269
|
+
* transcript via the built-in `load`. The drawer closes so the user lands back
|
|
2270
|
+
* in the conversation.
|
|
2271
|
+
*/
|
|
2272
|
+
_handleSwitchConversation(conversationID) {
|
|
2273
|
+
if (conversationID === this.conversationId) {
|
|
2274
|
+
this._handleCloseDrawer();
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
this._handleStop();
|
|
2278
|
+
this.conversationId = conversationID;
|
|
2279
|
+
// Adopt the picked conversation's title for the header (empty for an untitled
|
|
2280
|
+
// one, so the header falls back to the default label).
|
|
2281
|
+
this._conversationTitle = this._conversations.find((c) => c.conversationID === conversationID)?.title || '';
|
|
2282
|
+
this._persistConversationId();
|
|
2283
|
+
this._resetConversationState();
|
|
2284
|
+
this._drawerOpen = false;
|
|
2285
|
+
this._saveState();
|
|
2286
|
+
if (this.load) {
|
|
2287
|
+
this.load().then((messages) => {
|
|
2288
|
+
this.messages = messages || [];
|
|
2289
|
+
// A turn may still be streaming server-side for the conversation we just
|
|
2290
|
+
// opened (started in another tab/device) — attach to it if so.
|
|
2291
|
+
if (this.conversationId && !this._isStreaming) {
|
|
2292
|
+
this._reconnectStream();
|
|
2293
|
+
}
|
|
2294
|
+
}).catch(() => { });
|
|
2138
2295
|
}
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* Start a brand-new conversation (clicked in the history list): unpin the id (the
|
|
2299
|
+
* next turn sends `createNew` and the resolved id re-pins), clear the transcript
|
|
2300
|
+
* and transient state, and close the drawer back to an empty composer.
|
|
2301
|
+
*/
|
|
2302
|
+
_handleNewConversation() {
|
|
2303
|
+
this._handleStop();
|
|
2304
|
+
this.conversationId = null;
|
|
2305
|
+
// New conversation has no title yet — the header shows the default label until
|
|
2306
|
+
// the server generates one at the end of the first turn.
|
|
2307
|
+
this._conversationTitle = '';
|
|
2308
|
+
this._persistConversationId();
|
|
2309
|
+
this.messages = [];
|
|
2310
|
+
this._resetConversationState();
|
|
2311
|
+
this._drawerOpen = false;
|
|
2139
2312
|
this._saveState();
|
|
2140
2313
|
}
|
|
2314
|
+
/**
|
|
2315
|
+
* Clear the transient, per-conversation state that must not bleed across a
|
|
2316
|
+
* conversation switch/new — pending confirmations, context banners, errors, the
|
|
2317
|
+
* reconnect counter, and staged composer files. The transcript itself is set by
|
|
2318
|
+
* the caller (loaded for a switch, emptied for new).
|
|
2319
|
+
*/
|
|
2320
|
+
_resetConversationState() {
|
|
2321
|
+
this._pendingActions = [];
|
|
2322
|
+
this._inlineConfirm = true;
|
|
2323
|
+
this._contextWarning = '';
|
|
2324
|
+
this._contextLimitReached = false;
|
|
2325
|
+
this._contextLimitMessage = '';
|
|
2326
|
+
this._error = '';
|
|
2327
|
+
this._reconnectAttempts = 0;
|
|
2328
|
+
this._pendingConversationFiles.forEach((p) => {
|
|
2329
|
+
if (p.previewUrl) {
|
|
2330
|
+
URL.revokeObjectURL(p.previewUrl);
|
|
2331
|
+
}
|
|
2332
|
+
});
|
|
2333
|
+
this._pendingConversationFiles = [];
|
|
2334
|
+
}
|
|
2141
2335
|
/** Close the drawer. `_drawerView` is left intact so its content stays visible
|
|
2142
2336
|
* while the drawer slides shut. */
|
|
2143
2337
|
_handleCloseDrawer() {
|
|
@@ -2421,7 +2615,7 @@ export class KRChatbot extends LitElement {
|
|
|
2421
2615
|
// deliberately never show). Adopt it as the header label and pass it
|
|
2422
2616
|
// up so consumers can update their own chrome, e.g. a breadcrumb.
|
|
2423
2617
|
if (data.data?.title) {
|
|
2424
|
-
this.
|
|
2618
|
+
this._conversationTitle = data.data.title;
|
|
2425
2619
|
this.dispatchEvent(new CustomEvent('conversation-title', {
|
|
2426
2620
|
detail: { title: data.data.title },
|
|
2427
2621
|
bubbles: true,
|
|
@@ -3007,10 +3201,75 @@ export class KRChatbot extends LitElement {
|
|
|
3007
3201
|
* lifetime — plus a verbose per-iteration table. Never customer-facing.
|
|
3008
3202
|
*/
|
|
3009
3203
|
/**
|
|
3010
|
-
* The
|
|
3011
|
-
*
|
|
3012
|
-
*
|
|
3013
|
-
*
|
|
3204
|
+
* The History view's body — the user's previous conversations (most recent
|
|
3205
|
+
* first) plus a "New conversation" action. Clicking a row switches the widget to
|
|
3206
|
+
* that conversation; the current one is marked so the user sees where they are.
|
|
3207
|
+
* Floating-only (opened from the header history button).
|
|
3208
|
+
*/
|
|
3209
|
+
_renderHistoryContent() {
|
|
3210
|
+
return html `
|
|
3211
|
+
<div class="history">
|
|
3212
|
+
${this._conversationsLoading ? html `
|
|
3213
|
+
<div class="history__empty">Loading…</div>
|
|
3214
|
+
` : this._conversations.length === 0 ? html `
|
|
3215
|
+
<div class="history__empty">No previous conversations</div>
|
|
3216
|
+
` : html `
|
|
3217
|
+
<div class="history__list">
|
|
3218
|
+
${this._conversations.map((conversation) => html `
|
|
3219
|
+
<button
|
|
3220
|
+
type="button"
|
|
3221
|
+
class=${classMap({
|
|
3222
|
+
'history__item': true,
|
|
3223
|
+
'history__item--active': conversation.conversationID === this.conversationId,
|
|
3224
|
+
})}
|
|
3225
|
+
@click=${() => this._handleSwitchConversation(conversation.conversationID)}
|
|
3226
|
+
>
|
|
3227
|
+
<span class="history__item-title">${conversation.title || 'Untitled conversation'}</span>
|
|
3228
|
+
${conversation.lastModified ? html `
|
|
3229
|
+
<span class="history__item-date">${this._renderConversationDate(conversation.lastModified)}</span>
|
|
3230
|
+
` : nothing}
|
|
3231
|
+
</button>
|
|
3232
|
+
`)}
|
|
3233
|
+
</div>
|
|
3234
|
+
`}
|
|
3235
|
+
</div>
|
|
3236
|
+
`;
|
|
3237
|
+
}
|
|
3238
|
+
/**
|
|
3239
|
+
* Format a conversation's lastModified for the history list: today shows the
|
|
3240
|
+
* time, this year shows month/day, older shows month/day/year. Falls back to the
|
|
3241
|
+
* raw value if it isn't a parseable date.
|
|
3242
|
+
*/
|
|
3243
|
+
_renderConversationDate(lastModified) {
|
|
3244
|
+
const date = new Date(lastModified);
|
|
3245
|
+
if (isNaN(date.getTime())) {
|
|
3246
|
+
return lastModified;
|
|
3247
|
+
}
|
|
3248
|
+
const now = new Date();
|
|
3249
|
+
if (date.toDateString() === now.toDateString()) {
|
|
3250
|
+
return date.toLocaleTimeString('en-US', {
|
|
3251
|
+
hour: 'numeric',
|
|
3252
|
+
minute: '2-digit',
|
|
3253
|
+
});
|
|
3254
|
+
}
|
|
3255
|
+
if (date.getFullYear() === now.getFullYear()) {
|
|
3256
|
+
return date.toLocaleDateString('en-US', {
|
|
3257
|
+
month: 'short',
|
|
3258
|
+
day: 'numeric',
|
|
3259
|
+
});
|
|
3260
|
+
}
|
|
3261
|
+
return date.toLocaleDateString('en-US', {
|
|
3262
|
+
month: 'short',
|
|
3263
|
+
day: 'numeric',
|
|
3264
|
+
year: 'numeric',
|
|
3265
|
+
});
|
|
3266
|
+
}
|
|
3267
|
+
/**
|
|
3268
|
+
* The side drawer — one sliding panel that holds the Settings, Debug, History,
|
|
3269
|
+
* and tool-detail views. The header buttons switch `_drawerView`; the title and
|
|
3270
|
+
* body swap inside. In inline mode it slides in beside the transcript at a
|
|
3271
|
+
* consistent (resizable) width; in floating mode it fills the panel over the
|
|
3272
|
+
* transcript with a back button (the panel is too narrow to show both).
|
|
3014
3273
|
*/
|
|
3015
3274
|
_renderDrawer() {
|
|
3016
3275
|
// The debug view is gated on the server-side flag (inline variant + debug
|
|
@@ -3028,41 +3287,101 @@ export class KRChatbot extends LitElement {
|
|
|
3028
3287
|
title = this._toolDetail.use.data?.displayName || this._toolDetail.use.data?.toolName || 'Tool';
|
|
3029
3288
|
body = this._renderToolDetailContent();
|
|
3030
3289
|
}
|
|
3290
|
+
else if (this._drawerView === 'history') {
|
|
3291
|
+
title = 'Conversations';
|
|
3292
|
+
body = this._renderHistoryContent();
|
|
3293
|
+
}
|
|
3294
|
+
// In the floating widget the drawer fills the panel over the transcript (a
|
|
3295
|
+
// full-panel view with a back button) rather than sliding in beside it — the
|
|
3296
|
+
// panel is too narrow to show both. Inline keeps the side-drawer behavior.
|
|
3297
|
+
const fullPanel = this.variant === 'floating';
|
|
3031
3298
|
return html `
|
|
3032
3299
|
<div
|
|
3033
3300
|
class=${classMap({
|
|
3034
3301
|
'drawer': true,
|
|
3035
3302
|
'drawer--open': this._drawerOpen,
|
|
3036
3303
|
'drawer--resizing': this._isDrawerResizing,
|
|
3304
|
+
'drawer--full': fullPanel,
|
|
3037
3305
|
})}
|
|
3038
|
-
style=${styleMap({ width: `${this._drawerWidth}px` })}
|
|
3306
|
+
style=${styleMap(fullPanel ? {} : { width: `${this._drawerWidth}px` })}
|
|
3039
3307
|
>
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3308
|
+
${fullPanel ? nothing : html `
|
|
3309
|
+
<div
|
|
3310
|
+
class="drawer__resize"
|
|
3311
|
+
@mousedown=${this._handleDrawerResizeMouseDown}
|
|
3312
|
+
></div>
|
|
3313
|
+
`}
|
|
3044
3314
|
<div class="drawer__header">
|
|
3315
|
+
${fullPanel ? html `
|
|
3316
|
+
<button
|
|
3317
|
+
class="header-action"
|
|
3318
|
+
@click=${this._handleCloseDrawer}
|
|
3319
|
+
title="Back"
|
|
3320
|
+
>
|
|
3321
|
+
<svg
|
|
3322
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3323
|
+
fill="none"
|
|
3324
|
+
viewBox="0 0 24 24"
|
|
3325
|
+
stroke-width="2"
|
|
3326
|
+
stroke="currentColor"
|
|
3327
|
+
class="header-action-icon"
|
|
3328
|
+
>
|
|
3329
|
+
<path
|
|
3330
|
+
stroke-linecap="round"
|
|
3331
|
+
stroke-linejoin="round"
|
|
3332
|
+
d="M15.75 19.5 8.25 12l7.5-7.5"
|
|
3333
|
+
/>
|
|
3334
|
+
</svg>
|
|
3335
|
+
</button>
|
|
3336
|
+
` : nothing}
|
|
3045
3337
|
<span class="drawer__title">${title}</span>
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
fill="none"
|
|
3054
|
-
viewBox="0 0 24 24"
|
|
3055
|
-
stroke-width="2"
|
|
3056
|
-
stroke="currentColor"
|
|
3057
|
-
class="header-action-icon"
|
|
3338
|
+
${this._drawerView === 'history' ? html `
|
|
3339
|
+
<kr-button
|
|
3340
|
+
class="history__new"
|
|
3341
|
+
variant="flat"
|
|
3342
|
+
color="primary"
|
|
3343
|
+
size="small"
|
|
3344
|
+
@click=${this._handleNewConversation}
|
|
3058
3345
|
>
|
|
3059
|
-
<
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3346
|
+
<svg
|
|
3347
|
+
slot="icon"
|
|
3348
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3349
|
+
fill="none"
|
|
3350
|
+
viewBox="0 0 24 24"
|
|
3351
|
+
stroke-width="1.75"
|
|
3352
|
+
stroke="currentColor"
|
|
3353
|
+
>
|
|
3354
|
+
<path
|
|
3355
|
+
stroke-linecap="round"
|
|
3356
|
+
stroke-linejoin="round"
|
|
3357
|
+
d="M12 4.5v15m7.5-7.5h-15"
|
|
3358
|
+
/>
|
|
3359
|
+
</svg>
|
|
3360
|
+
New conversation
|
|
3361
|
+
</kr-button>
|
|
3362
|
+
` : nothing}
|
|
3363
|
+
${fullPanel ? nothing : html `
|
|
3364
|
+
<button
|
|
3365
|
+
class="header-action"
|
|
3366
|
+
@click=${this._handleCloseDrawer}
|
|
3367
|
+
title="Close"
|
|
3368
|
+
>
|
|
3369
|
+
<svg
|
|
3370
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3371
|
+
fill="none"
|
|
3372
|
+
viewBox="0 0 24 24"
|
|
3373
|
+
stroke-width="2"
|
|
3374
|
+
stroke="currentColor"
|
|
3375
|
+
class="header-action-icon"
|
|
3376
|
+
>
|
|
3377
|
+
<path
|
|
3378
|
+
stroke-linecap="round"
|
|
3379
|
+
stroke-linejoin="round"
|
|
3380
|
+
d="M6 18 18 6M6 6l12 12"
|
|
3381
|
+
/>
|
|
3382
|
+
</svg>
|
|
3383
|
+
</button>
|
|
3384
|
+
`}
|
|
3066
3385
|
</div>
|
|
3067
3386
|
<div class="drawer__body">
|
|
3068
3387
|
${body}
|
|
@@ -3323,9 +3642,10 @@ export class KRChatbot extends LitElement {
|
|
|
3323
3642
|
const isDebugEnabled = this.variant === 'inline' && this._aiFrameworkConfig.debugTelemetryEnabled === true;
|
|
3324
3643
|
// Push the content over by the open drawer's width so the drawer sits beside
|
|
3325
3644
|
// the content rather than overlaying it. One consistent (resizable) width for
|
|
3326
|
-
// both views. Only pushes while the drawer is open
|
|
3645
|
+
// both views. Only pushes while the drawer is open, and only inline — the
|
|
3646
|
+
// floating drawer fills the panel over the transcript, so it must not push.
|
|
3327
3647
|
const contentPadding = {};
|
|
3328
|
-
if (this._drawerOpen) {
|
|
3648
|
+
if (this._drawerOpen && this.variant === 'inline') {
|
|
3329
3649
|
contentPadding.paddingRight = `${this._drawerWidth}px`;
|
|
3330
3650
|
}
|
|
3331
3651
|
// Group the transcript into runs so an agent turn (its reasoning/tool/text/
|
|
@@ -3443,10 +3763,52 @@ export class KRChatbot extends LitElement {
|
|
|
3443
3763
|
<div class="header" @mousedown=${this._handleHeaderMouseDown}>
|
|
3444
3764
|
<div class="header-left">
|
|
3445
3765
|
<div class="header-info">
|
|
3446
|
-
<span class="header-title">${this.label}</span>
|
|
3766
|
+
<span class="header-title">${this._conversationTitle || this.label}</span>
|
|
3447
3767
|
</div>
|
|
3448
3768
|
</div>
|
|
3449
3769
|
<div class="header-actions">
|
|
3770
|
+
${this.variant === 'floating' ? html `
|
|
3771
|
+
<button
|
|
3772
|
+
class="header-action"
|
|
3773
|
+
@click=${this._handleNewConversation}
|
|
3774
|
+
title="New conversation"
|
|
3775
|
+
>
|
|
3776
|
+
<svg
|
|
3777
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3778
|
+
fill="none"
|
|
3779
|
+
viewBox="0 0 24 24"
|
|
3780
|
+
stroke-width="1.75"
|
|
3781
|
+
stroke="currentColor"
|
|
3782
|
+
class="header-action-icon"
|
|
3783
|
+
>
|
|
3784
|
+
<path
|
|
3785
|
+
stroke-linecap="round"
|
|
3786
|
+
stroke-linejoin="round"
|
|
3787
|
+
d="M12 4.5v15m7.5-7.5h-15"
|
|
3788
|
+
/>
|
|
3789
|
+
</svg>
|
|
3790
|
+
</button>
|
|
3791
|
+
<button
|
|
3792
|
+
class="header-action"
|
|
3793
|
+
@click=${() => this._toggleDrawer('history')}
|
|
3794
|
+
title="Conversations"
|
|
3795
|
+
>
|
|
3796
|
+
<svg
|
|
3797
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3798
|
+
fill="none"
|
|
3799
|
+
viewBox="0 0 24 24"
|
|
3800
|
+
stroke-width="1.5"
|
|
3801
|
+
stroke="currentColor"
|
|
3802
|
+
class="header-action-icon"
|
|
3803
|
+
>
|
|
3804
|
+
<path
|
|
3805
|
+
stroke-linecap="round"
|
|
3806
|
+
stroke-linejoin="round"
|
|
3807
|
+
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
|
3808
|
+
/>
|
|
3809
|
+
</svg>
|
|
3810
|
+
</button>
|
|
3811
|
+
` : nothing}
|
|
3450
3812
|
<button
|
|
3451
3813
|
class="header-action"
|
|
3452
3814
|
@click=${() => this._toggleDrawer('settings')}
|
|
@@ -4201,22 +4563,28 @@ KRChatbot.styles = css `
|
|
|
4201
4563
|
display: flex;
|
|
4202
4564
|
align-items: center;
|
|
4203
4565
|
gap: 10px;
|
|
4566
|
+
min-width: 0;
|
|
4204
4567
|
}
|
|
4205
4568
|
|
|
4206
4569
|
.header-info {
|
|
4207
4570
|
display: flex;
|
|
4208
4571
|
flex-direction: column;
|
|
4572
|
+
min-width: 0;
|
|
4209
4573
|
}
|
|
4210
4574
|
|
|
4211
4575
|
.header-title {
|
|
4212
4576
|
font-size: 16px;
|
|
4213
4577
|
font-weight: 600;
|
|
4578
|
+
white-space: nowrap;
|
|
4579
|
+
overflow: hidden;
|
|
4580
|
+
text-overflow: ellipsis;
|
|
4214
4581
|
}
|
|
4215
4582
|
|
|
4216
4583
|
.header-actions {
|
|
4217
4584
|
display: flex;
|
|
4218
4585
|
align-items: center;
|
|
4219
4586
|
gap: 4px;
|
|
4587
|
+
flex-shrink: 0;
|
|
4220
4588
|
}
|
|
4221
4589
|
|
|
4222
4590
|
.header-action {
|
|
@@ -5071,6 +5439,13 @@ KRChatbot.styles = css `
|
|
|
5071
5439
|
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
|
5072
5440
|
}
|
|
5073
5441
|
|
|
5442
|
+
/* Full-panel (floating) header: back button then title, left-aligned. */
|
|
5443
|
+
.drawer--full .drawer__header {
|
|
5444
|
+
justify-content: flex-start;
|
|
5445
|
+
gap: 8px;
|
|
5446
|
+
padding: 0 12px;
|
|
5447
|
+
}
|
|
5448
|
+
|
|
5074
5449
|
.drawer__title {
|
|
5075
5450
|
font-size: 14px;
|
|
5076
5451
|
font-weight: 600;
|
|
@@ -5084,6 +5459,78 @@ KRChatbot.styles = css `
|
|
|
5084
5459
|
padding: 4px 0;
|
|
5085
5460
|
}
|
|
5086
5461
|
|
|
5462
|
+
/* Floating variant: the drawer fills the whole panel over the transcript (a
|
|
5463
|
+
full-panel view with a back button), rather than sliding in beside it. */
|
|
5464
|
+
.drawer--full {
|
|
5465
|
+
left: 0;
|
|
5466
|
+
width: 100%;
|
|
5467
|
+
max-width: 100%;
|
|
5468
|
+
z-index: 6;
|
|
5469
|
+
}
|
|
5470
|
+
|
|
5471
|
+
.drawer--full.drawer--open {
|
|
5472
|
+
box-shadow: none;
|
|
5473
|
+
}
|
|
5474
|
+
|
|
5475
|
+
/* History (previous conversations) view. */
|
|
5476
|
+
.history {
|
|
5477
|
+
display: flex;
|
|
5478
|
+
flex-direction: column;
|
|
5479
|
+
}
|
|
5480
|
+
|
|
5481
|
+
/* "New conversation" action in the Conversations header — pushed to the right. */
|
|
5482
|
+
.history__new {
|
|
5483
|
+
margin-left: auto;
|
|
5484
|
+
}
|
|
5485
|
+
|
|
5486
|
+
.history__empty {
|
|
5487
|
+
padding: 24px 20px;
|
|
5488
|
+
text-align: center;
|
|
5489
|
+
font-size: 13px;
|
|
5490
|
+
color: rgb(0 0 0 / 50%);
|
|
5491
|
+
}
|
|
5492
|
+
|
|
5493
|
+
.history__list {
|
|
5494
|
+
display: flex;
|
|
5495
|
+
flex-direction: column;
|
|
5496
|
+
padding: 4px 0;
|
|
5497
|
+
}
|
|
5498
|
+
|
|
5499
|
+
.history__item {
|
|
5500
|
+
display: flex;
|
|
5501
|
+
flex-direction: column;
|
|
5502
|
+
align-items: flex-start;
|
|
5503
|
+
gap: 5px;
|
|
5504
|
+
padding: 10px 20px;
|
|
5505
|
+
background: transparent;
|
|
5506
|
+
border: none;
|
|
5507
|
+
text-align: left;
|
|
5508
|
+
cursor: pointer;
|
|
5509
|
+
transition: background 0.15s;
|
|
5510
|
+
}
|
|
5511
|
+
|
|
5512
|
+
.history__item:hover {
|
|
5513
|
+
background: rgba(0, 0, 0, 0.04);
|
|
5514
|
+
}
|
|
5515
|
+
|
|
5516
|
+
.history__item--active {
|
|
5517
|
+
background: rgba(0, 0, 0, 0.06);
|
|
5518
|
+
}
|
|
5519
|
+
|
|
5520
|
+
.history__item-title {
|
|
5521
|
+
font-size: 14px;
|
|
5522
|
+
color: #000000;
|
|
5523
|
+
white-space: nowrap;
|
|
5524
|
+
overflow: hidden;
|
|
5525
|
+
text-overflow: ellipsis;
|
|
5526
|
+
max-width: 100%;
|
|
5527
|
+
}
|
|
5528
|
+
|
|
5529
|
+
.history__item-date {
|
|
5530
|
+
font-size: 12px;
|
|
5531
|
+
color: rgb(0 0 0 / 60%);
|
|
5532
|
+
}
|
|
5533
|
+
|
|
5087
5534
|
.debug-section {
|
|
5088
5535
|
padding: 14px 20px;
|
|
5089
5536
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
@@ -6018,6 +6465,9 @@ __decorate([
|
|
|
6018
6465
|
__decorate([
|
|
6019
6466
|
state()
|
|
6020
6467
|
], KRChatbot.prototype, "_error", void 0);
|
|
6468
|
+
__decorate([
|
|
6469
|
+
state()
|
|
6470
|
+
], KRChatbot.prototype, "_conversationTitle", void 0);
|
|
6021
6471
|
__decorate([
|
|
6022
6472
|
state()
|
|
6023
6473
|
], KRChatbot.prototype, "_contextWarning", void 0);
|
|
@@ -6051,6 +6501,12 @@ __decorate([
|
|
|
6051
6501
|
__decorate([
|
|
6052
6502
|
state()
|
|
6053
6503
|
], KRChatbot.prototype, "_drawerView", void 0);
|
|
6504
|
+
__decorate([
|
|
6505
|
+
state()
|
|
6506
|
+
], KRChatbot.prototype, "_conversations", void 0);
|
|
6507
|
+
__decorate([
|
|
6508
|
+
state()
|
|
6509
|
+
], KRChatbot.prototype, "_conversationsLoading", void 0);
|
|
6054
6510
|
__decorate([
|
|
6055
6511
|
state()
|
|
6056
6512
|
], KRChatbot.prototype, "_toolDetail", void 0);
|