chore: move widget source into sidecar repo (widget-src/)

Widget builds from widget-src/ → public/widget.js
Vite outDir updated to ../public
.gitignore excludes node_modules

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 06:59:54 +05:30
parent 76fa6f51de
commit 517b2661b0
15 changed files with 2910 additions and 0 deletions

94
widget-src/src/chat.tsx Normal file
View File

@@ -0,0 +1,94 @@
import { useState, useRef, useEffect } from 'preact/hooks';
import { streamChat } from './api';
import type { ChatMessage } from './types';
const QUICK_ACTIONS = [
'Doctor availability',
'Clinic timings',
'Book appointment',
'Health packages',
];
export const Chat = () => {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages]);
const sendMessage = async (text: string) => {
if (!text.trim() || loading) return;
const userMsg: ChatMessage = { role: 'user', content: text.trim() };
const updated = [...messages, userMsg];
setMessages(updated);
setInput('');
setLoading(true);
try {
const stream = await streamChat(updated);
const reader = stream.getReader();
const decoder = new TextDecoder();
let assistantText = '';
setMessages([...updated, { role: 'assistant', content: '' }]);
while (true) {
const { done, value } = await reader.read();
if (done) break;
assistantText += decoder.decode(value, { stream: true });
setMessages([...updated, { role: 'assistant', content: assistantText }]);
}
} catch {
setMessages([...updated, { role: 'assistant', content: 'Sorry, I encountered an error. Please try again.' }]);
} finally {
setLoading(false);
}
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div class="chat-messages" ref={scrollRef}>
{messages.length === 0 && (
<div style={{ textAlign: 'center', padding: '20px 0' }}>
<div style={{ fontSize: '24px', marginBottom: '8px' }}>👋</div>
<div style={{ fontSize: '14px', fontWeight: 600, color: '#1f2937', marginBottom: '4px' }}>
How can we help you?
</div>
<div style={{ fontSize: '12px', color: '#6b7280', marginBottom: '16px' }}>
Ask about doctors, clinics, packages, or book an appointment.
</div>
<div class="quick-actions">
{QUICK_ACTIONS.map(q => (
<button key={q} class="quick-action" onClick={() => sendMessage(q)}>{q}</button>
))}
</div>
</div>
)}
{messages.map((msg, i) => (
<div key={i} class={`chat-msg ${msg.role}`}>
<div class="chat-bubble">{msg.content || '...'}</div>
</div>
))}
</div>
<div class="chat-input-row">
<input
class="widget-input chat-input"
placeholder="Type a message..."
value={input}
onInput={(e: any) => setInput(e.target.value)}
onKeyDown={(e: any) => e.key === 'Enter' && sendMessage(input)}
disabled={loading}
/>
<button class="chat-send" onClick={() => sendMessage(input)} disabled={loading || !input.trim()}>
</button>
</div>
</div>
);
};