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>
  );
}