Add lead collection form before chat + fix RC bot message delivery
- New ChatLeadForm component: collects name, email, company before chat starts - GDPR consent checkbox with Privacy Policy link - Lead info passed to backend and injected as LLM context - Visitor name from form used in Rocket.Chat room - RC bot messages: added logging + fallback to livechat/message endpoint - RC room caching to avoid repeated API calls Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2dc1414caa
commit
a8e8d8a71b
9 changed files with 433 additions and 61 deletions
|
|
@ -78,6 +78,16 @@ async def chat(req: ChatRequest):
|
|||
# Store user message and get conversation history
|
||||
messages = await store_messages(req.session_id, "user", message)
|
||||
|
||||
# If lead info provided, prepend context for LLM
|
||||
if req.lead and req.lead.name:
|
||||
lead_context = f"[System: The visitor has introduced themselves. Name: {req.lead.name}"
|
||||
if req.lead.email:
|
||||
lead_context += f", Email: {req.lead.email}"
|
||||
if req.lead.company:
|
||||
lead_context += f", Company: {req.lead.company}"
|
||||
lead_context += ". Use this info naturally — greet them by name. Don't re-ask for info you already have.]"
|
||||
messages = [{"role": "user", "content": lead_context}, {"role": "assistant", "content": "Understood."}] + messages
|
||||
|
||||
# Get AI response
|
||||
try:
|
||||
reply, lead_data = await get_ai_response(messages)
|
||||
|
|
@ -90,7 +100,8 @@ async def chat(req: ChatRequest):
|
|||
await store_messages(req.session_id, "assistant", reply)
|
||||
|
||||
# Mirror to Rocket.Chat (async, don't block response)
|
||||
room_id = await get_or_create_room(req.session_id)
|
||||
visitor_name = req.lead.name if req.lead and req.lead.name else "Website Visitor"
|
||||
room_id = await get_or_create_room(req.session_id, visitor_name)
|
||||
if room_id:
|
||||
asyncio.create_task(send_message(room_id, req.session_id, message, "visitor"))
|
||||
asyncio.create_task(send_message(room_id, req.session_id, reply, "bot"))
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ChatLead(BaseModel):
|
||||
name: str = ""
|
||||
email: str = ""
|
||||
company: str = ""
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
session_id: str = Field(..., min_length=1, max_length=64)
|
||||
message: str = Field(..., min_length=1, max_length=500)
|
||||
page_context: str = Field(default="/", max_length=200)
|
||||
lead: ChatLead | None = None
|
||||
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import httpx
|
||||
import logging
|
||||
from config import settings
|
||||
|
||||
logger = logging.getLogger("rocketchat")
|
||||
|
||||
HEADERS: dict[str, str] = {}
|
||||
# Cache: session_id -> room_id
|
||||
_room_cache: dict[str, str] = {}
|
||||
|
||||
|
||||
def _get_headers() -> dict[str, str]:
|
||||
|
|
@ -18,6 +23,11 @@ async def get_or_create_room(session_id: str, visitor_name: str = "Website Visit
|
|||
"""Get or create a Rocket.Chat livechat room for a session."""
|
||||
if not settings.rocketchat_auth_token:
|
||||
return None
|
||||
|
||||
# Return cached room
|
||||
if session_id in _room_cache:
|
||||
return _room_cache[session_id]
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as http:
|
||||
# Register visitor
|
||||
|
|
@ -42,8 +52,12 @@ async def get_or_create_room(session_id: str, visitor_name: str = "Website Visit
|
|||
timeout=10,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json().get("room", {}).get("_id")
|
||||
except Exception:
|
||||
room_id = resp.json().get("room", {}).get("_id")
|
||||
if room_id:
|
||||
_room_cache[session_id] = room_id
|
||||
return room_id
|
||||
except Exception as e:
|
||||
logger.error(f"RC get_or_create_room error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -54,7 +68,8 @@ async def send_message(room_id: str, session_id: str, text: str, sender: str = "
|
|||
try:
|
||||
async with httpx.AsyncClient() as http:
|
||||
if sender == "visitor":
|
||||
await http.post(
|
||||
# Visitor sends via livechat visitor API
|
||||
resp = await http.post(
|
||||
f"{settings.rocketchat_url}/api/v1/livechat/message",
|
||||
headers={"Content-Type": "application/json"},
|
||||
json={
|
||||
|
|
@ -64,17 +79,35 @@ async def send_message(room_id: str, session_id: str, text: str, sender: str = "
|
|||
},
|
||||
timeout=10,
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
logger.error(f"RC visitor msg error: {resp.status_code} {resp.text}")
|
||||
else:
|
||||
await http.post(
|
||||
# Bot/agent sends via authenticated API
|
||||
resp = await http.post(
|
||||
f"{settings.rocketchat_url}/api/v1/chat.sendMessage",
|
||||
headers=_get_headers(),
|
||||
json={
|
||||
"message": {
|
||||
"rid": room_id,
|
||||
"msg": f"[{sender.upper()}] {text}",
|
||||
"msg": f"🤖 {text}",
|
||||
}
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
if resp.status_code != 200:
|
||||
logger.error(f"RC bot msg error: {resp.status_code} {resp.text}")
|
||||
# Fallback: try livechat/message as agent
|
||||
resp2 = await http.post(
|
||||
f"{settings.rocketchat_url}/api/v1/livechat/message",
|
||||
headers=_get_headers(),
|
||||
json={
|
||||
"token": session_id,
|
||||
"rid": room_id,
|
||||
"msg": f"🤖 {text}",
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
if resp2.status_code != 200:
|
||||
logger.error(f"RC bot msg fallback error: {resp2.status_code} {resp2.text}")
|
||||
except Exception as e:
|
||||
logger.error(f"RC send_message exception: {e}")
|
||||
|
|
|
|||
149
src/components/ChatLeadForm.css
Normal file
149
src/components/ChatLeadForm.css
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
.chat-lead-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24px 20px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.chat-lead-form__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chat-lead-form__avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
background: var(--orange-100);
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-lead-form__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--light-grey-100);
|
||||
}
|
||||
|
||||
.chat-lead-form__subtitle {
|
||||
font-size: 12px;
|
||||
color: rgba(211, 221, 222, 0.5);
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.chat-lead-form__intro {
|
||||
font-size: 14px;
|
||||
color: rgba(211, 221, 222, 0.75);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chat-lead-form__fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chat-lead-form__group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.chat-lead-form__group input[type="text"],
|
||||
.chat-lead-form__group input[type="email"] {
|
||||
background: rgba(211, 221, 222, 0.08);
|
||||
border: 1px solid rgba(211, 221, 222, 0.15);
|
||||
border-radius: 10px;
|
||||
padding: 11px 14px;
|
||||
color: var(--light-grey-100);
|
||||
font-family: var(--font-primary);
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chat-lead-form__group input::placeholder {
|
||||
color: rgba(211, 221, 222, 0.4);
|
||||
}
|
||||
|
||||
.chat-lead-form__group input:focus {
|
||||
border-color: var(--orange-100);
|
||||
}
|
||||
|
||||
.chat-lead-form__group input.chat-lead-form__input--error {
|
||||
border-color: #e74c3c;
|
||||
}
|
||||
|
||||
.chat-lead-form__error {
|
||||
font-size: 12px;
|
||||
color: #e74c3c;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.chat-lead-form__consent {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.chat-lead-form__consent input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
accent-color: var(--orange-100);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-lead-form__consent span {
|
||||
font-size: 12px;
|
||||
color: rgba(211, 221, 222, 0.6);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chat-lead-form__consent a {
|
||||
color: var(--orange-100);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.chat-lead-form__consent a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.chat-lead-form__submit {
|
||||
margin-top: 16px;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: var(--orange-100);
|
||||
color: #fff;
|
||||
font-family: var(--font-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.1s;
|
||||
}
|
||||
|
||||
.chat-lead-form__submit:hover {
|
||||
background: var(--yellow-100);
|
||||
color: var(--dark-grey-100);
|
||||
}
|
||||
|
||||
.chat-lead-form__submit:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
107
src/components/ChatLeadForm.tsx
Normal file
107
src/components/ChatLeadForm.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import React, { useState } from 'react';
|
||||
import './ChatLeadForm.css';
|
||||
|
||||
export interface LeadInfo {
|
||||
name: string;
|
||||
email: string;
|
||||
company: string;
|
||||
}
|
||||
|
||||
interface ChatLeadFormProps {
|
||||
onSubmit: (lead: LeadInfo) => void;
|
||||
}
|
||||
|
||||
const ChatLeadForm: React.FC<ChatLeadFormProps> = ({ onSubmit }) => {
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [company, setCompany] = useState('');
|
||||
const [consent, setConsent] = useState(false);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const validate = () => {
|
||||
const errs: Record<string, string> = {};
|
||||
if (!name.trim()) errs.name = 'Please enter your name';
|
||||
if (!email.trim()) errs.email = 'Please enter your email';
|
||||
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) errs.email = 'Please enter a valid email';
|
||||
if (!consent) errs.consent = 'Please accept to continue';
|
||||
setErrors(errs);
|
||||
return Object.keys(errs).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!validate()) return;
|
||||
onSubmit({ name: name.trim(), email: email.trim(), company: company.trim() });
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="chat-lead-form" onSubmit={handleSubmit}>
|
||||
<div className="chat-lead-form__header">
|
||||
<div className="chat-lead-form__avatar">AI</div>
|
||||
<div>
|
||||
<div className="chat-lead-form__title">AImpress</div>
|
||||
<div className="chat-lead-form__subtitle">AI & Automation Consultancy</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="chat-lead-form__intro">
|
||||
Hi! Before we start, please introduce yourself so we can better assist you.
|
||||
</p>
|
||||
|
||||
<div className="chat-lead-form__fields">
|
||||
<div className="chat-lead-form__group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Your name *"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className={errors.name ? 'chat-lead-form__input--error' : ''}
|
||||
maxLength={100}
|
||||
/>
|
||||
{errors.name && <span className="chat-lead-form__error">{errors.name}</span>}
|
||||
</div>
|
||||
|
||||
<div className="chat-lead-form__group">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email *"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className={errors.email ? 'chat-lead-form__input--error' : ''}
|
||||
maxLength={200}
|
||||
/>
|
||||
{errors.email && <span className="chat-lead-form__error">{errors.email}</span>}
|
||||
</div>
|
||||
|
||||
<div className="chat-lead-form__group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Company (optional)"
|
||||
value={company}
|
||||
onChange={(e) => setCompany(e.target.value)}
|
||||
maxLength={200}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="chat-lead-form__consent">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={consent}
|
||||
onChange={(e) => setConsent(e.target.checked)}
|
||||
/>
|
||||
<span>
|
||||
I agree to the processing of my personal data in accordance with the{' '}
|
||||
<a href="/privacy" target="_blank" rel="noopener noreferrer">Privacy Policy</a>
|
||||
</span>
|
||||
</label>
|
||||
{errors.consent && <span className="chat-lead-form__error">{errors.consent}</span>}
|
||||
</div>
|
||||
|
||||
<button type="submit" className="chat-lead-form__submit">
|
||||
Start Chat
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatLeadForm;
|
||||
|
|
@ -13,7 +13,7 @@ const GREETING_DELAY = 30000; // 30 seconds
|
|||
const ChatWidget: React.FC = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [showGreeting, setShowGreeting] = useState(false);
|
||||
const { messages, loading, rateLimited, sendMessage, clearChat, sessionId } = useChat();
|
||||
const { messages, loading, rateLimited, lead, sendMessage, setLead, clearChat, sessionId } = useChat();
|
||||
const openedAtRef = React.useRef<number>(0);
|
||||
|
||||
// Auto-greeting after 30s (once per session)
|
||||
|
|
@ -67,9 +67,14 @@ const ChatWidget: React.FC = () => {
|
|||
messages={messages}
|
||||
loading={loading}
|
||||
rateLimited={rateLimited}
|
||||
hasLead={!!lead}
|
||||
onSend={handleSend}
|
||||
onClose={handleClose}
|
||||
onClear={clearChat}
|
||||
onLeadSubmit={(leadInfo) => {
|
||||
setLead(leadInfo);
|
||||
track('Chat Lead Form Submitted', { has_company: !!leadInfo.company });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import React, { useEffect, useRef } from 'react';
|
|||
import { motion } from 'framer-motion';
|
||||
import ChatMessage from './ChatMessage';
|
||||
import ChatInput from './ChatInput';
|
||||
import ChatLeadForm from './ChatLeadForm';
|
||||
import type { LeadInfo } from './ChatLeadForm';
|
||||
import type { ChatMessage as ChatMessageType } from '../types/chat';
|
||||
import './ChatWindow.css';
|
||||
|
||||
|
|
@ -9,18 +11,22 @@ interface ChatWindowProps {
|
|||
messages: ChatMessageType[];
|
||||
loading: boolean;
|
||||
rateLimited: boolean;
|
||||
hasLead: boolean;
|
||||
onSend: (text: string) => void;
|
||||
onClose: () => void;
|
||||
onClear: () => void;
|
||||
onLeadSubmit: (lead: LeadInfo) => void;
|
||||
}
|
||||
|
||||
const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
messages,
|
||||
loading,
|
||||
rateLimited,
|
||||
hasLead,
|
||||
onSend,
|
||||
onClose,
|
||||
onClear,
|
||||
onLeadSubmit,
|
||||
}) => {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
|
@ -36,55 +42,77 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
|||
exit={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
transition={{ duration: 0.25, ease: 'easeOut' }}
|
||||
>
|
||||
<div className="chat-window__header">
|
||||
<div className="chat-window__header-info">
|
||||
<div className="chat-window__avatar">AI</div>
|
||||
<div>
|
||||
<div className="chat-window__title">AImpress</div>
|
||||
<div className="chat-window__status">Online</div>
|
||||
{!hasLead ? (
|
||||
<>
|
||||
<div className="chat-window__header">
|
||||
<div className="chat-window__header-info" />
|
||||
<div className="chat-window__header-actions">
|
||||
<button
|
||||
className="chat-window__action-btn"
|
||||
onClick={onClose}
|
||||
aria-label="Close chat"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="18" height="18">
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ChatLeadForm onSubmit={onLeadSubmit} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="chat-window__header">
|
||||
<div className="chat-window__header-info">
|
||||
<div className="chat-window__avatar">AI</div>
|
||||
<div>
|
||||
<div className="chat-window__title">AImpress</div>
|
||||
<div className="chat-window__status">Online</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="chat-window__header-actions">
|
||||
<button
|
||||
className="chat-window__action-btn"
|
||||
onClick={onClear}
|
||||
aria-label="Clear chat"
|
||||
title="Clear chat"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||
<path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="chat-window__action-btn"
|
||||
onClick={onClose}
|
||||
aria-label="Close chat"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="18" height="18">
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="chat-window__header-actions">
|
||||
<button
|
||||
className="chat-window__action-btn"
|
||||
onClick={onClear}
|
||||
aria-label="Clear chat"
|
||||
title="Clear chat"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||
<path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="chat-window__action-btn"
|
||||
onClick={onClose}
|
||||
aria-label="Close chat"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="18" height="18">
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="chat-window__messages">
|
||||
{messages.length === 0 && (
|
||||
<div className="chat-window__welcome">
|
||||
<p>Hi! I'm the AImpress AI assistant. Ask me about our AI & automation services, pricing, or book a free consultation.</p>
|
||||
<div className="chat-window__messages">
|
||||
{messages.length === 0 && (
|
||||
<div className="chat-window__welcome">
|
||||
<p>Hi! I'm the AImpress AI assistant. Ask me about our AI & automation services, pricing, or book a free consultation.</p>
|
||||
</div>
|
||||
)}
|
||||
{messages.map((msg) => (
|
||||
<ChatMessage key={msg.id} message={msg} />
|
||||
))}
|
||||
{loading && (
|
||||
<div className="chat-window__typing">
|
||||
<span /><span /><span />
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
)}
|
||||
{messages.map((msg) => (
|
||||
<ChatMessage key={msg.id} message={msg} />
|
||||
))}
|
||||
{loading && (
|
||||
<div className="chat-window__typing">
|
||||
<span /><span /><span />
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
<ChatInput onSend={onSend} disabled={loading || rateLimited} />
|
||||
<ChatInput onSend={onSend} disabled={loading || rateLimited} />
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import type { ChatMessage, ChatAPIResponse, PendingMessage } from '../types/chat';
|
||||
import type { ChatMessage, ChatAPIResponse, ChatLead, PendingMessage } from '../types/chat';
|
||||
|
||||
const STORAGE_KEY = 'aimpress_chat';
|
||||
const SESSION_KEY = 'aimpress_chat_session';
|
||||
const LEAD_KEY = 'aimpress_chat_lead';
|
||||
const MAX_MESSAGE_LENGTH = 500;
|
||||
|
||||
function generateSessionId(): string {
|
||||
|
|
@ -36,10 +37,20 @@ function saveMessages(messages: ChatMessage[]) {
|
|||
}
|
||||
}
|
||||
|
||||
function loadLead(): ChatLead | null {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(LEAD_KEY);
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function useChat() {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>(loadMessages);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [rateLimited, setRateLimited] = useState(false);
|
||||
const [lead, setLeadState] = useState<ChatLead | null>(loadLead);
|
||||
const sessionId = useRef(getSessionId());
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const location = useLocation();
|
||||
|
|
@ -92,14 +103,19 @@ export function useChat() {
|
|||
setLoading(true);
|
||||
|
||||
try {
|
||||
const body: Record<string, unknown> = {
|
||||
session_id: sessionId.current,
|
||||
message: trimmed,
|
||||
page_context: location.pathname,
|
||||
};
|
||||
// Include lead info on first message of session
|
||||
if (lead && messages.length === 0) {
|
||||
body.lead = lead;
|
||||
}
|
||||
const res = await fetch('/api/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
session_id: sessionId.current,
|
||||
message: trimmed,
|
||||
page_context: location.pathname,
|
||||
}),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Chat API error');
|
||||
|
|
@ -131,14 +147,21 @@ export function useChat() {
|
|||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[loading, rateLimited, location.pathname],
|
||||
[loading, rateLimited, lead, messages.length, location.pathname],
|
||||
);
|
||||
|
||||
const setLead = useCallback((leadInfo: ChatLead) => {
|
||||
setLeadState(leadInfo);
|
||||
sessionStorage.setItem(LEAD_KEY, JSON.stringify(leadInfo));
|
||||
}, []);
|
||||
|
||||
const clearChat = useCallback(() => {
|
||||
setMessages([]);
|
||||
setRateLimited(false);
|
||||
setLeadState(null);
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
sessionStorage.removeItem(SESSION_KEY);
|
||||
sessionStorage.removeItem(LEAD_KEY);
|
||||
sessionId.current = generateSessionId();
|
||||
sessionStorage.setItem(SESSION_KEY, sessionId.current);
|
||||
}, []);
|
||||
|
|
@ -147,7 +170,9 @@ export function useChat() {
|
|||
messages,
|
||||
loading,
|
||||
rateLimited,
|
||||
lead,
|
||||
sendMessage,
|
||||
setLead,
|
||||
clearChat,
|
||||
sessionId: sessionId.current,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,10 +6,17 @@ export interface ChatMessage {
|
|||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface ChatLead {
|
||||
name: string;
|
||||
email: string;
|
||||
company: string;
|
||||
}
|
||||
|
||||
export interface ChatAPIRequest {
|
||||
session_id: string;
|
||||
message: string;
|
||||
page_context: string;
|
||||
lead?: ChatLead;
|
||||
}
|
||||
|
||||
export interface ChatAPIResponse {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue