File size: 5,379 Bytes
5b76fc9 fc38956 5b76fc9 fc38956 5b76fc9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | import { supabase } from './supabaseClient.js';
import { validateGuardrails } from './guardrails.js';
import { detectIntent } from './intent.js';
import { DataResponseCard } from './DataCard.js';
const { useState } = React;
let chatModel = null;
// Helper to strip echoed model prefixes
const cleanModelOutput = (rawText) => {
return rawText
.replace(/^(Data|Inventory|Data assistant|Assistant|Bot):\s*/i, '')
.trim();
};
export function App() {
const [messages, setMessages] = useState([
{ sender: "bot", type: "text", text: "HealthyLifePhil Assistant Active. Ask about inventory, sales metrics, or chat." }
]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const handleChat = async () => {
if (!input.trim() || loading) return;
const userText = input.trim();
setMessages(prev => [...prev, { sender: "user", type: "text", text: userText }]);
setInput("");
setLoading(true);
try {
// 1. Security Check
const guardrail = validateGuardrails(userText);
if (guardrail.blocked) {
setMessages(prev => [...prev, { sender: "bot", type: "text", text: guardrail.reason }]);
setLoading(false);
return;
}
// 2. Intent Routing
const intent = detectIntent(userText);
if (intent === 'INVENTORY') {
const { data: products, error } = await supabase.from('products').select('*');
if (error) throw error;
const lowStock = products ? products.filter(p => p.stock_quantity < 20) : [];
const totalUnits = products ? products.reduce((acc, p) => acc + Number(p.stock_quantity || 0), 0) : 0;
setMessages(prev => [...prev, {
sender: "bot",
type: "data",
data: { type: 'inventory', productCount: products ? products.length : 0, totalUnits, products: products || [], lowStock }
}]);
setLoading(false);
return;
}
if (intent === 'SALES') {
const { data: sales, error } = await supabase.from('orders').select('*');
if (error) throw error;
const totalRev = sales ? sales.reduce((acc, i) => acc + Number(i.total_amount || 0), 0) : 0;
const totalOrders = sales ? sales.length : 0;
const avgOrder = totalOrders > 0 ? (totalRev / totalOrders) : 0;
setMessages(prev => [...prev, {
sender: "bot",
type: "data",
data: { type: 'sales', totalRev, totalOrders, avgOrder }
}]);
setLoading(false);
return;
}
// 3. Clean Conversational Fallback
if (!chatModel) {
chatModel = await window.pipeline('text2text-generation', 'Xenova/LaMini-Flan-T5-77M');
}
const res = await chatModel(`Question: ${userText}\nAnswer:`, {
max_new_tokens: 45,
repetition_penalty: 1.2
});
const cleanedReply = cleanModelOutput(res[0].generated_text);
setMessages(prev => [...prev, { sender: "bot", type: "text", text: cleanedReply || "I am ready to help with your store queries." }]);
} catch (err) {
console.error(err);
setMessages(prev => [...prev, { sender: "bot", type: "text", text: "Unable to retrieve database info. Please check your Supabase connection." }]);
} finally {
setLoading(false);
}
};
return (
<div className="flex h-screen bg-slate-950 text-white">
<div className="w-64 bg-slate-900 p-4 border-r border-slate-800 flex flex-col justify-between">
<div>
<h1 className="text-md font-bold text-white mb-6">HealthyLifePhil</h1>
<p className="text-xs text-slate-400">Modular Multi-File Architecture Active.</p>
</div>
<div className="text-xs text-emerald-400">● Engine Ready</div>
</div>
<div className="flex-1 flex flex-col">
<header className="p-4 bg-slate-900 border-b border-slate-800 font-semibold text-sm">
HealthyLifePhil Store Assistant
</header>
<div className="flex-1 p-6 overflow-y-auto space-y-4">
{messages.map((m, index) => (
<div key={index} className={`flex ${m.sender === 'user' ? 'justify-end' : 'justify-start'}`}>
<div className={`p-4 rounded-xl max-w-lg text-sm ${m.sender === 'user' ? 'bg-emerald-600 text-white' : 'bg-slate-800 text-slate-200 border border-slate-700'}`}>
{m.type === 'data' ? <DataResponseCard data={m.data} /> : m.text}
</div>
</div>
))}
{loading && <div className="text-xs text-emerald-400 italic animate-pulse">Processing query...</div>}
</div>
<div className="p-4 bg-slate-900 border-t border-slate-800 flex gap-2">
<input
type="text"
className="flex-1 p-3 rounded-lg bg-slate-800 border border-slate-700 text-white text-sm focus:outline-none focus:border-emerald-500"
placeholder="Ask about inventory, sales, or chat..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleChat()}
/>
<button onClick={handleChat} disabled={loading} className="bg-emerald-600 hover:bg-emerald-500 px-5 py-2 rounded-lg font-bold text-sm text-black">
Send
</button>
</div>
</div>
</div>
);
} |