@salesforcedevs/docs-components 0.0.10-edit → 0.0.11-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.
@@ -9,7 +9,6 @@ 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
13
12
  }
14
13
 
15
14
  export default class Chat extends LightningElement {
@@ -17,7 +16,7 @@ export default class Chat extends LightningElement {
17
16
  @api placeholder: string = "Type your message...";
18
17
  @api assistantName: string = "Assistant";
19
18
  @api maxHeight: string = "400px";
20
-
19
+
21
20
  @api
22
21
  get disabled() {
23
22
  return this._disabled;
@@ -36,108 +35,26 @@ export default class Chat extends LightningElement {
36
35
  this._showTimestamp = normalizeBoolean(value);
37
36
  }
38
37
 
39
- @api
40
- get isOpen() {
41
- return this._isOpen;
42
- }
43
-
44
- set isOpen(value) {
45
- this._isOpen = normalizeBoolean(value);
46
- }
47
-
48
38
  @track messages: ChatMessage[] = [];
49
39
  @track currentMessage: string = "";
50
40
  @track isAssistantTyping: boolean = false;
51
-
41
+
52
42
  private _disabled: boolean = false;
53
43
  private _showTimestamp: boolean = false;
54
- private _isOpen: boolean = false;
55
44
  private messageIdCounter: number = 0;
56
45
 
57
- // API Configuration
58
- private static readonly API_URL =
59
- "https://276ca7264b79.ngrok-free.app/api/llm/search";
60
- private static readonly MAX_RESULTS = 5;
61
-
62
- // Development mode flag - set to true if running in development
63
- private static readonly IS_DEVELOPMENT =
64
- window.location.hostname === "localhost" ||
65
- window.location.hostname === "127.0.0.1";
66
-
67
- // localStorage keys for persisting messages and open state
68
- private static readonly STORAGE_KEY = "doc-chat-messages";
69
- private static readonly OPEN_STATE_KEY = "doc-chat-should-open";
70
-
71
46
  connectedCallback() {
72
- console.log("---- Connected callback ------");
73
- // Load existing messages from localStorage
74
- this.loadMessages();
75
-
76
- // Add welcome message only if no existing messages
77
- if (this.messages.length === 0) {
78
- this.addMessage("Hello! How can I help you today?", "assistant");
79
- }
80
-
81
- // Check if chat should be opened after reload
82
- this.checkAndOpenAfterReload();
83
-
84
- // Ensure body has proper class state
85
- this.updateBodyClass();
86
- }
87
-
88
- disconnectedCallback() {
89
- // Clean up body classes when component is destroyed
90
- document.body.classList.remove("chat-open", "chat-closed");
91
- }
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
- });
47
+ // Add a welcome message
48
+ this.addMessage("Hello! How can I help you today?", "assistant");
127
49
  }
128
50
 
129
51
  get chatContainerClass() {
130
52
  return cx(
131
53
  "chat-container",
132
- this.disabled && "chat-container_disabled",
133
- this.isOpen && "chat-container_open"
54
+ this.disabled && "chat-container--disabled"
134
55
  );
135
56
  }
136
57
 
