@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.
Files changed (42) hide show
  1. package/README.md +102 -0
  2. package/dist/client/connection.d.ts +39 -0
  3. package/dist/client/connection.js +198 -0
  4. package/dist/client/index.d.ts +6 -0
  5. package/dist/client/index.js +6 -0
  6. package/dist/components/ChatInput.svelte +73 -0
  7. package/dist/components/ChatInput.svelte.d.ts +9 -0
  8. package/dist/components/ChatPanel.svelte +117 -0
  9. package/dist/components/ChatPanel.svelte.d.ts +20 -0
  10. package/dist/components/ChatWidget.svelte +160 -0
  11. package/dist/components/ChatWidget.svelte.d.ts +4 -0
  12. package/dist/components/MessageBubble.svelte +60 -0
  13. package/dist/components/MessageBubble.svelte.d.ts +8 -0
  14. package/dist/components/MessageList.svelte +58 -0
  15. package/dist/components/MessageList.svelte.d.ts +10 -0
  16. package/dist/components/SupportChat.svelte +133 -0
  17. package/dist/components/SupportChat.svelte.d.ts +4 -0
  18. package/dist/index.d.ts +15 -0
  19. package/dist/index.js +17 -0
  20. package/dist/server/ai/anthropic.d.ts +16 -0
  21. package/dist/server/ai/anthropic.js +109 -0
  22. package/dist/server/ai/openai.d.ts +16 -0
  23. package/dist/server/ai/openai.js +112 -0
  24. package/dist/server/ai/provider.d.ts +21 -0
  25. package/dist/server/ai/provider.js +6 -0
  26. package/dist/server/ai/xai.d.ts +16 -0
  27. package/dist/server/ai/xai.js +113 -0
  28. package/dist/server/escalation.d.ts +18 -0
  29. package/dist/server/escalation.js +61 -0
  30. package/dist/server/handler.d.ts +8 -0
  31. package/dist/server/handler.js +235 -0
  32. package/dist/server/index.d.ts +11 -0
  33. package/dist/server/index.js +10 -0
  34. package/dist/server/session.d.ts +52 -0
  35. package/dist/server/session.js +115 -0
  36. package/dist/stores/chat.d.ts +18 -0
  37. package/dist/stores/chat.js +40 -0
  38. package/dist/stores/connection.d.ts +20 -0
  39. package/dist/stores/connection.js +52 -0
  40. package/dist/types.d.ts +79 -0
  41. package/dist/types.js +4 -0
  42. 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,6 @@
1
+ /**
2
+ * Client-side exports
3
+ */
4
+ export { connect, disconnect, send, sendMessage, requestEscalation, sendTyping } from './connection.js';
5
+ export { chatStore } from '../stores/chat.js';
6
+ export { connectionStore } from '../stores/connection.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Client-side exports
3
+ */
4
+ export { connect, disconnect, send, sendMessage, requestEscalation, sendTyping } from './connection.js';
5
+ export { chatStore } from '../stores/chat.js';
6
+ export { connectionStore } from '../stores/connection.js';
@@ -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;