@nuraly/lumenui 0.8.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.
@@ -420,6 +420,11 @@ export default css `
420
420
  justify-content: flex-start; /* Always align messages to top */
421
421
  }
422
422
 
423
+ .messages--inverted {
424
+ flex-direction: column-reverse;
425
+ justify-content: flex-start;
426
+ }
427
+
423
428
  .empty-state {
424
429
  display: flex;
425
430
  flex-direction: column;
@@ -560,6 +565,54 @@ export default css `
560
565
  box-shadow: none;
561
566
  }
562
567
 
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;
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
+
563
616
  .message.bot .message__content {
564
617
  background-color: var(--nuraly-color-bot-bubble-bg, transparent);
565
618
  color: var(--nuraly-color-bot-bubble-fg, inherit);
@@ -295,6 +295,8 @@ export interface ChatbotI18nMessages {
295
295
  startConversationLabel: string;
296
296
  suggestionPrefix: string;
297
297
  loadingConversationLabel: string;
298
+ showMoreLabel: string;
299
+ showLessLabel: string;
298
300
  }
299
301
  export interface ChatbotI18nUrlModal {
300
302
  addUrlTitle: string;
@@ -20,6 +20,8 @@ export interface ChatbotMainTemplateData {
20
20
  welcomeMessage?: string;
21
21
  /** True when activeThreadId points at a thread that has not yet been loaded. Renders a loading state in the messages area. */
22
22
  isPendingThread?: boolean;
23
+ /** Anchor messages to the bottom via flex-direction: column-reverse. New messages stay anchored without JS scroll. */
24
+ invertedScroll?: boolean;
23
25
  messages: ChatbotMessage[];
24
26
  isTyping: boolean;
25
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, data.welcomeMessage, data.isPendingThread)}
55
+ : nothing, handlers.message, data.i18n, data.welcomeMessage, data.isPendingThread, data.invertedScroll)}
56
56
  <slot name="messages"></slot>
57
57
  </div>
58
58
  `;
@@ -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
@@ -22,5 +24,5 @@ export declare function renderMessage(message: ChatbotMessage, handlers: Message
22
24
  export declare function renderBotTypingIndicator(isTyping: boolean, loadingIndicator: ChatbotLoadingType, loadingText?: string): TemplateResult | typeof nothing;
23
25
  export declare function renderEmptyState(i18n: ChatbotI18n, welcomeMessage?: string): TemplateResult;
24
26
  export declare function renderThreadLoading(i18n: ChatbotI18n): TemplateResult;
25
- export declare function renderMessages(messages: ChatbotMessage[], suggestions: TemplateResult | typeof nothing, typingIndicator: TemplateResult | typeof nothing, messageHandlers: MessageTemplateHandlers, i18n: ChatbotI18n, welcomeMessage?: string, isPendingThread?: boolean): 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;
26
28
  //# sourceMappingURL=message.template.d.ts.map
@@ -6,6 +6,7 @@
6
6
  import { html, nothing } from 'lit';
7
7
  import { unsafeHTML } from 'lit/directives/unsafe-html.js';
8
8
  import { classMap } from 'lit/directives/class-map.js';
9
+ import { repeat } from 'lit/directives/repeat.js';
9
10
  import { ChatbotLoadingType } from '../chatbot.types.js';
10
11
  import { formatTimestamp } from '../utils/format.js';
11
12
  /**
@@ -61,7 +62,7 @@ function getFileExtension(name, mimeType) {
61
62
  * Renders a single message
62
63
  */
63
64
  export function renderMessage(message, handlers, i18n) {
64
- var _a, _b, _c, _d, _e, _f, _g, _h;
65
+ var _a, _b, _c, _d, _e, _f;
65
66
  const isError = (_a = message.text) === null || _a === void 0 ? void 0 : _a.includes('[ERROR_START]');
66
67
  const messageClasses = {
67
68
  error: !!message.error || isError,
@@ -69,6 +70,15 @@ export function renderMessage(message, handlers, i18n) {
69
70
  [message.sender]: true,
70
71
  };
71
72
  const role = message.sender;
73
+ const rawText = (_c = (_b = message.text) === null || _b === void 0 ? void 0 : _b.trim()) !== null && _c !== void 0 ? _c : '';
74
+ const threshold = (_d = handlers.collapseThreshold) !== null && _d !== void 0 ? _d : 0;
75
+ const collapsible = role === 'user' && !isError && threshold > 0 && rawText.length > threshold;
76
+ const expanded = collapsible ? !!((_e = handlers.isExpanded) === null || _e === void 0 ? void 0 : _e.call(handlers, message.id)) : true;
77
+ const innerContent = isError
78
+ ? renderErrorMessage(rawText)
79
+ : ((_f = message === null || message === void 0 ? void 0 : message.metadata) === null || _f === void 0 ? void 0 : _f.renderAsHtml)
80
+ ? unsafeHTML(rawText)
81
+ : unsafeHTML(rawText.replaceAll('\n', '<br>'));
72
82
  return html `