137
- get showTriggerButton() {
138
- return !this.isOpen;
139
- }
140
-
141
58
  get messagesWithTyping() {
142
59
  const messages = [...this.messages];
143
60
  if (this.isAssistantTyping) {
@@ -154,151 +71,36 @@ export default class Chat extends LightningElement {
154
71
  return messages;
155
72
  }
156
73
 
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
- ) {
74
+ private addMessage(text: string, sender: "user" | "assistant") {
170
75
  const timestamp = new Date();
171
76
  const message: ChatMessage = {
172
77
  id: `msg-${this.messageIdCounter++}`,
173
78
  text,
174
79
  timestamp,
175
80
  sender,
176
- formattedTime: this.formatTimestamp(timestamp),
177
- isHTML
81
+ formattedTime: this.formatTimestamp(timestamp)
178
82
  };
179
83
  this.messages = [...this.messages, message];
180
- this.saveMessages();
181
84
  this.scrollToBottom();
182
85
  }
183
86
 
184
87
  private scrollToBottom() {
185
88
  // Use setTimeout to ensure DOM is updated
186
89
  setTimeout(() => {
187
- const messagesContainer =
188
- this.template.querySelector(".chat-messages");
90
+ const messagesContainer = this.template.querySelector('.chat-messages');
189
91
  if (messagesContainer) {
190
92
  messagesContainer.scrollTop = messagesContainer.scrollHeight;
191
93
  }
192
94
  }, 0);
193
95
  }
194
96
 
195
- private updateBodyClass() {
196
- // Update body classes to trigger content shift
197
- if (this.isOpen) {
198
- document.body.classList.remove("chat-closed");
199
- document.body.classList.add("chat-open");
200
- } else {
201
- document.body.classList.remove("chat-open");
202
- document.body.classList.add("chat-closed");
203
- }
204
- }
205
-
206
- private saveMessages() {
207
- try {
208
- const messagesToSave = this.messages.map((msg) => ({
209
- id: msg.id,
210
- text: msg.text,
211
- timestamp: msg.timestamp.toISOString(),
212
- sender: msg.sender,
213
- formattedTime: msg.formattedTime,
214
- isHTML: msg.isHTML || false
215
- }));
216
- localStorage.setItem(
217
- Chat.STORAGE_KEY,
218
- JSON.stringify(messagesToSave)
219
- );
220
- } catch (error) {
221
- console.warn("Failed to save messages to localStorage:", error);
222
- }
223
- }
224
-
225
- private loadMessages() {
226
- try {
227
- const savedMessages = localStorage.getItem(Chat.STORAGE_KEY);
228
- if (savedMessages) {
229
- const parsedMessages = JSON.parse(savedMessages);
230
- this.messages = parsedMessages.map((msg: any) => ({
231
- id: msg.id,
232
- text: msg.text,
233
- timestamp: new Date(msg.timestamp),
234
- sender: msg.sender,
235
- formattedTime: msg.formattedTime,
236
- isHTML: msg.isHTML || false
237
- }));
238
-
239
- // Update message counter to avoid ID conflicts
240
- const lastId =
241
- this.messages.length > 0
242
- ? Math.max(
243
- ...this.messages.map(
244
- (m) =>
245
- parseInt(m.id.replace("msg-", ""), 10) ||
246
- 0
247
- )
248
- )
249
- : 0;
250
- this.messageIdCounter = lastId + 1;
251
- }
252
- } catch (error) {
253
- console.warn("Failed to load messages from localStorage:", error);
254
- this.messages = [];
255
- }
256
- }
257
-
258
- private clearMessages() {
259
- try {
260
- localStorage.removeItem(Chat.STORAGE_KEY);
261
- this.messages = [];
262
- this.messageIdCounter = 0;
263
- // Add welcome message after clearing
264
- this.addMessage("Hello! How can I help you today?", "assistant");
265
- } catch (error) {
266
- console.warn("Failed to clear messages from localStorage:", error);
267
- }
268
- }
269
-
270
- private checkAndOpenAfterReload() {
271
- try {
272
- const shouldOpen = localStorage.getItem(Chat.OPEN_STATE_KEY);
273
- if (shouldOpen === "true") {
274
- // Clear the flag
275
- localStorage.removeItem(Chat.OPEN_STATE_KEY);
276
- // Open the chat
277
- this._isOpen = true;
278
- this.updateBodyClass();
279
-
280
- // Dispatch custom event to notify parent components
281
- this.dispatchEvent(
282
- new CustomEvent("chatopened", {
283
- detail: { opened: true }
284
- })
285
- );
286
- }
287
- } catch (error) {
288
- console.warn(
289
- "Failed to check open state from localStorage:",
290
- error
291
- );
292
- }
293
- }
294
-
295
97
  handleInputChange(event: Event) {
296
98
  const target = event.target as HTMLInputElement;
297
99
  this.currentMessage = target.value;
298
100
  }
299
101
 
300
102
  handleKeyDown(event: KeyboardEvent) {
301
- if (event.key === "Enter" && !event.shiftKey) {
103
+ if (event.key === 'Enter' && !event.shiftKey) {
302
104
  event.preventDefault();
303
105
  this.sendMessage();
304
106
  }
@@ -319,268 +121,43 @@ export default class Chat extends LightningElement {
319
121
  this.currentMessage = "";
320
122
 
321
123
  // Clear input
322
- const input = this.template.querySelector(
323
- ".chat-input"
324
- ) as HTMLInputElement;
124
+ const input = this.template.querySelector('.chat-input') as HTMLInputElement;
325
125
  if (input) {
326
126
  input.value = "";
327
127
  }
328
128
 
329
- // Show typing indicator
129
+ // Simulate assistant typing
330
130
  this.isAssistantTyping = true;
331
-
332
- // Get real assistant response from API
333
- // Add a small delay to show the typing indicator
131
+
132
+ // Simulate assistant response after a delay
334
133
  setTimeout(() => {
335
- this.getAssistantResponse(userMessage);
336
- }, 500);
337
- }
338
-
339
- private async callChatAPI(userMessage: string): Promise<string> {
340
- try {
341
- // Format chat history for API (exclude the current user message)
342
- const chatHistory = this.messages.map((msg) => ({
343
- text: msg.text,
344
- sender: msg.sender
345
- }));
346
-
347
- const requestBody = {
348
- query: userMessage,
349
- maxResults: Chat.MAX_RESULTS,
350
- chatHistory: chatHistory
351
- };
352
-
353
- console.log("Sending API request:", requestBody);
354
-
355
- const response = await fetch(Chat.API_URL, {
356
- method: "POST",
357
- headers: {
358
- "Content-Type": "application/json",
359
- "ngrok-skip-browser-warning": "true" // Skip ngrok browser warning
360
- },
361
- body: JSON.stringify(requestBody)
362
- });
363
-
364
- if (!response.ok) {
365
- const errorText = await response.text();
366
- console.error("API Error Response:", errorText);
367
- throw new Error(
368
- `API request failed with status: ${response.status}`
369
- );
370
- }
371
-
372
- const data = await response.json();
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
-
385
- if (!responseText) {
386
- console.warn("No response text found in API response:", data);
387
- return "I received your message but couldn't generate a proper response.";
388
- }
389
-
390
- return responseText;
391
- } catch (error) {
392
- console.error("Chat API error:", error);
393
-
394
- // Provide more specific error messages for CORS issues
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.";
397
- }
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.";
405
- }
406
-
407
- return "I apologize, but I'm having trouble connecting to the service right now. Please try again later.";
408
- }
409
- }
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
-
496
- private async getAssistantResponse(userMessage: string) {
497
- try {
498
- const response = await this.callChatAPI(userMessage);
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);
507
- } catch (error) {
508
- console.error("Error getting assistant response:", error);
509
- this.addMessage(
510
- "I apologize, but I encountered an error while processing your request. Please try again.",
511
- "assistant"
512
- );
513
- } finally {
514
134
  this.isAssistantTyping = false;
135
+ this.simulateAssistantResponse(userMessage);
136
+ }, 1000 + Math.random() * 2000);
137
+ }
138
+
139
+ private simulateAssistantResponse(userMessage: string) {
140
+ // Simple response simulation - in a real implementation, this would call an API
141
+ let response = `I received your message: "${userMessage}". `;
142
+
143
+ if (userMessage.toLowerCase().includes("hello") || userMessage.toLowerCase().includes("hi")) {
144
+ response = "Hello! Nice to meet you. How can I assist you today?";
145
+ } else if (userMessage.toLowerCase().includes("help")) {
146
+ response = "I'm here to help! Feel free to ask me any questions about the documentation or topics you're interested in.";
147
+ } else if (userMessage.toLowerCase().includes("documentation") || userMessage.toLowerCase().includes("docs")) {
148
+ response = "I can help you navigate the documentation. What specific topic are you looking for?";
149
+ } else {
150
+ response = "That's an interesting question. Let me help you with that. Could you provide more details about what you're looking for?";
515
151
  }
152
+
153
+ this.addMessage(response, "assistant");
516
154
  }
