@alliance-droid/chat-widget 0.1.5 → 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,
@@ -55,7 +55,7 @@
55
55
  });
56
56
  </script>
57
57
 
58
- <div class="flex flex-col h-full bg-surface rounded-2xl shadow-xl border border-border {className}" style="width: 100%; min-width: 0; overflow: hidden;">
58
+ <div class="chat-panel flex flex-col h-full bg-surface rounded-2xl shadow-xl border border-border {className}">
59
59
  <!-- Header -->
60
60
  <div class="flex items-center justify-between px-4 py-3 border-b border-border bg-surface">
61
61
  {#if header}
@@ -115,3 +115,11 @@
115
115
  </div>
116
116
  {/if}
117
117
  </div>
118
+
119
+ <style>
120
+ .chat-panel {
121
+ width: 100%;
122
+ min-width: 0;
123
+ overflow: hidden;
124
+ }
125
+ </style>
@@ -105,10 +105,7 @@
105
105
 
106
106
  <div class="fixed bottom-4 {positionClasses()} z-50 flex flex-col {alignmentClass()} {themeClass()} {className}">
107
107
  {#if isOpen}
108
- <div
109
- class="mb-4 overflow-hidden animate-in fade-in slide-in-from-bottom-4 duration-200"
110
- style="width: 380px; min-width: 380px; max-width: 380px; height: 600px; max-height: 80vh;"
111
- >
108
+ <div class="chat-container mb-4 animate-in">
112
109
  <ChatPanel
113
110
  messages={$messages}
114
111
  status={$status}
@@ -157,6 +154,16 @@
157
154
  </div>
158
155
 
159
156
  <style>
157
+ .chat-container {
158
+ width: 380px !important;
159
+ min-width: 380px !important;
160
+ max-width: 380px !important;
161
+ height: 600px;
162
+ max-height: 80vh;
163
+ overflow: hidden !important;
164
+ contain: strict;
165
+ }
166
+
160
167
  @keyframes fade-in {
161
168
  from { opacity: 0; }
162
169
  to { opacity: 1; }
@@ -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.5",
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": [