@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}"
|
|
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
|
-
|
|
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
|
-
//
|
|
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.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": [
|