@nuraly/lumenui 0.6.0 → 0.8.2

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.
@@ -79,7 +79,13 @@ export declare class NrChatbotElement extends NrChatbotElement_base {
79
79
  enableThreadCreation: boolean;
80
80
  /** Array of conversation threads */
81
81
  threads: ChatbotThread[];
82
- /** Currently active thread ID */
82
+ /**
83
+ * Currently active thread ID. Set this from a route loader to pre-select a
84
+ * conversation; when a controller is attached, the chatbot will call
85
+ * `controller.switchThread(activeThreadId)` to load that thread's messages.
86
+ * Emits an `nr-thread-change` event when the active thread changes via the
87
+ * sidebar (so a router can sync the URL back).
88
+ */
83
89
  activeThreadId?: string;
84
90
  /** Chatbot mode (chat, assistant, etc.) */
85
91
  mode: string;
@@ -89,12 +95,15 @@ export declare class NrChatbotElement extends NrChatbotElement_base {
89
95
  enableUrlSync: boolean;
90
96
  /** Show messages area (set to false for input-only mode) */
91
97
  showMessages: boolean;
98
+ /** Welcome heading shown in the empty state. Overridden by a slotted `empty-state` child if provided. */
99
+ welcomeMessage?: string;
92
100
  /** Enable file upload functionality */
93
101
  enableFileUpload: boolean;
94
102
  /** Uploaded files (synced from controller) */
95
103
  uploadedFiles: ChatbotFile[];
96
- /** Action buttons configuration */
104
+ /** Action buttons configuration. `enableFileUpload` unions an attach entry into the resolved list unless one is already present. */
97
105
  actionButtons: ChatbotAction[];
106
+ get resolvedActionButtons(): ChatbotAction[];
98
107
  /** Enable module selection dropdown */
99
108
  enableModuleSelection: boolean;
100
109
  /** Available modules for selection */
@@ -162,6 +171,21 @@ export declare class NrChatbotElement extends NrChatbotElement_base {
162
171
  */
163
172
  private teardownUrlSync;
164
173
  private handleHashChange;
174
+ private _pendingThreadId?;
175
+ private _expandedMessageIds;
176
+ /**
177
+ * Character count above which a user message is collapsed with a "Show more"
178
+ * toggle and a bubble-colored gradient. Set to 0 to disable. Default 600.
179
+ */
180
+ messageCollapseThreshold: number;
181
+ /**
182
+ * Anchor the messages column to the bottom. New messages stay at the bottom
183
+ * automatically (browser-native scroll anchoring via flex-direction column-reverse).
184
+ * When the user scrolls up to read history, new arriving messages do not pull
185
+ * them back down. Implies that auto-scroll JS is skipped.
186
+ */
187
+ invertedScroll: boolean;
188
+ private syncActiveThreadToController;
165
189
  private handleControllerStateChange;
166
190
  private handleControllerMessageSent;
167
191
  private handleControllerMessageReceived;
@@ -80,6 +80,9 @@ const DEFAULT_I18N = {
80
80
  retryButton: msg('Retry'),
81
81
  startConversationLabel: msg('Start a conversation'),
82
82
  suggestionPrefix: msg('Select suggestion: '),
83
+ loadingConversationLabel: msg('Loading conversation…'),
84
+ showMoreLabel: msg('Show more'),
85
+ showLessLabel: msg('Show less'),
83
86
  },
84
87
  urlModal: {
85
88
  addUrlTitle: msg('Add URL'),
@@ -180,7 +183,7 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
180
183
  this.enableFileUpload = false;
181
184
  /** Uploaded files (synced from controller) */
182
185
  this.uploadedFiles = [];
183
- /** Action buttons configuration */
186
+ /** Action buttons configuration. `enableFileUpload` unions an attach entry into the resolved list unless one is already present. */
184
187
  this.actionButtons = [];
185
188
  /** Enable module selection dropdown */
186
189
  this.enableModuleSelection = false;
@@ -213,10 +216,33 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
213
216
  this._audioMode = 'message';
214
217
  // Artifact panel resize state
215
218
  this._artifactResizeBound = false;
219
+ this._expandedMessageIds = new Set();
220
+ /**
221
+ * Character count above which a user message is collapsed with a "Show more"
222
+ * toggle and a bubble-colored gradient. Set to 0 to disable. Default 600.
223
+ */
224
+ this.messageCollapseThreshold = 600;
225
+ /**
226
+ * Anchor the messages column to the bottom. New messages stay at the bottom
227
+ * automatically (browser-native scroll anchoring via flex-direction column-reverse).
228
+ * When the user scrolls up to read history, new arriving messages do not pull
229
+ * them back down. Implies that auto-scroll JS is skipped.
230
+ */
231
+ this.invertedScroll = false;
216
232
  this.toggleThreadSidebar = () => {
217
233
  this.isThreadSidebarOpen = !this.isThreadSidebarOpen;
218
234
  };
219
235
  }
236
+ get resolvedActionButtons() {
237
+ var _a;
238
+ const explicit = (_a = this.actionButtons) !== null && _a !== void 0 ? _a : [];
239
+ if (!this.enableFileUpload)
240
+ return explicit;
241
+ const hasAttach = explicit.some((a) => (a === null || a === void 0 ? void 0 : a.type) === 'attach');
242
+ if (hasAttach)
243
+ return explicit;
244
+ return [...explicit, { type: 'attach', enabled: true }];
245
+ }
220
246
  /** Convert modules to select options */
221
247
  get moduleSelectOptions() {
222
248
  return this.modules.map(module => ({
@@ -236,7 +262,7 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
236
262
  this.setupUrlSync();
237
263
  }
238
264
  firstUpdated() {
239
- var _a, _b, _c, _d;
265
+ var _a, _b, _c, _d, _e;
240
266
  // Event delegation for artifact card clicks (injected via unsafeHTML)
241
267
  (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.addEventListener('click', (e) => {
242
268
  var _a, _b;
@@ -275,8 +301,24 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
275
301
  }
276
302
  }
277
303
  });
304
+ // Show more / Show less on collapsed user messages
305
+ (_d = this.shadowRoot) === null || _d === void 0 ? void 0 : _d.addEventListener('click', (e) => {
306
+ var _a, _b;
307
+ const toggle = (_b = (_a = e.target).closest) === null || _b === void 0 ? void 0 : _b.call(_a, '[data-message-toggle]');
308
+ if (toggle) {
309
+ const id = toggle.dataset.messageToggle;
310
+ if (id) {
311
+ const next = new Set(this._expandedMessageIds);
312
+ if (next.has(id))
313
+ next.delete(id);
314
+ else
315
+ next.add(id);
316
+ this._expandedMessageIds = next;
317
+ }
318
+ }
319
+ });
278
320
  // Keyboard support for artifact cards and selection cards
279
- (_d = this.shadowRoot) === null || _d === void 0 ? void 0 : _d.addEventListener('keydown', (e) => {
321
+ (_e = this.shadowRoot) === null || _e === void 0 ? void 0 : _e.addEventListener('keydown', (e) => {
280
322
  var _a, _b, _c, _d;
281
323
  const ke = e;
282
324
  if (ke.key !== 'Enter' && ke.key !== ' ')
@@ -329,8 +371,12 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
329
371
  if (this.enableUrlSync) {
330
372
  this.handleHashChange();
331
373
  }
374
+ this.syncActiveThreadToController();
332
375
  }
333
376
  }
377
+ if (changedProperties.has('activeThreadId')) {
378
+ this.syncActiveThreadToController();
379
+ }
334
380
  // Handle enableUrlSync toggled after connectedCallback
335
381
  if (changedProperties.has('enableUrlSync')) {
336
382
  if (this.enableUrlSync) {
@@ -356,8 +402,10 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
356
402
  }
357
403
  }
358
404
  }
359
- // Auto-scroll when messages are added or updated
360
- if (changedProperties.has('messages') && this.autoScroll && this.messages.length > 0) {
405
+ // Auto-scroll when messages are added or updated.
406
+ // Skipped in inverted-scroll mode: the column-reverse layout anchors the
407
+ // newest message at the bottom natively and JS scrolling fights that.
408
+ if (changedProperties.has('messages') && this.autoScroll && !this.invertedScroll && this.messages.length > 0) {
361
409
  this.scrollToLatestMessage();
362
410
  }
363
411
  // Attach/detach artifact panel resize handle
@@ -428,8 +476,38 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
428
476
  }
429
477
  }
430
478
  }
479
+ syncActiveThreadToController() {
480
+ var _a;
481
+ if (!this.controller || !this.activeThreadId) {
482
+ this._pendingThreadId = undefined;
483
+ return;
484
+ }
485
+ let state;
486
+ try {
487
+ state = this.controller.getState();
488
+ }
489
+ catch (_b) {
490
+ state = null;
491
+ }
492
+ if ((state === null || state === void 0 ? void 0 : state.currentThreadId) === this.activeThreadId) {
493
+ this._pendingThreadId = undefined;
494
+ return;
495
+ }
496
+ const exists = (_a = state === null || state === void 0 ? void 0 : state.threads) === null || _a === void 0 ? void 0 : _a.some((t) => t.id === this.activeThreadId);
497
+ if (!exists) {
498
+ this._pendingThreadId = this.activeThreadId;
499
+ return;
500
+ }
501
+ this._pendingThreadId = undefined;
502
+ try {
503
+ this.controller.switchThread(this.activeThreadId);
504
+ }
505
+ catch (_c) {
506
+ this._pendingThreadId = this.activeThreadId;
507
+ }
508
+ }
431
509
  handleControllerStateChange(state) {
432
- var _a, _b, _c, _d, _e;
510
+ var _a, _b, _c, _d, _e, _f, _g;
433
511
  // Sync controller state to component properties
434
512
  if (state.messages)
435
513
  this.messages = state.messages;
@@ -439,15 +517,26 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
439
517
  if (state.suggestions && state.suggestions.length > 0) {
440
518
  this.suggestions = state.suggestions;
441
519
  }
442
- if (state.currentThreadId)
520
+ if (state.currentThreadId && state.currentThreadId !== this.activeThreadId) {
443
521
  this.activeThreadId = state.currentThreadId;
522
+ }
523
+ if (this._pendingThreadId && ((_a = state.threads) === null || _a === void 0 ? void 0 : _a.some((t) => t.id === this._pendingThreadId))) {
524
+ const pending = this._pendingThreadId;
525
+ this._pendingThreadId = undefined;
526
+ try {
527
+ (_b = this.controller) === null || _b === void 0 ? void 0 : _b.switchThread(pending);
528
+ }
529
+ catch (_h) {
530
+ this._pendingThreadId = pending;
531
+ }
532
+ }
444
533
  if (this.enableUrlSync && state.currentThreadId) {
445
534
  const newHash = `#conversation/${encodeURIComponent(state.currentThreadId)}`;
446
535
  if (window.location.hash !== newHash) {
447
536
  history.replaceState(null, '', newHash);
448
537
  }
449
538
  }
450
- this.chatStarted = ((_a = state.messages) === null || _a === void 0 ? void 0 : _a.length) > 0;
539
+ this.chatStarted = ((_c = state.messages) === null || _c === void 0 ? void 0 : _c.length) > 0;
451
540
  this.isBotTyping = state.isTyping || false;
452
541
  this.statusText = state.statusText;
453
542
  // Keep Stop button in sync with provider processing lifecycle
@@ -457,19 +546,19 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
457
546
  this.uploadedFiles = state.uploadedFiles;
458
547
  // Reset artifact panel when switching to a conversation with no artifacts
459
548
  if (this.enableArtifacts) {
460
- const hasArtifacts = (_b = state.messages) === null || _b === void 0 ? void 0 : _b.some((m) => { var _a, _b; return ((_b = (_a = m.metadata) === null || _a === void 0 ? void 0 : _a.artifactIds) === null || _b === void 0 ? void 0 : _b.length) > 0; });
549
+ const hasArtifacts = (_d = state.messages) === null || _d === void 0 ? void 0 : _d.some((m) => { var _a, _b; return ((_b = (_a = m.metadata) === null || _a === void 0 ? void 0 : _a.artifactIds) === null || _b === void 0 ? void 0 : _b.length) > 0; });
461
550
  if (!hasArtifacts) {
462
551
  this.isArtifactPanelOpen = false;
463
552
  this.selectedArtifact = null;
464
553
  }
465
554
  }
466
555
  // Auto-select the last artifact when a new one appears
467
- if (this.enableArtifacts && ((_c = state.messages) === null || _c === void 0 ? void 0 : _c.length)) {
556
+ if (this.enableArtifacts && ((_e = state.messages) === null || _e === void 0 ? void 0 : _e.length)) {
468
557
  const lastBot = [...state.messages].reverse().find((m) => m.sender === 'bot');
469
- const artifactIds = (_d = lastBot === null || lastBot === void 0 ? void 0 : lastBot.metadata) === null || _d === void 0 ? void 0 : _d.artifactIds;
558
+ const artifactIds = (_f = lastBot === null || lastBot === void 0 ? void 0 : lastBot.metadata) === null || _f === void 0 ? void 0 : _f.artifactIds;
470
559
  if (artifactIds === null || artifactIds === void 0 ? void 0 : artifactIds.length) {
471
560
  const lastId = artifactIds[artifactIds.length - 1];
472
- if (((_e = this.selectedArtifact) === null || _e === void 0 ? void 0 : _e.id) !== lastId) {
561
+ if (((_g = this.selectedArtifact) === null || _g === void 0 ? void 0 : _g.id) !== lastId) {
473
562
  const plugin = this.getArtifactPlugin();
474
563
  const artifact = plugin === null || plugin === void 0 ? void 0 : plugin.getArtifact(lastId);
475
564
  if (artifact) {
@@ -513,6 +602,9 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
513
602
  const templateData = {
514
603
  boxed: this.boxed,
515
604
  showMessages: this.showMessages,
605
+ welcomeMessage: this.welcomeMessage,
606
+ isPendingThread: !!this._pendingThreadId,
607
+ invertedScroll: this.invertedScroll,
516
608
  messages: this.messages,
517
609
  isTyping: this.isBotTyping,
518
610
  loadingIndicator: this.loadingIndicator,
@@ -526,7 +618,7 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
526
618
  uploadedFiles: this.uploadedFiles,
527
619
  isQueryRunning: this.isQueryRunning,
528
620
  showSendButton: this.showSendButton,
529
- enableFileUpload: this.enableFileUpload,
621
+ enableFileUpload: this.resolvedActionButtons.some((action) => (action === null || action === void 0 ? void 0 : action.type) === 'attach' && (action === null || action === void 0 ? void 0 : action.enabled) !== false),
530
622
  fileUploadItems: [
531
623
  { id: 'upload-file', label: 'Upload File', icon: 'upload' },
532
624
  { id: 'upload-url', label: 'Upload from URL', icon: 'link' }
@@ -575,7 +667,9 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
575
667
  onRetryKeydown: () => { },
576
668
  onCopy: this.handleCopyMessage.bind(this),
577
669
  onCopyKeydown: () => { },
578
- onFileClick: this.handleFilePreview.bind(this)
670
+ onFileClick: this.handleFilePreview.bind(this),
671
+ collapseThreshold: this.messageCollapseThreshold,
672
+ isExpanded: (id) => this._expandedMessageIds.has(id),
579
673
  },
580
674
  suggestion: {
581
675
  onClick: this.handleSuggestionClick.bind(this),
@@ -601,10 +695,17 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
601
695
  onCreateNew: () => { var _a; (_a = this.controller) === null || _a === void 0 ? void 0 : _a.createThread('New Chat'); },
602
696
  onSelectThread: (threadId) => {
603
697
  var _a;
698
+ if (threadId === this.activeThreadId)
699
+ return;
604
700
  if (this.enableUrlSync) {
605
701
  history.pushState(null, '', `#conversation/${encodeURIComponent(threadId)}`);
606
702
  }
607
703
  (_a = this.controller) === null || _a === void 0 ? void 0 : _a.switchThread(threadId);
704
+ this.dispatchEvent(new CustomEvent('nr-thread-change', {
705
+ detail: { threadId },
706
+ bubbles: true,
707
+ composed: true,
708
+ }));
608
709
  },
609
710
  onDeleteThread: (threadId) => { var _a; (_a = this.controller) === null || _a === void 0 ? void 0 : _a.deleteThread(threadId); },
610
711
  onBookmarkThread: (threadId) => { var _a; (_a = this.controller) === null || _a === void 0 ? void 0 : _a.bookmarkThread(threadId); },
@@ -1194,7 +1295,7 @@ __decorate([
1194
1295
  property({ type: Array })
1195
1296
  ], NrChatbotElement.prototype, "threads", void 0);
1196
1297
  __decorate([
1197
- property({ type: String })
1298
+ property({ type: String, attribute: 'active-thread-id' })
1198
1299
  ], NrChatbotElement.prototype, "activeThreadId", void 0);
1199
1300
  __decorate([
1200
1301
  property({ type: String })
@@ -1208,6 +1309,9 @@ __decorate([
1208
1309
  __decorate([
1209
1310
  property({ type: Boolean })
1210
1311
  ], NrChatbotElement.prototype, "showMessages", void 0);
1312
+ __decorate([
1313
+ property({ type: String, attribute: 'welcome-message' })
1314
+ ], NrChatbotElement.prototype, "welcomeMessage", void 0);
1211
1315
  __decorate([
1212
1316
  property({ type: Boolean })
1213
1317
  ], NrChatbotElement.prototype, "enableFileUpload", void 0);
@@ -1286,6 +1390,18 @@ __decorate([
1286
1390
  __decorate([
1287
1391
  state()
1288
1392
  ], NrChatbotElement.prototype, "_isDragging", void 0);
1393
+ __decorate([
1394
+ state()
1395
+ ], NrChatbotElement.prototype, "_pendingThreadId", void 0);
1396
+ __decorate([
1397
+ state()
1398
+ ], NrChatbotElement.prototype, "_expandedMessageIds", void 0);
1399
+ __decorate([
1400
+ property({ type: Number, attribute: 'message-collapse-threshold' })
1401
+ ], NrChatbotElement.prototype, "messageCollapseThreshold", void 0);
1402
+ __decorate([
1403
+ property({ type: Boolean, attribute: 'inverted-scroll', reflect: true })
1404
+ ], NrChatbotElement.prototype, "invertedScroll", void 0);
1289
1405
  NrChatbotElement = __decorate([
1290
1406
  localized(),
1291
1407
  customElement('nr-chatbot')
@@ -19,10 +19,10 @@ export default css `
19
19
  display: flex;
20
20
  width: 100%;
21
21
  height: 100%;
22
- background-color: #ffffff;
23
22
  border-radius: 8px;
24
23
  position: relative;
25
24
  border: 1px solid #e0e0e0;
25
+ box-sizing: border-box;
26
26
  }
27
27
 
28
28
  .chatbot-container {
@@ -47,13 +47,24 @@ export default css `
47
47
  min-width: 0;
48
48
  }
49
49
 
50
+ .chatbot-boxed-area {
51
+ display: flex;
52
+ flex-direction: column;
53
+ flex: 1;
54
+ min-height: 0;
55
+ min-width: 0;
56
+ width: 100%;
57
+ }
58
+
50
59
  .chatbot-header {
51
60
  display: flex;
52
61
  align-items: center;
53
62
  justify-content: space-between;
54
63
  gap: 0.5rem;
55
64
  padding: 0.5rem;
56
- border-bottom: 1px solid #e0e0e0;
65
+ min-height: 43px;
66
+ box-sizing: border-box;
67
+ border-bottom: 1px solid var(--nuraly-color-divider, rgb(224, 224, 224));
57
68
  }
58
69
 
59
70
  .chatbot-content {
@@ -65,7 +76,6 @@ export default css `
65
76
  }
66
77
 
67
78
  :host([boxed]) .chat-container {
68
- background-color: #ffffff;
69
79
  border: none;
70
80
  border-radius: 0;
71
81
  }
@@ -76,22 +86,15 @@ export default css `
76
86
 
77
87
  :host([boxed]) .chatbot-main {
78
88
  width: 100%;
79
- max-width: 768px;
80
- margin: 0 auto;
81
- background-color: #ffffff;
82
89
  border: none;
83
90
  border-radius: 0;
84
91
  box-shadow: none;
85
92
  height: 100%;
86
93
  }
87
94
 
88
- /* Boxed layout with threads: background comes from theme variable with white fallback */
89
- :host([boxed]) .chat-container--boxed.chat-container--with-threads {
90
- background-color: #ffffff;
91
- }
92
-
93
- .chat-container--boxed.chat-container--with-threads .chatbot-main {
94
- background-color: #ffffff;
95
+ :host([boxed]) .chatbot-boxed-area {
96
+ max-width: 768px;
97
+ margin: 0 auto;
95
98
  }
96
99
 
97
100
  .chat-container--boxed.chat-container--with-threads .chat-box {
@@ -107,9 +110,7 @@ export default css `
107
110
  }
108
111
 
109
112
  :host([boxed]) .chatbot-header {
110
- /* Keep header at the top */
111
113
  flex: 0 0 auto;
112
- border-bottom: none;
113
114
  }
114
115
 
115
116
  :host([boxed]) .chatbot-content:has(.empty-state) {
@@ -123,24 +124,15 @@ export default css `
123
124
  min-height: 0;
124
125
  }
125
126
 
126
- :host([boxed]) .chatbot-main:has(.empty-state) {
127
- /* Make main container relative for absolute positioning */
128
- position: relative;
129
- }
130
-
131
127
  :host([boxed]) .empty-state {
132
- /* Position empty state in the center - moved up */
133
- position: absolute;
134
- top: 50%;
135
- left: 50%;
136
- transform: translate(-50%, calc(-50% - 80px));
128
+ display: flex;
129
+ align-items: center;
130
+ justify-content: center;
131
+ flex-direction: column;
137
132
  width: 100%;
133
+ height: 100%;
138
134
  max-width: 768px;
139
- height: auto;
140
135
  padding: 0;
141
- display: flex;
142
- flex-direction: column;
143
- align-items: center;
144
136
  gap: 1.5rem;
145
137
  }
146
138
 
@@ -149,13 +141,9 @@ export default css `
149
141
  }
150
142
 
151
143
  :host([boxed]) .chatbot-content:has(.empty-state) + .input-box {
152
- /* Position input-box in the middle with empty state - moved up */
153
- position: absolute;
154
- top: 50%;
155
- left: 50%;
156
- transform: translate(-50%, calc(-50% + 40px));
157
144
  width: 100%;
158
145
  max-width: 768px;
146
+ margin: 0 auto;
159
147
  }
160
148
 
161
149
  :host([boxed]) .suggestion-container {
@@ -165,7 +153,7 @@ export default css `
165
153
  :host([boxed]) .messages {
166
154
  box-shadow: none;
167
155
  margin-bottom: 0;
168
- background-color: #ffffff;
156
+ background-color: var(--chatbot-messages-bg, transparent);
169
157
  align-items: stretch;
170
158
  width: 98%;
171
159
  padding: 8px 1.5rem;
@@ -209,7 +197,7 @@ export default css `
209
197
  align-items: center;
210
198
  justify-content: space-between;
211
199
  padding: 0.75rem;
212
- border-bottom: 1px solid #e0e0e0;
200
+ border-bottom: 1px solid var(--nuraly-color-divider, rgb(224, 224, 224));
213
201
  }
214
202
 
215
203
  .thread-sidebar__header h3 {
@@ -420,17 +408,23 @@ export default css `
420
408
 
421
409
  .messages {
422
410
  flex: 1;
411
+ min-height: 0;
423
412
  overflow-y: auto;
424
413
  overflow-x: hidden;
425
414
  display: flex;
426
415
  flex-direction: column;
427
416
  gap: 0;
428
- background-color: #ffffff;
417
+ background-color: var(--chatbot-messages-bg, transparent);
429
418
  padding: 8px 1rem;
430
419
  box-sizing: border-box;
431
420
  justify-content: flex-start; /* Always align messages to top */
432
421
  }
433
422
 
423
+ .messages--inverted {
424
+ flex-direction: column-reverse;
425
+ justify-content: flex-start;
426
+ }
427
+
434
428
  .empty-state {
435
429
  display: flex;
436
430
  flex-direction: column;
@@ -564,16 +558,64 @@ export default css `
564
558
  }
565
559
 
566
560
  .message.user .message__content {
567
- background-color: #7c3aed;
568
- color: #ffffff;
561
+ background-color: var(--nuraly-color-user-bubble-bg, rgb(124, 58, 237));
562
+ color: var(--nuraly-color-user-bubble-fg, rgb(255, 255, 255));
569
563
  border-radius: var(--chatbot-radius, 8px);
570
564
  border: 0 solid transparent;
571
565
  box-shadow: none;
572
566
  }
573
567
 
574
- .message.bot .message__content {
575
- background-color: transparent;
568
+ .message__text-collapsible {
569
+ position: relative;
570
+ max-height: var(--chatbot-message-collapsed-height, 200px);
571
+ overflow: hidden;
572
+ }
573
+
574
+ .message__text-collapsible--expanded {
575
+ max-height: none;
576
+ }
577
+
578
+ .message__text-collapsible:not(.message__text-collapsible--expanded)::after {
579
+ content: '';
580
+ position: absolute;
581
+ inset: auto 0 0 0;
582
+ height: 48px;
583
+ pointer-events: none;
584
+ background: linear-gradient(
585
+ to bottom,
586
+ transparent,
587
+ var(--nuraly-color-user-bubble-bg, rgb(124, 58, 237))
588
+ );
589
+ }
590
+
591
+ .message__show-more-toggle {
592
+ margin-top: 6px;
593
+ background: transparent;
594
+ border: 0;
595
+ padding: 4px 0;
596
+ font: inherit;
597
+ font-size: 12px;
598
+ font-weight: 500;
576
599
  color: inherit;
600
+ opacity: 0.85;
601
+ cursor: pointer;
602
+ text-decoration: underline;
603
+ text-underline-offset: 2px;
604
+ }
605
+
606
+ .message__show-more-toggle:hover {
607
+ opacity: 1;
608
+ }
609
+
610
+ .message__show-more-toggle:focus-visible {
611
+ outline: 1px solid currentColor;
612
+ outline-offset: 2px;
613
+ border-radius: 2px;
614
+ }
615
+
616
+ .message.bot .message__content {
617
+ background-color: var(--nuraly-color-bot-bubble-bg, transparent);
618
+ color: var(--nuraly-color-bot-bubble-fg, inherit);
577
619
  border-radius: 0;
578
620
  border: 0 solid transparent;
579
621
  box-shadow: none;
@@ -1618,10 +1660,10 @@ export default css `
1618
1660
  .artifact-panel {
1619
1661
  width: 400px;
1620
1662
  min-width: 300px;
1663
+ min-height: 0;
1621
1664
  flex-shrink: 0;
1622
1665
  display: flex;
1623
1666
  flex-direction: row;
1624
- background-color: #ffffff;
1625
1667
  overflow: hidden;
1626
1668
  position: relative;
1627
1669
  }
@@ -294,6 +294,9 @@ export interface ChatbotI18nMessages {
294
294
  retryButton: string;
295
295
  startConversationLabel: string;
296
296
  suggestionPrefix: string;
297
+ loadingConversationLabel: string;
298
+ showMoreLabel: string;
299
+ showLessLabel: string;
297
300
  }
298
301
  export interface ChatbotI18nUrlModal {
299
302
  addUrlTitle: string;
@@ -16,6 +16,12 @@ export interface ChatbotMainTemplateData {
16
16
  boxed?: boolean;
17
17
  /** Show messages area (set to false for input-only mode) */
18
18
  showMessages?: boolean;
19
+ /** Welcome heading shown when the messages list is empty. Falls back to i18n.messages.startConversationLabel. */
20
+ welcomeMessage?: string;
21
+ /** True when activeThreadId points at a thread that has not yet been loaded. Renders a loading state in the messages area. */
22
+ isPendingThread?: boolean;
23
+ /** Anchor messages to the bottom via flex-direction: column-reverse. New messages stay anchored without JS scroll. */
24
+ invertedScroll?: boolean;
19
25
  messages: ChatbotMessage[];
20
26
  isTyping: boolean;
21
27
  loadingIndicator?: ChatbotLoadingType;
@@ -52,7 +52,7 @@ function renderContentArea(data, handlers) {
52
52
  <div class="chatbot-content" part="content">
53
53
  ${renderMessages(data.messages, renderSuggestions(data.chatStarted, data.suggestions, handlers.suggestion, data.i18n), data.isTyping
54
54
  ? renderBotTypingIndicator(data.isTyping, data.loadingIndicator || ChatbotLoadingType.Spinner, data.loadingText)
55
- : nothing, handlers.message, data.i18n)}
55
+ : nothing, handlers.message, data.i18n, data.welcomeMessage, data.isPendingThread, data.invertedScroll)}
56
56
  <slot name="messages"></slot>
57
57
  </div>
58
58
  `;
@@ -92,11 +92,13 @@ export function renderChatbotMain(data, handlers) {
92
92
  <div class="chatbot-main" part="main">
93
93
  ${renderThreadHeader(data, handlers)}
94
94
 
95
- <slot name="header"></slot>
95
+ <div class="chatbot-boxed-area" part="boxed-area">
96
+ <slot name="header"></slot>
96
97
 
97
- ${renderContentArea(data, handlers)}
98
+ ${renderContentArea(data, handlers)}
98
99
 
99
- ${renderInputBox(data.inputBox, handlers.inputBox)}
100
+ ${renderInputBox(data.inputBox, handlers.inputBox)}
101
+ </div>
100
102
 
101
103
  <slot name="footer"></slot>
102
104
  </div>
@@ -11,6 +11,8 @@ export interface MessageTemplateHandlers {
11
11
  onCopy: (message: ChatbotMessage) => void;
12
12
  onCopyKeydown: (e: KeyboardEvent, message: ChatbotMessage) => void;
13
13
  onFileClick?: (file: any) => void;
14
+ collapseThreshold?: number;
15
+ isExpanded?: (id: string) => boolean;
14
16
  }
15
17
  /**
16
18
  * Renders a single message
@@ -20,12 +22,7 @@ export declare function renderMessage(message: ChatbotMessage, handlers: Message
20
22
  * Renders bot typing indicator
21
23
  */
22
24
  export declare function renderBotTypingIndicator(isTyping: boolean, loadingIndicator: ChatbotLoadingType, loadingText?: string): TemplateResult | typeof nothing;
23
- /**
24
- * Renders empty state
25
- */
26
- export declare function renderEmptyState(i18n: ChatbotI18n): TemplateResult;
27
- /**
28
- * Renders messages container with all messages
29
- */
30
- export declare function renderMessages(messages: ChatbotMessage[], suggestions: TemplateResult | typeof nothing, typingIndicator: TemplateResult | typeof nothing, messageHandlers: MessageTemplateHandlers, i18n: ChatbotI18n): TemplateResult;
25
+ export declare function renderEmptyState(i18n: ChatbotI18n, welcomeMessage?: string): TemplateResult;
26
+ export declare function renderThreadLoading(i18n: ChatbotI18n): TemplateResult;
27
+ export declare function renderMessages(messages: ChatbotMessage[], suggestions: TemplateResult | typeof nothing, typingIndicator: TemplateResult | typeof nothing, messageHandlers: MessageTemplateHandlers, i18n: ChatbotI18n, welcomeMessage?: string, isPendingThread?: boolean, invertedScroll?: boolean): TemplateResult;
31
28
  //# sourceMappingURL=message.template.d.ts.map