@alliance-droid/chat-widget 0.1.6 → 0.1.7

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.
@@ -126,6 +126,10 @@ function handleServerMessage(message) {
126
126
  break;
127
127
  case 'message':
128
128
  if (message.id && message.content && message.sender) {
129
+ // Clear typing indicator when message arrives from AI or human
130
+ if (message.sender === 'ai' || message.sender === 'human') {
131
+ chatStore.setTyping(false, null);
132
+ }
129
133
  const msg = {
130
134
  id: message.id,
131
135
  content: message.content,
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { marked } from 'marked';
2
3
  import type { Message } from '../types.js';
3
4
 
4
5
  interface Props {
@@ -11,6 +12,24 @@
11
12
  const isUser = $derived(message.sender === 'user');
12
13
  const isSystem = $derived(message.sender === 'system');
13
14
 
15
+ // Configure marked for safe rendering
16
+ marked.setOptions({
17
+ breaks: true,
18
+ gfm: true
19
+ });
20
+
21
+ // Render markdown for AI/human messages, plain text for user messages
22
+ const renderedContent = $derived(() => {
23
+ if (isUser || isSystem) {
24
+ return null; // Use plain text
25
+ }
26
+ try {
27
+ return marked.parse(message.content);
28
+ } catch {
29
+ return null;
30
+ }
31
+ });
32
+
14
33
  const bubbleClasses = $derived(() => {
15
34
  if (isSystem) {
16
35
  return 'bg-surface text-foreground-secondary text-center text-sm italic mx-auto';
@@ -48,7 +67,13 @@
48
67
  {/if}
49
68
 
50
69
  <div class="rounded-2xl px-4 py-2 {bubbleClasses()}" style="max-width: 80%; overflow: hidden; word-wrap: break-word;">
51
- <p style="white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere;">{message.content}</p>
70
+ {#if renderedContent()}
71
+ <div class="prose prose-sm dark:prose-invert max-w-none" style="word-break: break-word; overflow-wrap: anywhere;">
72
+ {@html renderedContent()}
73
+ </div>
74
+ {:else}
75
+ <p style="white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere;">{message.content}</p>
76
+ {/if}
52
77
  {#if message.streaming}
53
78
  <span class="inline-block w-2 h-4 bg-current animate-pulse ml-1">|</span>
54
79
  {/if}
@@ -58,3 +83,58 @@
58
83
  {formatTime(message.timestamp)}
59
84
  </span>
60
85
  </div>
86
+
87
+ <style>
88
+ .prose :global(p) {
89
+ margin: 0.5em 0;
90
+ }
91
+ .prose :global(p:first-child) {
92
+ margin-top: 0;
93
+ }
94
+ .prose :global(p:last-child) {
95
+ margin-bottom: 0;
96
+ }
97
+ .prose :global(ul), .prose :global(ol) {
98
+ margin: 0.5em 0;
99
+ padding-left: 1.5em;
100
+ }
101
+ .prose :global(li) {
102
+ margin: 0.25em 0;
103
+ }
104
+ .prose :global(code) {
105
+ background: rgba(0, 0, 0, 0.1);
106
+ padding: 0.1em 0.3em;
107
+ border-radius: 0.25em;
108
+ font-size: 0.9em;
109
+ }
110
+ .prose :global(pre) {
111
+ background: rgba(0, 0, 0, 0.1);
112
+ padding: 0.75em;
113
+ border-radius: 0.5em;
114
+ overflow-x: auto;
115
+ margin: 0.5em 0;
116
+ }
117
+ .prose :global(pre code) {
118
+ background: none;
119
+ padding: 0;
120
+ }
121
+ .prose :global(h1), .prose :global(h2), .prose :global(h3) {
122
+ margin: 0.75em 0 0.5em;
123
+ font-weight: 600;
124
+ }
125
+ .prose :global(h1) { font-size: 1.25em; }
126
+ .prose :global(h2) { font-size: 1.125em; }
127
+ .prose :global(h3) { font-size: 1em; }
128
+ .prose :global(blockquote) {
129
+ border-left: 3px solid currentColor;
130
+ opacity: 0.8;
131
+ padding-left: 1em;
132
+ margin: 0.5em 0;
133
+ }
134
+ .prose :global(a) {
135
+ text-decoration: underline;
136
+ }
137
+ .prose :global(strong) {
138
+ font-weight: 600;
139
+ }
140
+ </style>
@@ -14,12 +14,23 @@
14
14
  const { messages, isTyping = false, typingSender = null, class: className = '' }: Props = $props();
15
15
 
16
16
  let listElement: HTMLDivElement | undefined = $state();
17
+ let lastMessageCount = $state(0);
18
+ let lastMessageId = $state<string | null>(null);
17
19
 
18
- // Auto-scroll to bottom when new messages arrive
20
+ // Track new messages and scroll appropriately
19
21
  $effect(() => {
20
- if (messages.length > 0) {
21
- scrollToBottom();
22
+ const count = messages.length;
23
+ if (count > lastMessageCount && count > 0) {
24
+ const newMessage = messages[count - 1];
25
+ // Only scroll to top of message for AI/human responses, scroll to bottom for user messages
26
+ if (newMessage.sender === 'user') {
27
+ scrollToBottom();
28
+ } else {
29
+ scrollToNewMessage(newMessage.id);
30
+ }
31
+ lastMessageId = newMessage.id;
22
32
  }
33
+ lastMessageCount = count;
23
34
  });
24
35
 
25
36
  async function scrollToBottom() {
@@ -29,6 +40,19 @@
29
40
  }
30
41
  }
31
42
 
43
+ async function scrollToNewMessage(messageId: string) {
44
+ await tick();
45
+ if (listElement) {
46
+ const messageEl = listElement.querySelector(`[data-message-id="${messageId}"]`);
47
+ if (messageEl) {
48
+ messageEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
49
+ } else {
50
+ // Fallback to bottom
51
+ listElement.scrollTop = listElement.scrollHeight;
52
+ }
53
+ }
54
+ }
55
+
32
56
  const typingLabel = $derived(() => {
33
57
  if (typingSender === 'human') return 'Support is typing...';
34
58
  return 'AI is typing...';
@@ -46,7 +70,9 @@
46
70
  </div>
47
71
  {:else}
48
72
  {#each messages as message (message.id)}
49
- <MessageBubble {message} />
73
+ <div data-message-id={message.id}>
74
+ <MessageBubble {message} />
75
+ </div>
50
76
  {/each}
51
77
  {/if}
52
78
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alliance-droid/chat-widget",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Svelte chat widget with AI support and human escalation",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -52,6 +52,7 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@alliance-droid/svelte-component-library": "^0.1.9",
55
+ "marked": "^17.0.3",
55
56
  "ws": "^8.18.0"
56
57
  },
57
58
  "keywords": [