Tina / src /App.js
KyleAIers's picture
Update src/App.js
5b76fc9 verified
Raw History Blame Contribute Delete
5.38 kB
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>
);
}