@alliance-droid/chat-widget 0.1.0
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.
- package/README.md +102 -0
- package/dist/client/connection.d.ts +39 -0
- package/dist/client/connection.js +198 -0
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.js +6 -0
- package/dist/components/ChatInput.svelte +73 -0
- package/dist/components/ChatInput.svelte.d.ts +9 -0
- package/dist/components/ChatPanel.svelte +117 -0
- package/dist/components/ChatPanel.svelte.d.ts +20 -0
- package/dist/components/ChatWidget.svelte +160 -0
- package/dist/components/ChatWidget.svelte.d.ts +4 -0
- package/dist/components/MessageBubble.svelte +60 -0
- package/dist/components/MessageBubble.svelte.d.ts +8 -0
- package/dist/components/MessageList.svelte +58 -0
- package/dist/components/MessageList.svelte.d.ts +10 -0
- package/dist/components/SupportChat.svelte +133 -0
- package/dist/components/SupportChat.svelte.d.ts +4 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +17 -0
- package/dist/server/ai/anthropic.d.ts +16 -0
- package/dist/server/ai/anthropic.js +109 -0
- package/dist/server/ai/openai.d.ts +16 -0
- package/dist/server/ai/openai.js +112 -0
- package/dist/server/ai/provider.d.ts +21 -0
- package/dist/server/ai/provider.js +6 -0
- package/dist/server/ai/xai.d.ts +16 -0
- package/dist/server/ai/xai.js +113 -0
- package/dist/server/escalation.d.ts +18 -0
- package/dist/server/escalation.js +61 -0
- package/dist/server/handler.d.ts +8 -0
- package/dist/server/handler.js +235 -0
- package/dist/server/index.d.ts +11 -0
- package/dist/server/index.js +10 -0
- package/dist/server/session.d.ts +52 -0
- package/dist/server/session.js +115 -0
- package/dist/stores/chat.d.ts +18 -0
- package/dist/stores/chat.js +40 -0
- package/dist/stores/connection.d.ts +20 -0
- package/dist/stores/connection.js +52 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.js +4 -0
- package/package.json +73 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# @alliance-droid/chat-widget
|
|
2
|
+
|
|
3
|
+
A Svelte chat widget with AI support and human escalation.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @alliance-droid/chat-widget
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Client Widget
|
|
14
|
+
|
|
15
|
+
```svelte
|
|
16
|
+
<script>
|
|
17
|
+
import { ChatWidget } from '@alliance-droid/chat-widget';
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<ChatWidget
|
|
21
|
+
serverUrl="wss://your-server.com/chat"
|
|
22
|
+
theme="dark"
|
|
23
|
+
position="bottom-right"
|
|
24
|
+
greeting="Hi! How can I help you today?"
|
|
25
|
+
enableEscalation={true}
|
|
26
|
+
/>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Server Setup
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { createChatHandler } from '@alliance-droid/chat-widget/server';
|
|
33
|
+
import { WebSocketServer } from 'ws';
|
|
34
|
+
|
|
35
|
+
const wss = new WebSocketServer({ port: 8080 });
|
|
36
|
+
|
|
37
|
+
const chatHandler = createChatHandler({
|
|
38
|
+
ai: {
|
|
39
|
+
provider: 'xai',
|
|
40
|
+
apiKey: process.env.XAI_API_KEY,
|
|
41
|
+
model: 'grok-2-latest',
|
|
42
|
+
systemPrompt: 'You are a helpful assistant...'
|
|
43
|
+
},
|
|
44
|
+
escalation: {
|
|
45
|
+
telegram: {
|
|
46
|
+
botToken: process.env.TELEGRAM_BOT_TOKEN,
|
|
47
|
+
chatId: process.env.TELEGRAM_CHAT_ID
|
|
48
|
+
},
|
|
49
|
+
joinUrl: (sessionId) => `https://yourapp.com/support/chat/${sessionId}`
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
wss.on('connection', chatHandler);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Support Agent View
|
|
57
|
+
|
|
58
|
+
```svelte
|
|
59
|
+
<script>
|
|
60
|
+
import { SupportChat } from '@alliance-droid/chat-widget';
|
|
61
|
+
import { page } from '$app/stores';
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<SupportChat
|
|
65
|
+
serverUrl="wss://your-server.com/chat"
|
|
66
|
+
sessionId={$page.params.sessionId}
|
|
67
|
+
/>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Props
|
|
71
|
+
|
|
72
|
+
### ChatWidget
|
|
73
|
+
|
|
74
|
+
| Prop | Type | Default | Description |
|
|
75
|
+
|------|------|---------|-------------|
|
|
76
|
+
| serverUrl | string | required | WebSocket server URL |
|
|
77
|
+
| theme | 'light' \| 'dark' \| 'auto' | 'dark' | Color theme |
|
|
78
|
+
| position | 'bottom-right' \| 'bottom-left' | 'bottom-right' | Widget position |
|
|
79
|
+
| greeting | string | undefined | Initial greeting message |
|
|
80
|
+
| placeholder | string | 'Type a message...' | Input placeholder |
|
|
81
|
+
| enableEscalation | boolean | true | Show "Talk to human" button |
|
|
82
|
+
| escalationLabel | string | 'Talk to a human' | Escalation button text |
|
|
83
|
+
|
|
84
|
+
## Development
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Install dependencies
|
|
88
|
+
npm install
|
|
89
|
+
|
|
90
|
+
# Start dev server
|
|
91
|
+
npm run dev
|
|
92
|
+
|
|
93
|
+
# Build package
|
|
94
|
+
npm run build:package
|
|
95
|
+
|
|
96
|
+
# Run tests
|
|
97
|
+
npm test
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Connection Client
|
|
3
|
+
*
|
|
4
|
+
* Handles WebSocket connection with auto-reconnect
|
|
5
|
+
*/
|
|
6
|
+
import type { ClientMessage } from '../types.js';
|
|
7
|
+
export interface ConnectionOptions {
|
|
8
|
+
serverUrl: string;
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
reconnect?: boolean;
|
|
11
|
+
reconnectInterval?: number;
|
|
12
|
+
maxReconnectAttempts?: number;
|
|
13
|
+
userId?: string;
|
|
14
|
+
userMeta?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Connect to the chat server
|
|
18
|
+
*/
|
|
19
|
+
export declare function connect(opts: ConnectionOptions): void;
|
|
20
|
+
/**
|
|
21
|
+
* Disconnect from the server
|
|
22
|
+
*/
|
|
23
|
+
export declare function disconnect(): void;
|
|
24
|
+
/**
|
|
25
|
+
* Send a message to the server
|
|
26
|
+
*/
|
|
27
|
+
export declare function send(message: ClientMessage): void;
|
|
28
|
+
/**
|
|
29
|
+
* Send a chat message
|
|
30
|
+
*/
|
|
31
|
+
export declare function sendMessage(content: string): void;
|
|
32
|
+
/**
|
|
33
|
+
* Request escalation to human support
|
|
34
|
+
*/
|
|
35
|
+
export declare function requestEscalation(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Send typing indicator
|
|
38
|
+
*/
|
|
39
|
+
export declare function sendTyping(): void;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Connection Client
|
|
3
|
+
*
|
|
4
|
+
* Handles WebSocket connection with auto-reconnect
|
|
5
|
+
*/
|
|
6
|
+
import { connectionStore } from '../stores/connection.js';
|
|
7
|
+
import { chatStore } from '../stores/chat.js';
|
|
8
|
+
let ws = null;
|
|
9
|
+
let reconnectTimeout = null;
|
|
10
|
+
let options = null;
|
|
11
|
+
/**
|
|
12
|
+
* Connect to the chat server
|
|
13
|
+
*/
|
|
14
|
+
export function connect(opts) {
|
|
15
|
+
options = opts;
|
|
16
|
+
connectionStore.setConnecting();
|
|
17
|
+
try {
|
|
18
|
+
ws = new WebSocket(opts.serverUrl);
|
|
19
|
+
ws.onopen = () => {
|
|
20
|
+
console.log('[ChatWidget] WebSocket connected, authenticating...');
|
|
21
|
+
// Send auth message with API key
|
|
22
|
+
if (ws && opts.apiKey) {
|
|
23
|
+
ws.send(JSON.stringify({
|
|
24
|
+
type: 'auth',
|
|
25
|
+
apiKey: opts.apiKey,
|
|
26
|
+
userId: opts.userId,
|
|
27
|
+
userMeta: opts.userMeta
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
ws.onmessage = (event) => {
|
|
32
|
+
try {
|
|
33
|
+
const message = JSON.parse(event.data);
|
|
34
|
+
handleServerMessage(message);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
console.error('[ChatWidget] Failed to parse message:', err);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
ws.onclose = (event) => {
|
|
41
|
+
console.log('[ChatWidget] WebSocket closed:', event.code, event.reason);
|
|
42
|
+
connectionStore.setDisconnected();
|
|
43
|
+
// Attempt reconnect if enabled
|
|
44
|
+
if (opts.reconnect !== false && opts.maxReconnectAttempts !== 0) {
|
|
45
|
+
scheduleReconnect();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
ws.onerror = (error) => {
|
|
49
|
+
console.error('[ChatWidget] WebSocket error:', error);
|
|
50
|
+
connectionStore.setError('Connection error');
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
console.error('[ChatWidget] Failed to connect:', err);
|
|
55
|
+
connectionStore.setDisconnected('Failed to connect');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Disconnect from the server
|
|
60
|
+
*/
|
|
61
|
+
export function disconnect() {
|
|
62
|
+
if (reconnectTimeout) {
|
|
63
|
+
clearTimeout(reconnectTimeout);
|
|
64
|
+
reconnectTimeout = null;
|
|
65
|
+
}
|
|
66
|
+
if (ws) {
|
|
67
|
+
ws.close();
|
|
68
|
+
ws = null;
|
|
69
|
+
}
|
|
70
|
+
connectionStore.reset();
|
|
71
|
+
chatStore.clear();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Send a message to the server
|
|
75
|
+
*/
|
|
76
|
+
export function send(message) {
|
|
77
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
78
|
+
console.warn('[ChatWidget] Cannot send message: not connected');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
ws.send(JSON.stringify(message));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Send a chat message
|
|
85
|
+
*/
|
|
86
|
+
export function sendMessage(content) {
|
|
87
|
+
const id = generateId();
|
|
88
|
+
const timestamp = Date.now();
|
|
89
|
+
// Add message to local store immediately
|
|
90
|
+
chatStore.addMessage({
|
|
91
|
+
id,
|
|
92
|
+
content,
|
|
93
|
+
sender: 'user',
|
|
94
|
+
timestamp
|
|
95
|
+
});
|
|
96
|
+
// Send to server
|
|
97
|
+
send({
|
|
98
|
+
type: 'message',
|
|
99
|
+
content
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Request escalation to human support
|
|
104
|
+
*/
|
|
105
|
+
export function requestEscalation() {
|
|
106
|
+
chatStore.setStatus('escalating');
|
|
107
|
+
send({ type: 'escalate' });
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Send typing indicator
|
|
111
|
+
*/
|
|
112
|
+
export function sendTyping() {
|
|
113
|
+
send({ type: 'typing' });
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Handle incoming server message
|
|
117
|
+
*/
|
|
118
|
+
function handleServerMessage(message) {
|
|
119
|
+
switch (message.type) {
|
|
120
|
+
case 'connected':
|
|
121
|
+
case 'auth_success':
|
|
122
|
+
if (message.sessionId) {
|
|
123
|
+
connectionStore.setConnected(message.sessionId);
|
|
124
|
+
chatStore.setStatus('ai');
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
case 'message':
|
|
128
|
+
if (message.id && message.content && message.sender) {
|
|
129
|
+
const msg = {
|
|
130
|
+
id: message.id,
|
|
131
|
+
content: message.content,
|
|
132
|
+
sender: message.sender,
|
|
133
|
+
timestamp: message.timestamp || Date.now(),
|
|
134
|
+
streaming: message.streaming
|
|
135
|
+
};
|
|
136
|
+
if (message.streaming) {
|
|
137
|
+
// Update existing message or add new one
|
|
138
|
+
chatStore.updateMessage(message.id, { content: message.content });
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
chatStore.addMessage(msg);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
case 'typing':
|
|
146
|
+
chatStore.setTyping(true, message.sender);
|
|
147
|
+
// Auto-clear after 3 seconds
|
|
148
|
+
setTimeout(() => chatStore.setTyping(false, null), 3000);
|
|
149
|
+
break;
|
|
150
|
+
case 'escalated':
|
|
151
|
+
chatStore.setStatus('human');
|
|
152
|
+
chatStore.addMessage({
|
|
153
|
+
id: generateId(),
|
|
154
|
+
content: 'You are now connected with support.',
|
|
155
|
+
sender: 'system',
|
|
156
|
+
timestamp: Date.now()
|
|
157
|
+
});
|
|
158
|
+
break;
|
|
159
|
+
case 'status':
|
|
160
|
+
if (message.content) {
|
|
161
|
+
chatStore.addMessage({
|
|
162
|
+
id: generateId(),
|
|
163
|
+
content: message.content,
|
|
164
|
+
sender: 'system',
|
|
165
|
+
timestamp: Date.now()
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
case 'error':
|
|
170
|
+
connectionStore.setError(message.error || 'Unknown error');
|
|
171
|
+
break;
|
|
172
|
+
case 'auth_error':
|
|
173
|
+
connectionStore.setError(message.error || 'Authentication failed');
|
|
174
|
+
// Don't reconnect on auth errors
|
|
175
|
+
if (ws) {
|
|
176
|
+
ws.close();
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Schedule a reconnection attempt
|
|
183
|
+
*/
|
|
184
|
+
function scheduleReconnect() {
|
|
185
|
+
if (!options)
|
|
186
|
+
return;
|
|
187
|
+
const interval = options.reconnectInterval || 3000;
|
|
188
|
+
reconnectTimeout = setTimeout(() => {
|
|
189
|
+
connectionStore.setReconnecting();
|
|
190
|
+
connect(options);
|
|
191
|
+
}, interval);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Generate a unique ID
|
|
195
|
+
*/
|
|
196
|
+
function generateId() {
|
|
197
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
198
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { IconButton } from '@alliance-droid/svelte-component-library/components/atoms';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
onsubmit?: (message: string) => void;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
placeholder = 'Type a message...',
|
|
13
|
+
disabled = false,
|
|
14
|
+
onsubmit,
|
|
15
|
+
class: className = ''
|
|
16
|
+
}: Props = $props();
|
|
17
|
+
|
|
18
|
+
let inputValue = $state('');
|
|
19
|
+
let inputElement: HTMLTextAreaElement | undefined = $state();
|
|
20
|
+
|
|
21
|
+
function handleSubmit() {
|
|
22
|
+
const message = inputValue.trim();
|
|
23
|
+
if (!message || disabled) return;
|
|
24
|
+
|
|
25
|
+
onsubmit?.(message);
|
|
26
|
+
inputValue = '';
|
|
27
|
+
|
|
28
|
+
// Reset textarea height
|
|
29
|
+
if (inputElement) {
|
|
30
|
+
inputElement.style.height = 'auto';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
35
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
36
|
+
event.preventDefault();
|
|
37
|
+
handleSubmit();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function handleInput() {
|
|
42
|
+
// Auto-resize textarea
|
|
43
|
+
if (inputElement) {
|
|
44
|
+
inputElement.style.height = 'auto';
|
|
45
|
+
inputElement.style.height = Math.min(inputElement.scrollHeight, 120) + 'px';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<div class="flex items-end gap-2 p-4 border-t border-border {className}">
|
|
51
|
+
<textarea
|
|
52
|
+
bind:this={inputElement}
|
|
53
|
+
bind:value={inputValue}
|
|
54
|
+
{placeholder}
|
|
55
|
+
{disabled}
|
|
56
|
+
rows="1"
|
|
57
|
+
class="flex-1 resize-none bg-surface border border-border rounded-xl px-4 py-2 text-foreground placeholder-foreground-tertiary focus:outline-none focus:border-primary transition-colors disabled:opacity-50"
|
|
58
|
+
onkeydown={handleKeydown}
|
|
59
|
+
oninput={handleInput}
|
|
60
|
+
></textarea>
|
|
61
|
+
|
|
62
|
+
<IconButton
|
|
63
|
+
variant="primary"
|
|
64
|
+
size="md"
|
|
65
|
+
disabled={disabled || !inputValue.trim()}
|
|
66
|
+
onclick={handleSubmit}
|
|
67
|
+
label="Send message"
|
|
68
|
+
>
|
|
69
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
70
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
|
71
|
+
</svg>
|
|
72
|
+
</IconButton>
|
|
73
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
placeholder?: string;
|
|
3
|
+
disabled?: boolean;
|
|
4
|
+
onsubmit?: (message: string) => void;
|
|
5
|
+
class?: string;
|
|
6
|
+
}
|
|
7
|
+
declare const ChatInput: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type ChatInput = ReturnType<typeof ChatInput>;
|
|
9
|
+
export default ChatInput;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { IconButton, Button, Badge } from '@alliance-droid/svelte-component-library/components/atoms';
|
|
4
|
+
import MessageList from './MessageList.svelte';
|
|
5
|
+
import ChatInput from './ChatInput.svelte';
|
|
6
|
+
import type { Message, ChatStatus, ConnectionState } from '../types.js';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
messages: Message[];
|
|
10
|
+
status: ChatStatus;
|
|
11
|
+
connectionState: ConnectionState;
|
|
12
|
+
isTyping?: boolean;
|
|
13
|
+
typingSender?: 'ai' | 'human' | null;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
enableEscalation?: boolean;
|
|
16
|
+
escalationLabel?: string;
|
|
17
|
+
onmessage?: (message: string) => void;
|
|
18
|
+
onescalate?: () => void;
|
|
19
|
+
onclose?: () => void;
|
|
20
|
+
header?: Snippet;
|
|
21
|
+
class?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
messages,
|
|
26
|
+
status,
|
|
27
|
+
connectionState,
|
|
28
|
+
isTyping = false,
|
|
29
|
+
typingSender = null,
|
|
30
|
+
placeholder = 'Type a message...',
|
|
31
|
+
enableEscalation = true,
|
|
32
|
+
escalationLabel = 'Talk to a human',
|
|
33
|
+
onmessage,
|
|
34
|
+
onescalate,
|
|
35
|
+
onclose,
|
|
36
|
+
header,
|
|
37
|
+
class: className = ''
|
|
38
|
+
}: Props = $props();
|
|
39
|
+
|
|
40
|
+
const isConnected = $derived(connectionState === 'connected');
|
|
41
|
+
const isEscalated = $derived(status === 'human');
|
|
42
|
+
const isEscalating = $derived(status === 'escalating');
|
|
43
|
+
|
|
44
|
+
const statusBadge = $derived(() => {
|
|
45
|
+
switch (status) {
|
|
46
|
+
case 'ai':
|
|
47
|
+
return { label: 'AI', variant: 'info' as const };
|
|
48
|
+
case 'human':
|
|
49
|
+
return { label: 'Support', variant: 'success' as const };
|
|
50
|
+
case 'escalating':
|
|
51
|
+
return { label: 'Connecting...', variant: 'warning' as const };
|
|
52
|
+
default:
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<div class="flex flex-col h-full bg-surface rounded-2xl shadow-xl border border-border overflow-hidden {className}">
|
|
59
|
+
<!-- Header -->
|
|
60
|
+
<div class="flex items-center justify-between px-4 py-3 border-b border-border bg-surface">
|
|
61
|
+
{#if header}
|
|
62
|
+
{@render header()}
|
|
63
|
+
{:else}
|
|
64
|
+
<div class="flex items-center gap-2">
|
|
65
|
+
<span class="font-semibold text-foreground">Chat</span>
|
|
66
|
+
{#if statusBadge()}
|
|
67
|
+
<Badge variant={statusBadge()!.variant} size="sm">
|
|
68
|
+
{statusBadge()!.label}
|
|
69
|
+
</Badge>
|
|
70
|
+
{/if}
|
|
71
|
+
</div>
|
|
72
|
+
{/if}
|
|
73
|
+
|
|
74
|
+
<div class="flex items-center gap-1">
|
|
75
|
+
{#if enableEscalation && !isEscalated && !isEscalating}
|
|
76
|
+
<Button
|
|
77
|
+
variant="neutral"
|
|
78
|
+
style="ghost"
|
|
79
|
+
size="sm"
|
|
80
|
+
onclick={onescalate}
|
|
81
|
+
disabled={!isConnected}
|
|
82
|
+
>
|
|
83
|
+
{escalationLabel}
|
|
84
|
+
</Button>
|
|
85
|
+
{/if}
|
|
86
|
+
{#if onclose}
|
|
87
|
+
<IconButton
|
|
88
|
+
variant="neutral"
|
|
89
|
+
size="sm"
|
|
90
|
+
onclick={onclose}
|
|
91
|
+
label="Close chat"
|
|
92
|
+
>
|
|
93
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
94
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
95
|
+
</svg>
|
|
96
|
+
</IconButton>
|
|
97
|
+
{/if}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<!-- Messages -->
|
|
102
|
+
<MessageList {messages} {isTyping} {typingSender} class="flex-1" />
|
|
103
|
+
|
|
104
|
+
<!-- Input -->
|
|
105
|
+
<ChatInput
|
|
106
|
+
{placeholder}
|
|
107
|
+
disabled={!isConnected}
|
|
108
|
+
onsubmit={onmessage}
|
|
109
|
+
/>
|
|
110
|
+
|
|
111
|
+
<!-- Connection status -->
|
|
112
|
+
{#if connectionState === 'connecting' || connectionState === 'reconnecting'}
|
|
113
|
+
<div class="px-4 py-2 bg-warning/10 text-warning text-sm text-center">
|
|
114
|
+
{connectionState === 'reconnecting' ? 'Reconnecting...' : 'Connecting...'}
|
|
115
|
+
</div>
|
|
116
|
+
{/if}
|
|
117
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { Message, ChatStatus, ConnectionState } from '../types.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
messages: Message[];
|
|
5
|
+
status: ChatStatus;
|
|
6
|
+
connectionState: ConnectionState;
|
|
7
|
+
isTyping?: boolean;
|
|
8
|
+
typingSender?: 'ai' | 'human' | null;
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
enableEscalation?: boolean;
|
|
11
|
+
escalationLabel?: string;
|
|
12
|
+
onmessage?: (message: string) => void;
|
|
13
|
+
onescalate?: () => void;
|
|
14
|
+
onclose?: () => void;
|
|
15
|
+
header?: Snippet;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
declare const ChatPanel: import("svelte").Component<Props, {}, "">;
|
|
19
|
+
type ChatPanel = ReturnType<typeof ChatPanel>;
|
|
20
|
+
export default ChatPanel;
|