517
155
 
518
156
  formatTimestamp(timestamp: Date) {
519
- return timestamp.toLocaleTimeString([], {
520
- hour: "2-digit",
521
- minute: "2-digit"
522
- });
157
+ return timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
523
158
  }
524
159
 
525
160
  get sendButtonDisabled() {
526
161
  return this.disabled || !this.currentMessage.trim();
527
162
  }
528
-
529
- handleCloseClick() {
530
- this._isOpen = false;
531
- this.updateBodyClass();
532
-
533
- // Dispatch custom event to notify parent components
534
- this.dispatchEvent(
535
- new CustomEvent("chatclosed", {
536
- detail: { closed: true }
537
- })
538
- );
539
- }
540
-
541
- handleClearClick() {
542
- this.clearMessages();
543
-
544
- // Dispatch custom event to notify parent components
545
- this.dispatchEvent(
546
- new CustomEvent("chatcleared", {
547
- detail: { cleared: true }
548
- })
549
- );
550
- }
551
-
552
- handleOpenClick() {
553
- try {
554
- // Set flag to open chat after reload
555
- localStorage.setItem(Chat.OPEN_STATE_KEY, "true");
556
- } catch (error) {
557
- console.warn("Failed to set open state in localStorage:", error);
558
- }
559
- // Hard reload the page to clear cache when opening chat
560
- window.location.reload();
561
- }
562
-
563
- openChat() {
564
- try {
565
- // Set flag to open chat after reload
566
- localStorage.setItem(Chat.OPEN_STATE_KEY, "true");
567
- } catch (error) {
568
- console.warn("Failed to set open state in localStorage:", error);
569
- }
570
-
571
- // Hard reload the page to clear cache when opening chat
572
- window.location.reload();
573
- }
574
-
575
- closeChat() {
576
- this._isOpen = false;
577
- this.updateBodyClass();
578
-
579
- // Dispatch custom event to notify parent components
580
- this.dispatchEvent(
581
- new CustomEvent("chatclosed", {
582
- detail: { closed: true }
583
- })
584
- );
585
- }
586
- }
163
+ }
@@ -42,23 +42,23 @@
42
42
  <slot name="doc-phase"></slot>