73
83
  <div
74
84
  class="message ${classMap(messageClasses)}"
@@ -77,11 +87,21 @@ export function renderMessage(message, handlers, i18n) {
77
87
  data-id="${message.id}"
78
88
  >
79
89
  <div class="message__content" part=${`message-content message-content-${role}`}>
80
- ${isError
81
- ? renderErrorMessage((_c = (_b = message.text) === null || _b === void 0 ? void 0 : _b.trim()) !== null && _c !== void 0 ? _c : '')
82
- : ((_d = message === null || message === void 0 ? void 0 : message.metadata) === null || _d === void 0 ? void 0 : _d.renderAsHtml)
83
- ? unsafeHTML((_f = (_e = message.text) === null || _e === void 0 ? void 0 : _e.trim()) !== null && _f !== void 0 ? _f : '')
84
- : unsafeHTML(((_h = (_g = message.text) === null || _g === void 0 ? void 0 : _g.trim()) !== null && _h !== void 0 ? _h : '').replaceAll('\n', '<br>'))}
90
+ ${collapsible ? html `
91
+ <div
92
+ class="message__text-collapsible ${expanded ? 'message__text-collapsible--expanded' : ''}"
93
+ part="message-text-collapsible"
94
+ >
95
+ <div class="message__text-inner">${innerContent}</div>
96
+ </div>
97
+ <button
98
+ class="message__show-more-toggle"
99
+ part="message-show-more"
100
+ type="button"
101
+ data-message-toggle="${message.id}"
102
+ aria-expanded="${expanded ? 'true' : 'false'}"
103
+ >${expanded ? i18n.messages.showLessLabel : i18n.messages.showMoreLabel}</button>
104
+ ` : innerContent}
85
105
  </div>
86
106
  ${message.files && message.files.length > 0 ? html `
87
107
  <div class="message__attachments" part="message-attachments" role="list" aria-label="${i18n.messages.attachedFilesLabel}">
@@ -219,15 +239,28 @@ export function renderThreadLoading(i18n) {
219
239
  </div>
220
240
  `;
221
241
  }
222
- export function renderMessages(messages, suggestions, typingIndicator, messageHandlers, i18n, welcomeMessage, isPendingThread) {
223
- return html `
224
- <div class="messages" part="messages">
225
- ${messages.length === 0
242
+ export function renderMessages(messages, suggestions, typingIndicator, messageHandlers, i18n, welcomeMessage, isPendingThread, invertedScroll) {
243
+ const emptyContent = messages.length === 0
226
244
  ? isPendingThread
227
245
  ? renderThreadLoading(i18n)
228
246
  : renderEmptyState(i18n, welcomeMessage)
229
- : nothing}
230
- ${messages.map((message) => renderMessage(message, messageHandlers, i18n))}
247
+ : nothing;
248
+ const renderMsg = (m) => renderMessage(m, messageHandlers, i18n);
249
+ if (invertedScroll) {
250
+ const reversed = [...messages].reverse();
251
+ return html `
252
+ <div class="messages messages--inverted" part="messages">
253
+ ${typingIndicator}
254
+ ${suggestions}
255
+ ${repeat(reversed, (m) => m.id, renderMsg)}
256
+ ${emptyContent}
257
+ </div>
258
+ `;
259
+ }
260
+ return html `
261
+ <div class="messages" part="messages">
262
+ ${emptyContent}
263
+ ${repeat(messages, (m) => m.id, renderMsg)}
231
264
  ${suggestions}
232
265
  ${typingIndicator}
233
266
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuraly/lumenui",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "A comprehensive collection of enterprise-class web components built with Lit and TypeScript",
5
5
  "type": "module",
6
6
  "main": "dist/nuralyui.bundle.js",