@salesforcedevs/docs-components 0.0.37-chat → 0.0.39-chat

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/docs-components",
3
- "version": "0.0.37-chat",
3
+ "version": "0.0.39-chat",
4
4
  "description": "Docs Lightning web components for DSC",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
@@ -137,7 +137,7 @@ body.chat-closed .global-header {
137
137
  /* Chat container */
138
138
  .chat-container {
139
139
  height: 100vh;
140
- width: 400px;
140
+ width: 700px;
141
141
  background-color: #fff;
142
142
  box-shadow: -8px 0 32px rgb(0 0 0 / 12%), -2px 0 8px rgb(0 0 0 / 8%);
143
143
  transform: translateX(100%);
@@ -429,6 +429,68 @@ body.chat-closed .global-header {
429
429
  font-weight: 400;
430
430
  }
431
431
 
432
+ /* HTML content styling for rich messages */
433
+ .message-text .chat-link {
434
+ color: #0176d3;
435
+ text-decoration: none;
436
+ border-bottom: 1px solid transparent;
437
+ transition: all 0.2s ease;
438
+ }
439
+
440
+ .message-text .chat-link:hover {
441
+ color: #005fb2;
442
+ border-bottom-color: #005fb2;
443
+ }
444
+
445
+ .message-text .chat-list {
446
+ margin: 12px 0;
447
+ padding-left: 20px;
448
+ counter-reset: none;
449
+ }
450
+
451
+ .message-text .chat-list li {
452
+ margin: 12px 0;
453
+ line-height: 1.6;
454
+ font-weight: 500;
455
+ }
456
+
457
+ .message-text .chat-description {
458
+ font-weight: 400;
459
+ color: #666;
460
+ font-size: 14px;
461
+ line-height: 1.5;
462
+ margin-top: 4px;
463
+ display: block;
464
+ }
465
+
466
+ .message-text strong {
467
+ font-weight: 600;
468
+ color: #181818;
469
+ }
470
+
471
+ .message-text em {
472
+ font-style: italic;
473
+ color: #555;
474
+ }
475
+
476
+ /* Style for line breaks in HTML content */
477
+ .message-text br {
478
+ margin: 4px 0;
479
+ }
480
+
481
+ /* Ensure proper spacing for HTML content */
482
+ .message-text p {
483
+ margin: 8px 0;
484
+ }
485
+
486
+ .message-text p:first-child {
487
+ margin-top: 0;
488
+ }
489
+
490
+ .message-text p:last-child {
491
+ margin-bottom: 0;
492
+ }
493
+
432
494
  .message-timestamp {
433
495
  font-size: 12px;
434
496
  color: #706e6b;
@@ -175,7 +175,16 @@
175
175
  </div>
176
176
  </template>
177
177
  <template lwc:else>
178
- <p class="message-text">{message.text}</p>
178
+ <template lwc:if={message.isHTML}>
179
+ <div
180
+ class="message-text html-content"
181
+ lwc:dom="manual"
182
+ data-message-id={message.id}
183
+ ></div>
184
+ </template>
185
+ <template lwc:else>
186
+ <p class="message-text">{message.text}</p>
187
+ </template>
179
188
  </template>
180
189
  </div>
181
190
  <template lwc:if={showTimestamp}>
@@ -9,6 +9,7 @@ interface ChatMessage {
9
9
  sender: "user" | "assistant";
10
10
  isTyping?: boolean;
11
11
  formattedTime?: string;
12
+ isHTML?: boolean; // Flag to indicate if content should be rendered as HTML
12
13
  }
13
14
 
14
15
  export default class Chat extends LightningElement {
@@ -54,11 +55,14 @@ export default class Chat extends LightningElement {
54
55
  private messageIdCounter: number = 0;
55
56
 
56
57
  // API Configuration
57
- private static readonly API_URL = "https://276ca7264b79.ngrok-free.app/api/llm/search";
58
+ private static readonly API_URL =
59
+ "https://276ca7264b79.ngrok-free.app/api/llm/search";
58
60
  private static readonly MAX_RESULTS = 5;
59
-
61
+
60
62
  // Development mode flag - set to true if running in development
61
- private static readonly IS_DEVELOPMENT = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
63
+ private static readonly IS_DEVELOPMENT =
64
+ window.location.hostname === "localhost" ||
65
+ window.location.hostname === "127.0.0.1";
62
66
 
63
67
  // localStorage keys for persisting messages and open state
64
68
  private static readonly STORAGE_KEY = "doc-chat-messages";
@@ -86,6 +90,42 @@ export default class Chat extends LightningElement {
86
90
  document.body.classList.remove("chat-open", "chat-closed");
87
91
  }
88
92
 
93
+ renderedCallback() {
94
+ // Handle HTML content rendering for messages with isHTML flag
95
+ this.renderHTMLMessages();
96
+ }
97
+
98
+ private renderHTMLMessages() {
99
+ console.log("=== renderHTMLMessages called ===");
100
+ console.log("All messages:", this.messages);
101
+ console.log(
102
+ "HTML messages:",
103
+ this.messages.filter((msg) => msg.isHTML)
104
+ );
105
+
106
+ // Use a simpler selector approach
107
+ const htmlMessageElements = this.template.querySelectorAll(
108
+ ".html-content[data-message-id]"
109
+ );
110
+ console.log("Found HTML elements:", htmlMessageElements.length);
111
+
112
+ htmlMessageElements.forEach((element) => {
113
+ const messageId = element.getAttribute("data-message-id");
114
+ console.log("Processing element with ID:", messageId);
115
+
116
+ const message = this.messages.find(
117
+ (msg) => msg.id === messageId && msg.isHTML
118
+ );
119
+ console.log("Found message:", message);
120
+
121
+ if (message) {
122
+ console.log("Setting innerHTML for message:", messageId);
123
+ console.log("Content:", message.text);
124
+ element.innerHTML = message.text;
125
+ }
126
+ });
127
+ }
128
+
89
129
  get chatContainerClass() {
90
130
  return cx(
91
131
  "chat-container",
@@ -114,14 +154,27 @@ export default class Chat extends LightningElement {
114
154
  return messages;
115
155
  }
116
156
 
117
- private addMessage(text: string, sender: "user" | "assistant") {
157
+ // Helper method to get HTML content for a message
158
+ getMessageHTML(messageId: string): string {
159
+ const message = this.messages.find(
160
+ (msg) => msg.id === messageId && msg.isHTML
161
+ );
162
+ return message ? message.text : "";
163
+ }
164
+
165
+ private addMessage(
166
+ text: string,
167
+ sender: "user" | "assistant",
168
+ isHTML: boolean = false
169
+ ) {
118
170
  const timestamp = new Date();
119
171
  const message: ChatMessage = {
120
172
  id: `msg-${this.messageIdCounter++}`,
121
173
  text,
122
174
  timestamp,
123
175
  sender,
124
- formattedTime: this.formatTimestamp(timestamp)
176
+ formattedTime: this.formatTimestamp(timestamp),
177
+ isHTML
125
178
  };
126
179
  this.messages = [...this.messages, message];
127
180
  this.saveMessages();
@@ -157,7 +210,8 @@ export default class Chat extends LightningElement {
157
210
  text: msg.text,
158
211
  timestamp: msg.timestamp.toISOString(),
159
212
  sender: msg.sender,
160
- formattedTime: msg.formattedTime
213
+ formattedTime: msg.formattedTime,
214
+ isHTML: msg.isHTML || false
161
215
  }));
162
216
  localStorage.setItem(
163
217
  Chat.STORAGE_KEY,
@@ -178,7 +232,8 @@ export default class Chat extends LightningElement {
178
232
  text: msg.text,
179
233
  timestamp: new Date(msg.timestamp),
180
234
  sender: msg.sender,
181
- formattedTime: msg.formattedTime
235
+ formattedTime: msg.formattedTime,
236
+ isHTML: msg.isHTML || false
182
237
  }));
183
238
 
184
239
  // Update message counter to avoid ID conflicts
@@ -284,7 +339,7 @@ export default class Chat extends LightningElement {
284
339
  private async callChatAPI(userMessage: string): Promise<string> {
285
340
  try {
286
341
  // Format chat history for API (exclude the current user message)
287
- const chatHistory = this.messages.map(msg => ({
342
+ const chatHistory = this.messages.map((msg) => ({
288
343
  text: msg.text,
289
344
  sender: msg.sender
290
345
  }));
@@ -295,62 +350,164 @@ export default class Chat extends LightningElement {
295
350
  chatHistory: chatHistory
296
351
  };
297
352
 
298
- console.log('Sending API request:', requestBody);
353
+ console.log("Sending API request:", requestBody);
299
354
 
300
355
  const response = await fetch(Chat.API_URL, {
301
- method: 'POST',
302
- mode: 'cors', // Explicitly set CORS mode
356
+ method: "POST",
303
357
  headers: {
304
- 'Content-Type': 'application/json',
305
- 'ngrok-skip-browser-warning': 'true', // Skip ngrok browser warning
306
- ...(Chat.IS_DEVELOPMENT && { 'Access-Control-Allow-Origin': '*' })
358
+ "Content-Type": "application/json",
359
+ "ngrok-skip-browser-warning": "true" // Skip ngrok browser warning
307
360
  },
308
361
  body: JSON.stringify(requestBody)
309
362
  });
310
363
 
311
364
  if (!response.ok) {
312
365
  const errorText = await response.text();
313
- console.error('API Error Response:', errorText);
314
- throw new Error(`API request failed with status: ${response.status}`);
366
+ console.error("API Error Response:", errorText);
367
+ throw new Error(
368
+ `API request failed with status: ${response.status}`
369
+ );
315
370
  }
316
371
 
317
372
  const data = await response.json();
318
- console.log('API Response:', data);
319
-
320
- // Handle different possible response formats
321
- const responseText = data.answer || data.text || data.response || data.result || data.message;
322
-
373
+ console.log("API Response:", data);
374
+
375
+ // Handle the new API response format with "data" field
376
+ const responseText =
377
+ data.data ||
378
+ data.answer ||
379
+ data.text ||
380
+ data.response ||
381
+ data.result ||
382
+ data.message;
383
+ console.log("Response Text:", responseText);
384
+
323
385
  if (!responseText) {
324
- console.warn('No response text found in API response:', data);
325
- return 'I received your message but couldn\'t generate a proper response.';
386
+ console.warn("No response text found in API response:", data);
387
+ return "I received your message but couldn't generate a proper response.";
326
388
  }
327
-
389
+
328
390
  return responseText;
329
-
330
391
  } catch (error) {
331
- console.error('Chat API error:', error);
332
-
392
+ console.error("Chat API error:", error);
393
+
333
394
  // Provide more specific error messages for CORS issues
334
- if (error instanceof TypeError && error.message.includes('fetch')) {
335
- return 'I\'m having trouble connecting to the service. This might be a CORS configuration issue. Please check your internet connection and try again.';
395
+ if (error instanceof TypeError && error.message.includes("fetch")) {
396
+ return "I'm having trouble connecting to the service. This might be a CORS configuration issue. Please check your internet connection and try again.";
336
397
  }
337
-
338
- if (error instanceof Error && (error.message.includes('CORS') || error.message.includes('cross-origin'))) {
339
- return 'I\'m having trouble connecting due to browser security restrictions. Please ask your administrator to configure CORS headers on the backend API.';
398
+
399
+ if (
400
+ error instanceof Error &&
401
+ (error.message.includes("CORS") ||
402
+ error.message.includes("cross-origin"))
403
+ ) {
404
+ return "I'm having trouble connecting due to browser security restrictions. Please ask your administrator to configure CORS headers on the backend API.";
340
405
  }
341
-
342
- return 'I apologize, but I\'m having trouble connecting to the service right now. Please try again later.';
406
+
407
+ return "I apologize, but I'm having trouble connecting to the service right now. Please try again later.";
343
408
  }
344
409
  }
345
410
 
411
+ private parseMarkdownToHTML(text: string): string {
412
+ // Convert markdown-like text to HTML
413
+ let html = text;
414
+
415
+ console.log("Original text:", text);
416
+
417
+ // First, convert markdown links [text](url) to HTML links
418
+ // Make sure links are absolute (relative to root) by adding leading slash if missing
419
+ html = html.replace(
420
+ /\[([^\]]+)\]\(([^)]+)\)/g,
421
+ (match, linkText, url) => {
422
+ // If URL doesn't start with http, https, or /, make it absolute
423
+ let absoluteUrl = url;
424
+ if (!url.startsWith('http') && !url.startsWith('/')) {
425
+ absoluteUrl = '/' + url;
426
+ }
427
+ return `<a href="${absoluteUrl}" target="_blank" rel="noopener noreferrer" class="chat-link">${linkText}</a>`;
428
+ }
429
+ );
430
+
431
+ // Handle numbered lists with descriptions (more complex pattern)
432
+ // Split into lines to process each line
433
+ const lines = html.split("\n");
434
+ const processedLines = [];
435
+ let inList = false;
436
+
437
+ for (let i = 0; i < lines.length; i++) {
438
+ const line = lines[i];
439
+
440
+ // Check if this is a numbered list item (starts with number and dot)
441
+ if (/^\d+\.\s+/.test(line)) {
442
+ if (!inList) {
443
+ processedLines.push('<ol class="chat-list">');
444
+ inList = true;
445
+ }
446
+ // Extract the content after the number
447
+ const content = line.replace(/^\d+\.\s+/, "");
448
+ processedLines.push(`<li>${content}`);
449
+
450
+ // Check if next line is indented (description)
451
+ if (i + 1 < lines.length && /^\s{2,}/.test(lines[i + 1])) {
452
+ i++; // Skip to next line
453
+ const description = lines[i].trim();
454
+ processedLines.push(
455
+ `<br><span class="chat-description">${description}</span></li>`
456
+ );
457
+ } else {
458
+ processedLines.push("</li>");
459
+ }
460
+ } else if (line.trim() === "" && inList) {
461
+ // Empty line might end the list
462
+ continue;
463
+ } else {
464
+ if (inList && line.trim() !== "") {
465
+ processedLines.push("</ol>");
466
+ inList = false;
467
+ }
468
+ if (line.trim() !== "") {
469
+ processedLines.push(line);
470
+ }
471
+ }
472
+ }
473
+
474
+ // Close any open list
475
+ if (inList) {
476
+ processedLines.push("</ol>");
477
+ }
478
+
479
+ html = processedLines.join("<br>");
480
+
481
+ // Clean up extra breaks
482
+ html = html.replace(/<br><br>/g, "<br>");
483
+ html = html.replace(/^<br>/, "");
484
+
485
+ // Convert **bold** to <strong>
486
+ html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
487
+
488
+ // Convert *italic* to <em>
489
+ html = html.replace(/\*([^*]+)\*/g, "<em>$1</em>");
490
+
491
+ console.log("Processed HTML:", html);
492
+
493
+ return html;
494
+ }
495
+
346
496
  private async getAssistantResponse(userMessage: string) {
347
497
  try {
348
498
  const response = await this.callChatAPI(userMessage);
349
- this.addMessage(response, "assistant");
499
+ // Convert markdown to HTML for rich content display
500
+ const htmlContent = this.parseMarkdownToHTML(response);
501
+ this.addMessage(htmlContent, "assistant", true); // true indicates HTML content
502
+
503
+ // Force a re-render after adding the message
504
+ setTimeout(() => {
505
+ this.renderHTMLMessages();
506
+ }, 100);
350
507
  } catch (error) {
351
- console.error('Error getting assistant response:', error);
508
+ console.error("Error getting assistant response:", error);
352
509
  this.addMessage(
353
- 'I apologize, but I encountered an error while processing your request. Please try again.',
510
+ "I apologize, but I encountered an error while processing your request. Please try again.",
354
511
  "assistant"
355
512
  );
356
513
  } finally {