@nuraly/lumenui 0.5.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,8 @@ export declare class NrChatbotElement extends NrChatbotElement_base {
162
171
  */
163
172
  private teardownUrlSync;
164
173
  private handleHashChange;
174
+ private _pendingThreadId?;
175
+ private syncActiveThreadToController;
165
176
  private handleControllerStateChange;
166
177
  private handleControllerMessageSent;
167
178
  private handleControllerMessageReceived;
@@ -80,6 +80,7 @@ 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…'),
83
84
  },
84
85
  urlModal: {
85
86
  addUrlTitle: msg('Add URL'),
@@ -180,7 +181,7 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
180
181
  this.enableFileUpload = false;
181
182
  /** Uploaded files (synced from controller) */
182
183
  this.uploadedFiles = [];
183
- /** Action buttons configuration */
184
+ /** Action buttons configuration. `enableFileUpload` unions an attach entry into the resolved list unless one is already present. */
184
185
  this.actionButtons = [];
185
186
  /** Enable module selection dropdown */
186
187
  this.enableModuleSelection = false;
@@ -217,6 +218,16 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
217
218
  this.isThreadSidebarOpen = !this.isThreadSidebarOpen;
218
219
  };
219
220
  }
221
+ get resolvedActionButtons() {
222
+ var _a;
223
+ const explicit = (_a = this.actionButtons) !== null && _a !== void 0 ? _a : [];
224
+ if (!this.enableFileUpload)
225
+ return explicit;
226
+ const hasAttach = explicit.some((a) => (a === null || a === void 0 ? void 0 : a.type) === 'attach');
227
+ if (hasAttach)
228
+ return explicit;
229
+ return [...explicit, { type: 'attach', enabled: true }];
230
+ }
220
231
  /** Convert modules to select options */
221
232
  get moduleSelectOptions() {
222
233
  return this.modules.map(module => ({
@@ -329,8 +340,12 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
329
340
  if (this.enableUrlSync) {
330
341
  this.handleHashChange();
331
342
  }
343
+ this.syncActiveThreadToController();
332
344
  }
333
345
  }
346
+ if (changedProperties.has('activeThreadId')) {
347
+ this.syncActiveThreadToController();
348
+ }
334
349
  // Handle enableUrlSync toggled after connectedCallback
335
350
  if (changedProperties.has('enableUrlSync')) {
336
351
  if (this.enableUrlSync) {
@@ -428,8 +443,38 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
428
443
  }
429
444
  }
430
445
  }
446
+ syncActiveThreadToController() {
447
+ var _a;
448
+ if (!this.controller || !this.activeThreadId) {
449
+ this._pendingThreadId = undefined;
450
+ return;
451
+ }
452
+ let state;
453
+ try {
454
+ state = this.controller.getState();
455
+ }
456
+ catch (_b) {
457
+ state = null;
458
+ }
459
+ if ((state === null || state === void 0 ? void 0 : state.currentThreadId) === this.activeThreadId) {
460
+ this._pendingThreadId = undefined;
461
+ return;
462
+ }
463
+ 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);
464
+ if (!exists) {
465
+ this._pendingThreadId = this.activeThreadId;
466
+ return;
467
+ }
468
+ this._pendingThreadId = undefined;
469
+ try {
470
+ this.controller.switchThread(this.activeThreadId);
471
+ }
472
+ catch (_c) {
473
+ this._pendingThreadId = this.activeThreadId;
474
+ }
475
+ }
431
476
  handleControllerStateChange(state) {
432
- var _a, _b, _c, _d, _e;
477
+ var _a, _b, _c, _d, _e, _f, _g;
433
478
  // Sync controller state to component properties
434
479
  if (state.messages)
435
480
  this.messages = state.messages;
@@ -439,15 +484,26 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
439
484
  if (state.suggestions && state.suggestions.length > 0) {
440
485
  this.suggestions = state.suggestions;
441
486
  }
442
- if (state.currentThreadId)
487
+ if (state.currentThreadId && state.currentThreadId !== this.activeThreadId) {
443
488
  this.activeThreadId = state.currentThreadId;
489
+ }
490
+ if (this._pendingThreadId && ((_a = state.threads) === null || _a === void 0 ? void 0 : _a.some((t) => t.id === this._pendingThreadId))) {
491
+ const pending = this._pendingThreadId;
492
+ this._pendingThreadId = undefined;
493
+ try {
494
+ (_b = this.controller) === null || _b === void 0 ? void 0 : _b.switchThread(pending);
495
+ }
496
+ catch (_h) {
497
+ this._pendingThreadId = pending;
498
+ }
499
+ }
444
500
  if (this.enableUrlSync && state.currentThreadId) {
445
501
  const newHash = `#conversation/${encodeURIComponent(state.currentThreadId)}`;
446
502
  if (window.location.hash !== newHash) {
447
503
  history.replaceState(null, '', newHash);
448
504
  }
449
505
  }
450
- this.chatStarted = ((_a = state.messages) === null || _a === void 0 ? void 0 : _a.length) > 0;
506
+ this.chatStarted = ((_c = state.messages) === null || _c === void 0 ? void 0 : _c.length) > 0;
451
507
  this.isBotTyping = state.isTyping || false;
452
508
  this.statusText = state.statusText;
453
509
  // Keep Stop button in sync with provider processing lifecycle
@@ -457,19 +513,19 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
457
513
  this.uploadedFiles = state.uploadedFiles;
458
514
  // Reset artifact panel when switching to a conversation with no artifacts
459
515
  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; });
