@alliance-droid/chat-widget 0.1.6 → 0.1.8
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
|
-
|
|
70
|
+
{#if renderedContent()}
|
|
71
|
+
<div class="markdown-content" 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,73 @@
|
|
|
58
83
|
{formatTime(message.timestamp)}
|
|
59
84
|
</span>
|
|
60
85
|
</div>
|
|
86
|
+
|
|
87
|
+
<style>
|
|
88
|
+
.markdown-content {
|
|
89
|
+
color: inherit;
|
|
90
|
+
}
|
|
91
|
+
.markdown-content :global(p) {
|
|
92
|
+
margin: 0.5em 0;
|
|
93
|
+
color: inherit;
|
|
94
|
+
}
|
|
95
|
+
.markdown-content :global(p:first-child) {
|
|
96
|
+
margin-top: 0;
|
|
97
|
+
}
|
|
98
|
+
.markdown-content :global(p:last-child) {
|
|
99
|
+
margin-bottom: 0;
|
|
100
|
+
}
|
|
101
|
+
.markdown-content :global(ul), .markdown-content :global(ol) {
|
|
102
|
+
margin: 0.5em 0;
|
|
103
|
+
padding-left: 1.5em;
|
|
104
|
+
color: inherit;
|
|
105
|
+
}
|
|
106
|
+
.markdown-content :global(li) {
|
|
107
|
+
margin: 0.25em 0;
|
|
108
|
+
color: inherit;
|
|
109
|
+
}
|
|
110
|
+
.markdown-content :global(code) {
|
|
111
|
+
background: rgba(128, 128, 128, 0.2);
|
|
112
|
+
padding: 0.1em 0.3em;
|
|
113
|
+
border-radius: 0.25em;
|
|
114
|
+
font-size: 0.9em;
|
|
115
|
+
color: inherit;
|
|
116
|
+
}
|
|
117
|
+
.markdown-content :global(pre) {
|
|
118
|
+
background: rgba(128, 128, 128, 0.2);
|
|
119
|
+
padding: 0.75em;
|
|
120
|
+
border-radius: 0.5em;
|
|
121
|
+
overflow-x: auto;
|
|
122
|
+
margin: 0.5em 0;
|
|
123
|
+
color: inherit;
|
|
124
|
+
}
|
|
125
|
+
.markdown-content :global(pre code) {
|
|
126
|
+
background: none;
|
|
127
|
+
padding: 0;
|
|
128
|
+
}
|
|
129
|
+
.markdown-content :global(h1), .markdown-content :global(h2), .markdown-content :global(h3) {
|
|
130
|
+
margin: 0.75em 0 0.5em;
|
|
131
|
+
font-weight: 600;
|
|
132
|
+
color: inherit;
|
|
133
|
+
}
|
|
134
|
+
.markdown-content :global(h1) { font-size: 1.25em; }
|
|
135
|
+
.markdown-content :global(h2) { font-size: 1.125em; }
|
|
136
|
+
.markdown-content :global(h3) { font-size: 1em; }
|
|
137
|
+
.markdown-content :global(blockquote) {
|
|
138
|
+
border-left: 3px solid currentColor;
|
|
139
|
+
opacity: 0.8;
|
|
140
|
+
padding-left: 1em;
|
|
141
|
+
margin: 0.5em 0;
|
|
142
|
+
color: inherit;
|
|
143
|
+
}
|
|
144
|
+
.markdown-content :global(a) {
|
|
145
|
+
text-decoration: underline;
|
|
146
|
+
color: inherit;
|
|
147
|
+
}
|
|
148
|
+
.markdown-content :global(strong) {
|
|
149
|
+
font-weight: 600;
|
|
150
|
+
color: inherit;
|
|
151
|
+
}
|
|
152
|
+
.markdown-content :global(em) {
|
|
153
|
+
color: inherit;
|
|
154
|
+
}
|
|
155
|
+
</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
|
-
//
|
|
20
|
+
// Track new messages and scroll appropriately
|
|
19
21
|
$effect(() => {
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
<
|
|
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.
|
|
3
|
+
"version": "0.1.8",
|
|
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": [
|