43
43
  <slot name="version-banner"></slot>
44
44
  <div class="content-body-container">
45
- <div class="content-body">
46
- <doc-breadcrumbs
47
- lwc:if={showBreadcrumbs}
48
- breadcrumbs={breadcrumbs}
49
- ></doc-breadcrumbs>
50
- <slot onslotchange={onSlotChange}></slot>
51
- <doc-sprig-survey
52
- lwc:if={shouldDisplayFeedback}
53
- ></doc-sprig-survey>
54
- </div>
55
- <div lwc:if={showToc} class="right-nav-bar is-sticky">
56
- <dx-toc
57
- header={tocTitle}
58
- options={tocOptions}
59
- value={tocValue}
60
- ></dx-toc>
61
- </div>
45
+ <div class="content-body">
46
+ <doc-breadcrumbs
47
+ lwc:if={showBreadcrumbs}
48
+ breadcrumbs={breadcrumbs}
49
+ ></doc-breadcrumbs>
50
+ <slot onslotchange={onSlotChange}></slot>
51
+ <doc-sprig-survey
52
+ lwc:if={shouldDisplayFeedback}
53
+ ></doc-sprig-survey>
54
+ </div>
55
+ <div lwc:if={showToc} class="right-nav-bar is-sticky">
56
+ <dx-toc
57
+ header={tocTitle}
58
+ options={tocOptions}
59
+ value={tocValue}
60
+ ></dx-toc>
61
+ </div>
62
62
  </div>
63
63
  <div lwc:if={showFooter} class="footer-container">
64
64
  <dx-footer variant="no-signup"></dx-footer>