516
+ 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
517
  if (!hasArtifacts) {
462
518
  this.isArtifactPanelOpen = false;
463
519
  this.selectedArtifact = null;
464
520
  }
465
521
  }
466
522
  // 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)) {
523
+ if (this.enableArtifacts && ((_e = state.messages) === null || _e === void 0 ? void 0 : _e.length)) {
468
524
  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;
525
+ const artifactIds = (_f = lastBot === null || lastBot === void 0 ? void 0 : lastBot.metadata) === null || _f === void 0 ? void 0 : _f.artifactIds;
470
526
  if (artifactIds === null || artifactIds === void 0 ? void 0 : artifactIds.length) {
471
527
  const lastId = artifactIds[artifactIds.length - 1];
472
- if (((_e = this.selectedArtifact) === null || _e === void 0 ? void 0 : _e.id) !== lastId) {
528
+ if (((_g = this.selectedArtifact) === null || _g === void 0 ? void 0 : _g.id) !== lastId) {
473
529
  const plugin = this.getArtifactPlugin();
474
530
  const artifact = plugin === null || plugin === void 0 ? void 0 : plugin.getArtifact(lastId);
475
531
  if (artifact) {
@@ -513,6 +569,8 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
513
569
  const templateData = {
514
570
  boxed: this.boxed,
515
571
  showMessages: this.showMessages,
572
+ welcomeMessage: this.welcomeMessage,
573
+ isPendingThread: !!this._pendingThreadId,
516
574
  messages: this.messages,
517
575
  isTyping: this.isBotTyping,
518
576
  loadingIndicator: this.loadingIndicator,
@@ -526,7 +584,7 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
526
584
  uploadedFiles: this.uploadedFiles,
527
585
  isQueryRunning: this.isQueryRunning,
528
586
  showSendButton: this.showSendButton,
529
- enableFileUpload: this.enableFileUpload,
587
+ 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
588
  fileUploadItems: [
531
589
  { id: 'upload-file', label: 'Upload File', icon: 'upload' },
532
590
  { id: 'upload-url', label: 'Upload from URL', icon: 'link' }
@@ -601,10 +659,17 @@ let NrChatbotElement = class NrChatbotElement extends NuralyUIBaseMixin(LitEleme
601
659
  onCreateNew: () => { var _a; (_a = this.controller) === null || _a === void 0 ? void 0 : _a.createThread('New Chat'); },
602
660
  onSelectThread: (threadId) => {
603
661
  var _a;
662
+ if (threadId === this.activeThreadId)
663
+ return;
604
664
  if (this.enableUrlSync) {
605
665
  history.pushState(null, '', `#conversation/${encodeURIComponent(threadId)}`);
606
666
  }
607
667
  (_a = this.controller) === null || _a === void 0 ? void 0 : _a.switchThread(threadId);
668
+ this.dispatchEvent(new CustomEvent('nr-thread-change', {
669
+ detail: { threadId },
670
+ bubbles: true,
671
+ composed: true,
672
+ }));
608
673
  },
609
674
  onDeleteThread: (threadId) => { var _a; (_a = this.controller) === null || _a === void 0 ? void 0 : _a.deleteThread(threadId); },
610
675
  onBookmarkThread: (threadId) => { var _a; (_a = this.controller) === null || _a === void 0 ? void 0 : _a.bookmarkThread(threadId); },
@@ -1194,7 +1259,7 @@ __decorate([
1194
1259
  property({ type: Array })
1195
1260
  ], NrChatbotElement.prototype, "threads", void 0);
1196
1261
  __decorate([
1197
- property({ type: String })
1262
+ property({ type: String, attribute: 'active-thread-id' })
1198
1263
  ], NrChatbotElement.prototype, "activeThreadId", void 0);
1199
1264
  __decorate([
1200
1265
  property({ type: String })
@@ -1208,6 +1273,9 @@ __decorate([
1208
1273
  __decorate([
1209
1274
  property({ type: Boolean })
1210
1275
  ], NrChatbotElement.prototype, "showMessages", void 0);
1276
+ __decorate([
1277
+ property({ type: String, attribute: 'welcome-message' })
1278
+ ], NrChatbotElement.prototype, "welcomeMessage", void 0);
1211
1279
  __decorate([
1212
1280
  property({ type: Boolean })
1213
1281
  ], NrChatbotElement.prototype, "enableFileUpload", void 0);
@@ -1286,6 +1354,9 @@ __decorate([
1286
1354
  __decorate([
1287
1355
  state()
1288
1356
  ], NrChatbotElement.prototype, "_isDragging", void 0);
1357
+ __decorate([
1358
+ state()
1359
+ ], NrChatbotElement.prototype, "_pendingThreadId", void 0);
1289
1360
  NrChatbotElement = __decorate([
1290
1361
  localized(),
1291
1362
  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,12 +408,13 @@ 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 */
@@ -564,16 +553,16 @@ export default css `
564
553
  }
565
554
 
566
555
  .message.user .message__content {
567
- background-color: #7c3aed;
568
- color: #ffffff;
556
+ background-color: var(--nuraly-color-user-bubble-bg, rgb(124, 58, 237));
557
+ color: var(--nuraly-color-user-bubble-fg, rgb(255, 255, 255));
569
558
  border-radius: var(--chatbot-radius, 8px);
570
559
  border: 0 solid transparent;
571
560
  box-shadow: none;
572
561
  }
573
562
 
574
563
  .message.bot .message__content {
575
- background-color: transparent;
576
- color: inherit;
564
+ background-color: var(--nuraly-color-bot-bubble-bg, transparent);
565
+ color: var(--nuraly-color-bot-bubble-fg, inherit);
577
566
  border-radius: 0;
578
567
  border: 0 solid transparent;
579
568
  box-shadow: none;
@@ -1618,10 +1607,10 @@ export default css `
1618
1607
  .artifact-panel {
1619
1608
  width: 400px;
1620
1609
  min-width: 300px;
1610
+ min-height: 0;
1621
1611
  flex-shrink: 0;
1622
1612
  display: flex;
1623
1613
  flex-direction: row;
1624
- background-color: #ffffff;
1625
1614
  overflow: hidden;
1626
1615
  position: relative;
1627
1616
  }
@@ -294,6 +294,7 @@ export interface ChatbotI18nMessages {
294
294
  retryButton: string;
295
295
  startConversationLabel: string;
296
296
  suggestionPrefix: string;
297
+ loadingConversationLabel: string;
297
298
  }
298
299
  export interface ChatbotI18nUrlModal {
299
300
  addUrlTitle: string;
@@ -17,18 +17,18 @@ function renderArtifactContent(artifact) {
17
17
  catch (_a) {
18
18
  prettyContent = artifact.content;
19
19
  }
20
- return html `<pre class="artifact-panel__code"><code>${prettyContent}</code></pre>`;
20
+ return html `<pre class="artifact-panel__code" part="artifact-panel-code"><code>${prettyContent}</code></pre>`;
21
21
  }
22
22
  case 'md':
23
23
  case 'markdown':
24
- return html `<div class="artifact-panel__rendered-md">${unsafeHTML(renderMarkdown(artifact.content))}</div>`;
24
+ return html `<div class="artifact-panel__rendered-md" part="artifact-panel-md">${unsafeHTML(renderMarkdown(artifact.content))}</div>`;
25
25
  case 'html':
26
- return html `<div class="artifact-panel__rendered-html">${unsafeHTML(artifact.content)}</div>`;
26
+ return html `<div class="artifact-panel__rendered-html" part="artifact-panel-html">${unsafeHTML(artifact.content)}</div>`;
27
27
  case 'text':
28
28
  case 'txt':
29
- return html `<div class="artifact-panel__rendered-text">${artifact.content}</div>`;
29
+ return html `<div class="artifact-panel__rendered-text" part="artifact-panel-text">${artifact.content}</div>`;
30
30
  default:
31
- return html `<pre class="artifact-panel__code"><code>${artifact.content}</code></pre>`;
31
+ return html `<pre class="artifact-panel__code" part="artifact-panel-code"><code>${artifact.content}</code></pre>`;
32
32
  }
33
33
  }
34
34
  /**
@@ -43,15 +43,15 @@ export function renderArtifactPanel(data, handlers) {
43
43
  return html `
44
44
  <div class="artifact-panel" part="artifact-panel">
45
45
  <div class="artifact-panel__resize-handle" part="artifact-panel-resize-handle">
46
- <div class="artifact-panel__resize-bar"></div>
46
+ <div class="artifact-panel__resize-bar" part="artifact-panel-resize-bar"></div>
47
47
  </div>
48
- <div class="artifact-panel__body">
49
- <div class="artifact-panel__header">
50
- <div class="artifact-panel__header-info">
51
- <nr-tag size="small" class="artifact-panel__lang-badge">${langLabel}</nr-tag>
52
- <span class="artifact-panel__title">${artifact.title}</span>
48
+ <div class="artifact-panel__body" part="artifact-panel-body">
49
+ <div class="artifact-panel__header" part="artifact-panel-header">
50
+ <div class="artifact-panel__header-info" part="artifact-panel-header-info">
51
+ <nr-tag size="small" class="artifact-panel__lang-badge" part="artifact-panel-lang">${langLabel}</nr-tag>
52
+ <span class="artifact-panel__title" part="artifact-panel-title">${artifact.title}</span>
53
53
  </div>
54
- <div class="artifact-panel__actions">
54
+ <div class="artifact-panel__actions" part="artifact-panel-actions">
55
55
  <nr-button
56
56
  type="text"
57
57
  size="small"
@@ -70,7 +70,7 @@ export function renderArtifactPanel(data, handlers) {
70
70
  ></nr-button>
71
71
  </div>
72
72
  </div>
73
- <div class="artifact-panel__content">
73
+ <div class="artifact-panel__content" part="artifact-panel-content">
74
74
  ${((_a = data.renderContent) === null || _a === void 0 ? void 0 : _a.call(data, artifact)) || renderArtifactContent(artifact)}
75
75
  </div>
76
76
  </div>
@@ -16,6 +16,10 @@ 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;
19
23
  messages: ChatbotMessage[];
20
24
  isTyping: boolean;
21
25
  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)}
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>
@@ -10,9 +10,9 @@ export function renderFileUploadArea(data) {
10
10
  class="file-upload-area ${data.isDragging ? 'file-upload-area--dragging' : ''}"
11
11
  part="file-upload-area"
12
12
  >
13
- <div class="file-upload-area__content">
14
- <nr-icon name="upload" size="xlarge"></nr-icon>
15
- <div class="file-upload-area__text">
13
+ <div class="file-upload-area__content" part="file-upload-area-content">
14
+ <nr-icon name="upload" size="xlarge" part="file-upload-area-icon"></nr-icon>
15
+ <div class="file-upload-area__text" part="file-upload-area-text">
16
16
  ${data.label}
17
17
  </div>
18
18
  </div>