Download src/App.js from KyleAIers/Tina: direct link, hf CLI and curl.
- Browser
- Download file 5.38 kB
-
https://huggingface.co/spaces/KyleAIers/Tina/resolve/main/src/App.js
- Command line
-
hf download hf://spaces/KyleAIers/Tina/src/App.js
-
curl -L -o App.js https://huggingface.co/spaces/KyleAIers/Tina/resolve/main/src/App.js
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> | |
| ); | |
| } |