Universities commited on
Commit
53d69ca
Β·
verified Β·
1 Parent(s): 67ca9f8

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +27 -0
  2. app.py +713 -0
  3. requirements.txt +9 -0
README.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Manager Intelligence Agent
3
+ emoji: 🧠
4
+ colorFrom: blue
5
+ colorTo: yellow
6
+ sdk: gradio
7
+ sdk_version: 4.44.1
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ short_description: Find any file, name, memo β€” powered by free Llama-3-8B AI
12
+ ---
13
+
14
+ # Manager Intelligence Agent
15
+
16
+ Upload company files. Search any name, date, memo. Get exact answers β€” powered by **free Llama-3-8B** via HF Inference API. No paid API key needed.
17
+
18
+ ## Features
19
+ - πŸ“ Upload & index PDF, DOCX, XLSX, CSV, PPTX, TXT
20
+ - πŸ” Keyword search across all documents
21
+ - πŸ’¬ Chat with documents using Llama-3-8B (free)
22
+ - βœ‰οΈ AI email drafting
23
+ - πŸ“‹ Task manager & calendar
24
+ - 🏠 Executive dashboard
25
+
26
+ ## Optional: Add HF Token (removes rate limits)
27
+ Space β†’ Settings β†’ Secrets β†’ add `HF_TOKEN` (read-only token from huggingface.co/settings/tokens)
app.py ADDED
@@ -0,0 +1,713 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Manager Intelligence Agent
3
+ Free HF Inference API (Llama-3-8B) β€” no paid API key needed
4
+ Deploy on Hugging Face Spaces (Gradio)
5
+ """
6
+ import os, re, json, shutil, pickle, hashlib, datetime, logging
7
+ from pathlib import Path
8
+ import gradio as gr
9
+
10
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
11
+ log = logging.getLogger(__name__)
12
+
13
+ # ── CONFIG ───────────────────────────────────────────────────────
14
+ INDEX_DIR = os.path.join(os.path.expanduser("~"), "manager_agent_index")
15
+ TASKS_FILE = os.path.join(INDEX_DIR, "_tasks.json")
16
+ EVENTS_FILE = os.path.join(INDEX_DIR, "_events.json")
17
+ os.makedirs(INDEX_DIR, exist_ok=True)
18
+
19
+ SUPPORTED = {".pdf",".docx",".doc",".xlsx",".xls",".csv",".txt",".eml",".rtf",".pptx",".ppt"}
20
+ MAX_MB = 50
21
+ ICONS = {".pdf":"πŸ“•",".docx":"πŸ“˜",".doc":"πŸ“˜",".xlsx":"πŸ“—",".xls":"πŸ“—",
22
+ ".csv":"πŸ“Š",".pptx":"πŸ“™",".ppt":"πŸ“™",".txt":"πŸ“„",".eml":"πŸ“§",".msg":"πŸ“§"}
23
+
24
+ HF_TOKEN = os.environ.get("HF_TOKEN", "") # optional β€” removes rate limits
25
+ HF_MODEL = "meta-llama/Meta-Llama-3-8B-Instruct"
26
+
27
+ # ── AI BACKEND ───────────────────────────────────────────────────
28
+ def get_client():
29
+ try:
30
+ from huggingface_hub import InferenceClient
31
+ return InferenceClient(model=HF_MODEL, token=HF_TOKEN if HF_TOKEN else None)
32
+ except Exception as e:
33
+ log.error(f"InferenceClient init: {e}")
34
+ return None
35
+
36
+ def ai_status():
37
+ try:
38
+ from huggingface_hub import InferenceClient
39
+ return "βœ… HF Inference (Llama-3-8B) β€” Free"
40
+ except:
41
+ return "⚠️ huggingface_hub not installed"
42
+
43
+ def call_llm(prompt, system=None, history=None):
44
+ client = get_client()
45
+ if not client:
46
+ return "❌ Could not connect to HF Inference API."
47
+ messages = []
48
+ if system:
49
+ messages.append({"role": "system", "content": system})
50
+ if history:
51
+ for u, b in history[-6:]:
52
+ messages += [{"role":"user","content":str(u)},
53
+ {"role":"assistant","content":str(b)}]
54
+ messages.append({"role": "user", "content": prompt})
55
+ try:
56
+ resp = client.chat_completion(messages=messages, max_tokens=1024, temperature=0.7)
57
+ return resp.choices[0].message.content.strip()
58
+ except Exception as e:
59
+ log.error(f"call_llm: {e}")
60
+ return f"❌ Inference error: {e}"
61
+
62
+ def do_generate(prompt):
63
+ return call_llm(prompt)
64
+
65
+ def do_chat_llm(prompt, context, history):
66
+ system = ("You are an elite executive assistant AI with access to the manager's document archive. "
67
+ "Answer precisely using the provided context. Cite document names. "
68
+ "Use bullet points. Flag deadlines and action items proactively.")
69
+ return call_llm(f"{prompt}\n\n--- Document Context ---\n{context}", system=system, history=history)
70
+
71
+ # ── TEXT EXTRACTION ──────────────────────────────────────────────
72
+ def extract(fp):
73
+ ext = Path(fp).suffix.lower()
74
+ try:
75
+ if ext == ".pdf":
76
+ import pdfplumber
77
+ with pdfplumber.open(fp) as pdf:
78
+ return "\n".join(p.extract_text() or "" for p in pdf.pages)
79
+ if ext in (".docx", ".doc"):
80
+ from docx import Document
81
+ doc = Document(fp)
82
+ parts = [p.text for p in doc.paragraphs if p.text.strip()]
83
+ for t in doc.tables:
84
+ for row in t.rows:
85
+ parts.append(" | ".join(c.text.strip() for c in row.cells if c.text.strip()))
86
+ return "\n".join(parts)
87
+ if ext in (".xlsx", ".xls"):
88
+ import pandas as pd
89
+ xl = pd.ExcelFile(fp)
90
+ return "\n\n".join(f"[{s}]\n{xl.parse(s).head(200).to_string(index=False)}" for s in xl.sheet_names)
91
+ if ext == ".csv":
92
+ import pandas as pd
93
+ return pd.read_csv(fp, encoding="utf-8", errors="ignore").to_string(index=False)
94
+ if ext in (".pptx", ".ppt"):
95
+ from pptx import Presentation
96
+ prs = Presentation(fp)
97
+ return "\n".join(" ".join(s.text for s in sl.shapes if hasattr(s,"text")) for sl in prs.slides)
98
+ return open(fp, "r", encoding="utf-8", errors="ignore").read()
99
+ except Exception as e:
100
+ log.warning(f"extract({fp}): {e}")
101
+ return ""
102
+
103
+ # ── INDEXING ─────────────────────────────────────────────────────
104
+ def fhash(fp):
105
+ return hashlib.md5(f"{fp}{os.path.getmtime(fp)}".encode()).hexdigest()[:12]
106
+
107
+ def is_indexed(fp):
108
+ return os.path.exists(f"{INDEX_DIR}/{fhash(fp)}.pkl")
109
+
110
+ def make_chunks(text, fname, size=350, overlap=70):
111
+ words = re.sub(r'\s+', ' ', text).strip().split()
112
+ chunks = []
113
+ for s in range(0, len(words), size - overlap):
114
+ e = min(s + size, len(words))
115
+ c = " ".join(words[s:e])
116
+ if len(c) > 50:
117
+ chunks.append({"text": c, "source": fname, "preview": c[:200]})
118
+ if e == len(words): break
119
+ return chunks
120
+
121
+ def index_file(fp):
122
+ fname = Path(fp).name
123
+ try:
124
+ size_mb = os.path.getsize(fp) / (1024*1024)
125
+ except Exception as e:
126
+ return False, f"Cannot read: {e}"
127
+ if size_mb > MAX_MB:
128
+ return False, f">{MAX_MB}MB skipped"
129
+ text = extract(fp)
130
+ if not text or len(text.strip()) < 30:
131
+ return False, "No text extracted"
132
+ chunks = make_chunks(text, fname)
133
+ if not chunks:
134
+ return False, "No chunks"
135
+ fh = fhash(fp)
136
+ meta = {"filename": fname, "filepath": str(fp),
137
+ "ftype": Path(fp).suffix.upper().strip("."),
138
+ "words": len(text.split()),
139
+ "mb": round(size_mb, 2),
140
+ "date": datetime.datetime.fromtimestamp(os.path.getmtime(fp)).strftime("%Y-%m-%d")}
141
+ with open(f"{INDEX_DIR}/{fh}.pkl", "wb") as f:
142
+ pickle.dump({"chunks": chunks, "meta": meta}, f)
143
+ return True, f"{len(chunks)} chunks indexed"
144
+
145
+ # ── SEARCH ───────────────────────────────────────────────────────
146
+ def load_all():
147
+ chunks = []
148
+ for ff in os.listdir(INDEX_DIR):
149
+ if not ff.endswith(".pkl") or ff.startswith("_"):
150
+ continue
151
+ try:
152
+ with open(f"{INDEX_DIR}/{ff}", "rb") as f:
153
+ data = pickle.load(f)
154
+ for c in data["chunks"]:
155
+ c = c.copy(); c["meta"] = data["meta"]; chunks.append(c)
156
+ except Exception as e:
157
+ log.warning(f"load_all {ff}: {e}")
158
+ return chunks
159
+
160
+ def keyword_search(query, chunks):
161
+ stop = {"the","a","an","is","in","on","at","to","for","of","and","or","it","was","are","with","this","that"}
162
+ kws = {w.lower() for w in re.findall(r'\w+', query) if len(w) > 2} - stop
163
+ if not kws: return {}
164
+ seen = {}
165
+ for c in chunks:
166
+ kh = sum(1 for kw in kws if kw in c["text"].lower())
167
+ if kh == 0: continue
168
+ fn = c["source"]
169
+ if fn not in seen or seen[fn]["kw"] < kh:
170
+ seen[fn] = {"chunk": c, "score": kh * 0.1, "kw": kh}
171
+ return seen
172
+
173
+ def run_search(query):
174
+ EMPTY = gr.Dropdown(choices=[], value=None, label="Select result")
175
+ if not query.strip():
176
+ return "<p style='color:#6b7280;padding:20px;text-align:center'>Enter a search query.</p>", [], EMPTY
177
+ chunks = load_all()
178
+ if not chunks:
179
+ return "<p style='color:#dc2626;padding:20px'>❌ No files indexed. Upload files in Documents tab.</p>", [], EMPTY
180
+ stop = {"the","a","an","is","in","on","at","to","for","of","and","or","it","was","are","with","this","that"}
181
+ kws = {w.lower() for w in re.findall(r'\w+', query) if len(w) > 2} - stop
182
+ seen = keyword_search(query, chunks)
183
+ if not seen:
184
+ all_fnames = list(dict.fromkeys(c["source"] for c in chunks))
185
+ cards = "".join(f"""<div style="background:#fff;border:1.5px solid #d1d5db;border-left:4px solid #6b7280;border-radius:8px;padding:12px 16px;margin-bottom:8px">
186
+ <div style="font-weight:700;color:#374151">πŸ“„ {fn}</div></div>""" for fn in all_fnames)
187
+ return (f"<div style='background:#fffbeb;border:1px solid #fde68a;border-left:4px solid #d97706;border-radius:8px;padding:14px 18px;margin-bottom:12px'>"
188
+ f"<strong>No matches for: <em>{query}</em></strong><br>Try shorter words.</div>"
189
+ f"<p style='font-size:.85rem;color:#374151;margin-bottom:10px'><strong>All indexed files ({len(all_fnames)}):</strong></p>" + cards,
190
+ all_fnames, gr.Dropdown(choices=all_fnames, value=None, label="Select a file"))
191
+ results = sorted(seen.items(), key=lambda x: -x[1]["score"])[:8]
192
+ html = f"<p style='color:#374151;margin-bottom:12px;font-size:.85rem'>βœ… <strong>{len(results)} documents</strong> for: <em>{query}</em></p>"
193
+ choices = []
194
+ for fname, v in results:
195
+ m = v["chunk"]["meta"]
196
+ icon = ICONS.get("." + m.get("ftype","").lower(), "πŸ“„")
197
+ prev = v["chunk"]["preview"]
198
+ for kw in kws:
199
+ prev = re.sub(f"({re.escape(kw)})",
200
+ r"<mark style='background:#fef08a;color:#713f12;border-radius:2px;padding:0 2px'>\1</mark>",
201
+ prev, flags=re.IGNORECASE)
202
+ html += f"""<div style="background:#fff;border:1.5px solid #d1d5db;border-left:5px solid #1d4ed8;
203
+ border-radius:10px;padding:16px 20px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.07)">
204
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
205
+ <div style="display:flex;gap:10px;align-items:center">
206
+ <span style="font-size:1.4rem">{icon}</span>
207
+ <div>
208
+ <div style="font-size:.92rem;font-weight:700;color:#111827">{fname}</div>
209
+ <div style="font-size:.68rem;color:#9ca3af">{m.get("ftype","")} Β· {m.get("mb",0)} MB Β· {m.get("words",0):,} words Β· {m.get("date","")}</div>
210
+ </div>
211
+ </div>
212
+ <span style="background:#eff6ff;border:1px solid #bfdbfe;color:#1d4ed8;padding:3px 10px;border-radius:50px;font-size:.72rem;font-weight:700">πŸ”‘ {v["kw"]} hits</span>
213
+ </div>
214
+ <div style="font-size:.82rem;color:#374151;line-height:1.65;border-top:1px solid #f3f4f6;padding-top:8px">{prev}…</div>
215
+ </div>"""
216
+ choices.append(fname)
217
+ return html, choices, gr.Dropdown(choices=choices, value=choices[0], label="πŸ“‚ Select a file to preview")
218
+
219
+ # ── DOCUMENT HELPERS ─────────────────────────────────────────────
220
+ def get_text(fname):
221
+ for ff in os.listdir(INDEX_DIR):
222
+ if not ff.endswith(".pkl"): continue
223
+ try:
224
+ with open(f"{INDEX_DIR}/{ff}", "rb") as f: data = pickle.load(f)
225
+ if data["meta"]["filename"] == fname:
226
+ return "\n\n".join(c["text"] for c in data["chunks"])
227
+ except: pass
228
+ return ""
229
+
230
+ def all_meta():
231
+ docs = []
232
+ for ff in os.listdir(INDEX_DIR):
233
+ if not ff.endswith(".pkl"): continue
234
+ try:
235
+ with open(f"{INDEX_DIR}/{ff}", "rb") as f: data = pickle.load(f)
236
+ docs.append(data["meta"])
237
+ except: pass
238
+ return sorted(docs, key=lambda x: x.get("date",""), reverse=True)
239
+
240
+ def all_names(): return [d["filename"] for d in all_meta()]
241
+
242
+ def lib_stats():
243
+ docs = all_meta()
244
+ if not docs: return "*No files indexed yet.*"
245
+ tw = sum(d.get("words",0) for d in docs)
246
+ lines = [f"**πŸ“š {len(docs)} files Β· {tw:,} words**\n"]
247
+ for d in docs[:40]:
248
+ icon = ICONS.get("." + d.get("ftype","").lower(), "πŸ“„")
249
+ lines.append(f"{icon} **{d['filename']}** Β· {d.get('words',0):,}w Β· {d.get('date','')} Β· {d.get('ftype','')}")
250
+ if len(docs) > 40: lines.append(f"*...and {len(docs)-40} more*")
251
+ return "\n".join(lines)
252
+
253
+ # ── TASKS ────────────────────────────────────────────────────────
254
+ def load_tasks():
255
+ try:
256
+ if os.path.exists(TASKS_FILE):
257
+ with open(TASKS_FILE) as f: return json.load(f)
258
+ except: pass
259
+ return []
260
+
261
+ def save_tasks(t):
262
+ with open(TASKS_FILE, "w") as f: json.dump(t, f, indent=2)
263
+
264
+ def tasks_html():
265
+ tasks = load_tasks(); today = datetime.date.today().isoformat()
266
+ if not tasks:
267
+ return "<p style='padding:20px;text-align:center;color:#6b7280;background:#f9fafb;border:2px dashed #d1d5db;border-radius:8px'>No tasks yet.</p>"
268
+ rows = ""
269
+ for i, t in enumerate(tasks):
270
+ done = t.get("done", False)
271
+ due = t.get("due", "")
272
+ ov = due and due < today and not done
273
+ bg = "#fef2f2" if ov else ("#f9fafb" if done else "#fff")
274
+ bl = "#dc2626" if ov else ("#d1d5db" if done else "#1d4ed8")
275
+ pri = t.get("priority","medium")
276
+ pc = {"high":"#dc2626","medium":"#d97706","low":"#15803d"}.get(pri,"#6b7280")
277
+ rows += f"""<div style="display:flex;align-items:center;gap:10px;background:{bg};
278
+ border:1px solid #e5e7eb;border-left:4px solid {bl};border-radius:8px;padding:11px 14px;opacity:{'0.55' if done else '1'};margin-bottom:7px">
279
+ <span style="font-family:monospace;font-size:.68rem;color:#9ca3af;background:#f3f4f6;border:1px solid #e5e7eb;border-radius:4px;padding:1px 6px;flex-shrink:0">#{i}</span>
280
+ <div style="flex:1">
281
+ <div style="font-size:.85rem;font-weight:500;color:#111827;{'text-decoration:line-through;color:#9ca3af' if done else ''}">{t['text']}</div>
282
+ {f'<div style="font-size:.70rem;color:{"#dc2626" if ov else "#9ca3af"};margin-top:2px">πŸ“… {due}{" ⚠️ OVERDUE" if ov else ""}</div>' if due else ''}
283
+ </div>
284
+ <span style="font-size:.62rem;font-weight:700;padding:2px 8px;border-radius:50px;color:{pc};border:1px solid {pc}40">{pri.upper()}</span>
285
+ </div>"""
286
+ return rows + "<p style='font-size:.70rem;color:#9ca3af;text-align:center;margin-top:4px;font-style:italic'>Use task # to toggle/delete</p>"
287
+
288
+ # ── EVENTS ───────────────────────────────────────────────────────
289
+ def load_events():
290
+ try:
291
+ if os.path.exists(EVENTS_FILE):
292
+ with open(EVENTS_FILE) as f: return json.load(f)
293
+ except: pass
294
+ return []
295
+
296
+ def save_events(e):
297
+ with open(EVENTS_FILE, "w") as f: json.dump(e, f, indent=2)
298
+
299
+ def events_html():
300
+ evs = load_events(); today = datetime.date.today().isoformat()
301
+ up = sorted([e for e in evs if e.get("date","") >= today], key=lambda x: x["date"])
302
+ past = sorted([e for e in evs if e.get("date","") < today], key=lambda x: x["date"], reverse=True)[:3]
303
+ if not up and not past:
304
+ return "<p style='padding:20px;text-align:center;color:#6b7280;background:#f9fafb;border:2px dashed #d1d5db;border-radius:8px'>No events yet.</p>"
305
+ def row(e, old=False):
306
+ try: day=datetime.datetime.strptime(e["date"],"%Y-%m-%d").strftime("%d"); mon=datetime.datetime.strptime(e["date"],"%Y-%m-%d").strftime("%b %Y")
307
+ except: day=e.get("date",""); mon=""
308
+ return f"""<div style="display:flex;align-items:center;gap:12px;background:{'#f9fafb' if old else '#fff'};
309
+ border:1px solid #e5e7eb;border-radius:8px;padding:11px 14px;margin-bottom:7px;opacity:{'0.45' if old else '1'}">
310
+ <div style="text-align:center;background:#eff6ff;border-radius:6px;padding:6px 10px;min-width:50px;flex-shrink:0">
311
+ <div style="font-size:1.3rem;font-weight:800;color:#1d4ed8;line-height:1">{day}</div>
312
+ <div style="font-size:.58rem;color:#3b82f6;text-transform:uppercase">{mon}</div>
313
+ </div>
314
+ <div>
315
+ <div style="font-size:.85rem;font-weight:600;color:#111827">{e['title']}</div>
316
+ {f'<div style="font-size:.70rem;color:#6b7280;margin-top:2px">πŸ• {e["time"]}</div>' if e.get("time") else ''}
317
+ {f'<div style="font-size:.70rem;color:#9ca3af;font-style:italic">{e["note"]}</div>' if e.get("note") else ''}
318
+ </div>
319
+ </div>"""
320
+ html = ""
321
+ if up:
322
+ html += "<p style='font-size:.72rem;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px'>πŸ“… Upcoming</p>"
323
+ html += "".join(row(e) for e in up[:10])
324
+ if past:
325
+ html += "<p style='font-size:.72rem;font-weight:700;color:#9ca3af;text-transform:uppercase;letter-spacing:.08em;margin:10px 0 6px'>Past</p>"
326
+ html += "".join(row(e, True) for e in past)
327
+ return html
328
+
329
+ # ── DASHBOARD ────────────────────────────────────────────────────
330
+ def dashboard_html():
331
+ tasks = load_tasks(); today = datetime.date.today().isoformat()
332
+ today_s = datetime.date.today().strftime("%A, %B %d, %Y")
333
+ hr = datetime.datetime.now().hour
334
+ greet = "Good morning" if hr < 12 else "Good afternoon" if hr < 17 else "Good evening"
335
+ pending = [t for t in tasks if not t.get("done")]
336
+ overdue = [t for t in pending if t.get("due","") and t["due"] < today]
337
+ hi = [t for t in pending if t.get("priority") == "high"]
338
+ evs = load_events()
339
+ up = sorted([e for e in evs if e.get("date","") >= today], key=lambda x: x["date"])[:5]
340
+ docs = all_meta(); recent = docs[:6]
341
+ def stat(n, lbl, bg, tc, bc):
342
+ return f"""<div style="background:{bg};border:1.5px solid {bc};border-radius:10px;padding:16px 18px;box-shadow:0 1px 4px rgba(0,0,0,.06)">
343
+ <div style="font-size:1.9rem;font-weight:800;color:{tc};line-height:1">{n}</div>
344
+ <div style="font-size:.70rem;color:{tc};font-weight:600;text-transform:uppercase;letter-spacing:.07em;margin-top:5px;opacity:.8">{lbl}</div>
345
+ </div>"""
346
+ stats = f"""<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin-bottom:20px">
347
+ {stat(len(docs),"Indexed Docs","#eff6ff","#1d4ed8","#bfdbfe")}
348
+ {stat(len(pending),"Pending Tasks","#fffbeb","#d97706","#fde68a")}
349
+ {stat(len(overdue),"Overdue","#fef2f2" if overdue else "#f0fdf4","#dc2626" if overdue else "#15803d","#fecaca" if overdue else "#bbf7d0")}
350
+ {stat(len(hi),"High Priority","#fef2f2" if hi else "#f0fdf4","#dc2626" if hi else "#15803d","#fecaca" if hi else "#bbf7d0")}
351
+ {stat(len(up),"Upcoming Events","#f5f3ff","#7c3aed","#ddd6fe")}
352
+ </div>"""
353
+ def card(title, rows_html, empty_msg):
354
+ return f"""<div style="background:#fff;border:1.5px solid #e5e7eb;border-radius:10px;padding:16px 18px;box-shadow:0 1px 4px rgba(0,0,0,.06)">
355
+ <div style="font-size:.72rem;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px;padding-bottom:8px;border-bottom:2px solid #fef3c7">{title}</div>
356
+ {rows_html or f'<p style="color:#9ca3af;font-size:.80rem;text-align:center;padding:12px 0">{empty_msg}</p>'}
357
+ </div>"""
358
+ def drow(icon, text, meta):
359
+ return f"""<div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid #f9fafb;font-size:.82rem">
360
+ <span>{icon}</span><span style="flex:1;color:#374151;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{text}</span>
361
+ <span style="font-size:.68rem;color:#9ca3af">{meta}</span>
362
+ </div>"""
363
+ task_rows = "".join(drow("⬜", t["text"][:45], t.get("due","")) for t in pending[:5])
364
+ ev_rows = "".join(drow("πŸ“…", e["title"][:45], e["date"]) for e in up)
365
+ doc_rows = "".join(drow(ICONS.get("."+d.get("ftype","").lower(),"πŸ“„"), d["filename"][:45], d.get("date","")) for d in recent)
366
+ return f"""<div style="padding:4px 0">
367
+ <div style="font-size:1.5rem;font-weight:800;color:#111827;letter-spacing:-.02em">{greet}, Manager</div>
368
+ <div style="font-size:.80rem;color:#6b7280;margin-bottom:20px">{today_s}</div>
369
+ {stats}
370
+ <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px">
371
+ {card("πŸ“‹ Active Tasks", task_rows, "All tasks complete! πŸŽ‰")}
372
+ {card("πŸ—“οΈ Upcoming Events", ev_rows, "No upcoming events")}
373
+ {card("πŸ“ Recent Documents", doc_rows, "No documents indexed yet")}
374
+ </div></div>"""
375
+
376
+ # ── CHAT ─────────────────────────────────────────────────────────
377
+ def do_chat(message, history, focus):
378
+ if not message.strip(): return history, ""
379
+ ctx = []
380
+ if focus and focus not in ("", "β€” All Documents β€”"):
381
+ t = get_text(focus)
382
+ if t: ctx.append(f"[{focus}]\n{t[:3000]}")
383
+ else:
384
+ chunks = load_all()
385
+ kw = keyword_search(message, chunks)
386
+ for fn, v in sorted(kw.items(), key=lambda x:-x[1]["kw"])[:5]:
387
+ t = get_text(fn)
388
+ if t: ctx.append(f"[{fn}]\n{t[:800]}")
389
+ context = "\n\n---\n\n".join(ctx) if ctx else "No documents indexed yet."
390
+ try:
391
+ ans = do_chat_llm(message, context, [(h[0],h[1]) for h in history[-8:]])
392
+ except Exception as e:
393
+ ans = f"❌ {e}"
394
+ return history + [[message, ans]], ""
395
+
396
+ def do_analyze(filename):
397
+ if not filename: return [["","⚠️ Select a document first."]], []
398
+ text = get_text(filename)
399
+ if not text: return [[f'❌',f"'{filename}' not in index."]], []
400
+ prompt = f"""Analyze this document as an executive assistant.
401
+ # {filename}
402
+ ## Executive Summary
403
+ ## Key People
404
+ ## Important Dates
405
+ ## Financial Data
406
+ ## Decisions & Action Items
407
+ ## Risks & Flags
408
+ Document:\n{text[:4000]}"""
409
+ try:
410
+ return [[f"πŸ“Š {filename}", do_generate(prompt)]], []
411
+ except Exception as e:
412
+ return [[f"πŸ“Š {filename}", f"❌ {e}"]], []
413
+
414
+ # ── EMAIL ────────────────────────────────────────────────────────
415
+ def do_email(instructions, doc, tone):
416
+ if not instructions.strip(): return "⚠️ Describe the email first."
417
+ ctx = ""
418
+ if doc and doc not in ("", "β€” None β€”"):
419
+ t = get_text(doc)
420
+ if t: ctx = f"\n\nDocument context ({doc}):\n{t[:2000]}"
421
+ tones = {"Formal & Executive":"formal, authoritative",
422
+ "Professional & Warm":"professional, warm",
423
+ "Concise & Direct":"very concise, direct",
424
+ "Diplomatic":"diplomatic, nuanced"}
425
+ prompt = f"""Write a complete professional business email.
426
+ Tone: {tones.get(tone,"formal")}
427
+ Instructions: {instructions}{ctx}
428
+ Format:
429
+ Subject: [subject]
430
+ Dear [Recipient],
431
+ [body]
432
+ Best regards,
433
+ [Manager Name]"""
434
+ try: return do_generate(prompt)
435
+ except Exception as e: return f"❌ {e}"
436
+
437
+ # ── TASK HANDLERS ────────────────────────────────────────────────
438
+ def add_task(txt, due, pri, note):
439
+ if not txt.strip(): return tasks_html(), "⚠️ Enter task text", "", "", "medium", ""
440
+ t = load_tasks()
441
+ t.append({"text":txt.strip(),"due":due.strip(),"priority":pri,"note":note,
442
+ "done":False,"created":datetime.date.today().isoformat()})
443
+ save_tasks(t); return tasks_html(), "", "", "", "medium", ""
444
+
445
+ def toggle_task(idx):
446
+ t = load_tasks()
447
+ try:
448
+ i = int(idx.strip())
449
+ if 0 <= i < len(t): t[i]["done"] = not t[i]["done"]
450
+ save_tasks(t)
451
+ except: pass
452
+ return tasks_html(), ""
453
+
454
+ def delete_task(idx):
455
+ t = load_tasks()
456
+ try:
457
+ i = int(idx.strip())
458
+ if 0 <= i < len(t): t.pop(i)
459
+ save_tasks(t)
460
+ except: pass
461
+ return tasks_html(), ""
462
+
463
+ # ── EVENT HANDLERS ───────────────────────────────────────────────
464
+ def add_event(title, date, time, note):
465
+ if not title.strip() or not date.strip():
466
+ return events_html(), "⚠️ Title and date required", "", "", "", ""
467
+ e = load_events()
468
+ e.append({"title":title.strip(),"date":date.strip(),"time":time.strip(),"note":note.strip()})
469
+ save_events(e); return events_html(), "", "", "", "", ""
470
+
471
+ def delete_event(idx):
472
+ e = load_events()
473
+ try:
474
+ i = int(idx.strip())
475
+ if 0 <= i < len(e): e.pop(i)
476
+ save_events(e)
477
+ except: pass
478
+ return events_html(), ""
479
+
480
+ # ── INDEX HANDLERS ───────────────────────────────────────────────
481
+ def do_upload(files, progress=gr.Progress()):
482
+ if not files: return "⚠️ No files selected.", lib_stats()
483
+ results = []
484
+ for i, f in enumerate(files):
485
+ progress(i / len(files), desc=f"Indexing {Path(f.name).name}")
486
+ good, msg = index_file(f.name)
487
+ results.append(f"{'βœ…' if good else '⚠️'} {Path(f.name).name} β€” {msg}")
488
+ return "\n".join(results), lib_stats()
489
+
490
+ def do_clear():
491
+ shutil.rmtree(INDEX_DIR, ignore_errors=True)
492
+ os.makedirs(INDEX_DIR, exist_ok=True)
493
+ return "πŸ—‘οΈ Index cleared.", lib_stats()
494
+
495
+ def do_load(fname):
496
+ if not fname: return "*Select a file.*", ""
497
+ text = get_text(fname)
498
+ if not text: return f"❌ '{fname}' not found.", ""
499
+ for ff in os.listdir(INDEX_DIR):
500
+ if not ff.endswith(".pkl"): continue
501
+ try:
502
+ with open(f"{INDEX_DIR}/{ff}", "rb") as f: data = pickle.load(f)
503
+ if data["meta"]["filename"] == fname:
504
+ m = data["meta"]
505
+ return f"**{fname}** Β· {m.get('words',0):,} words Β· {m.get('mb',0)} MB Β· {m.get('date','')}", text
506
+ except: pass
507
+ return f"**{fname}**", text
508
+
509
+ # ── CSS ──────────────────────────────────────────────────────────
510
+ CSS = """
511
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
512
+ body, .gradio-container{background:#f0f4f8!important;font-family:'Inter',sans-serif!important;color:#111827!important}
513
+ .gradio-container{max-width:100%!important;padding:0!important}
514
+ textarea,input[type=text]{background:#fff!important;border:1.5px solid #d1d5db!important;border-radius:8px!important;color:#111827!important;font-family:'Inter',sans-serif!important;font-size:.86rem!important}
515
+ textarea:focus,input:focus{border-color:#1d4ed8!important;outline:none!important;box-shadow:0 0 0 3px rgba(29,78,216,.1)!important}
516
+ .gr-button{font-family:'Inter',sans-serif!important;font-weight:600!important;border-radius:8px!important;font-size:.83rem!important}
517
+ .gr-button.primary{background:#1d4ed8!important;color:#fff!important;border:none!important}
518
+ .gr-button.primary:hover{background:#1e40af!important}
519
+ .gr-button.secondary{background:#fff!important;color:#374151!important;border:1.5px solid #d1d5db!important}
520
+ ::-webkit-scrollbar{width:5px;height:5px}
521
+ ::-webkit-scrollbar-thumb{background:#d1d5db;border-radius:3px}
522
+ """
523
+
524
+ # ── UI ───────────────────────────────────────────────────────────
525
+ _ok = True
526
+ _badge = ai_status()
527
+
528
+ with gr.Blocks(title="Manager Intelligence Agent", css=CSS) as demo:
529
+ HIST = gr.State([])
530
+ gr.HTML(f"""<div style="background:linear-gradient(135deg,#1e3a8a,#1d4ed8);padding:18px 32px 16px;border-bottom:3px solid #d97706">
531
+ <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:14px">
532
+ <div style="display:flex;align-items:center;gap:14px">
533
+ <div style="width:46px;height:46px;background:rgba(255,255,255,.15);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:22px">🧠</div>
534
+ <div>
535
+ <div style="font-size:1.4rem;font-weight:800;color:#fff">Manager <span style="color:#fbbf24">Intelligence</span> Agent</div>
536
+ <div style="font-size:.68rem;color:rgba(255,255,255,.7);text-transform:uppercase;letter-spacing:.1em;margin-top:3px">Executive OS Β· Free AI Β· Hugging Face Spaces</div>
537
+ </div>
538
+ </div>
539
+ <div style="display:flex;gap:7px;flex-wrap:wrap;align-items:center">
540
+ <span style="background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.2);color:rgba(255,255,255,.9);padding:3px 11px;border-radius:50px;font-size:.68rem">πŸ¦™ Llama-3-8B</span>
541
+ <span style="background:rgba(251,191,36,.15);border:1px solid rgba(251,191,36,.4);color:#fbbf24;padding:3px 11px;border-radius:50px;font-size:.68rem">PDF Β· DOCX Β· XLSX Β· CSV Β· PPTX</span>
542
+ <span style="padding:5px 14px;border-radius:50px;font-size:.72rem;font-weight:600;background:rgba(74,222,128,.15);border:1px solid rgba(74,222,128,.5);color:#4ade80">{_badge}</span>
543
+ </div>
544
+ </div>
545
+ </div>""")
546
+
547
+ with gr.Tabs():
548
+
549
+ with gr.Tab("🏠 Dashboard"):
550
+ dash = gr.HTML(dashboard_html())
551
+ gr.Button("πŸ”„ Refresh", variant="secondary").click(dashboard_html, outputs=[dash])
552
+ gr.HTML("""<div style="background:#eff6ff;border:1px solid #bfdbfe;border-left:4px solid #1d4ed8;
553
+ border-radius:8px;padding:12px 16px;font-size:.82rem;color:#1e40af;margin-top:10px">
554
+ <strong>Getting started:</strong> Go to <strong>Documents</strong> β†’ upload files β†’ click Index.
555
+ Then use <strong>Search</strong> or <strong>Chat</strong> to work with your documents.
556
+ </div>""")
557
+
558
+ with gr.Tab("πŸ” Search"):
559
+ with gr.Row():
560
+ s_q = gr.Textbox(label="Search", placeholder='"Ahmed Al-Rashidi 2023" Β· "Q3 budget" Β· "contract renewal"', lines=1, scale=5)
561
+ s_btn = gr.Button("πŸ” Search", variant="primary", scale=1)
562
+ s_sum = gr.Markdown("*Enter a query and click Search.*")
563
+ s_html = gr.HTML("")
564
+ s_dd = gr.Dropdown(label="πŸ“‚ Select a file to preview", choices=[], value=None)
565
+ def do_search_all(q):
566
+ html, choices, dd = run_search(q)
567
+ return html, f"πŸ”‘ Found **{len(choices)}** results for: *{q}*" if choices else "*No results.*", dd
568
+ s_btn.click(do_search_all, inputs=[s_q], outputs=[s_html, s_sum, s_dd])
569
+ s_q.submit(do_search_all, inputs=[s_q], outputs=[s_html, s_sum, s_dd])
570
+
571
+ with gr.Tab("πŸ’¬ Chat & Intelligence"):
572
+ with gr.Row():
573
+ with gr.Column(scale=1, min_width=260):
574
+ c_focus = gr.Dropdown(label="Focus on file (optional)",
575
+ choices=["β€” All Documents β€”"] + all_names(), value="β€” All Documents β€”")
576
+ gr.Button("πŸ”„ Refresh Files", variant="secondary").click(
577
+ lambda: gr.Dropdown(choices=["β€” All Documents β€”"] + all_names()), outputs=[c_focus])
578
+ gr.HTML("""<div style="background:#eff6ff;border:1px solid #bfdbfe;border-left:4px solid #1d4ed8;
579
+ border-radius:8px;padding:12px 16px;font-size:.82rem;color:#1e40af;margin-top:12px;line-height:1.8">
580
+ <strong>Try asking:</strong><br>β€’ Find all records for Ahmed Hassan<br>
581
+ β€’ Summarize the Q3 financial report<br>β€’ List all salary changes 2020–2024<br>
582
+ β€’ Who approved the merger?<br>β€’ What contracts expire this year?</div>""")
583
+ gr.HTML("<div style='font-size:.72rem;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.08em;margin:16px 0 8px'>⚑ Document Analysis</div>")
584
+ a_file = gr.Dropdown(label="Select document", choices=all_names())
585
+ gr.Button("πŸ”„", variant="secondary").click(lambda: gr.Dropdown(choices=all_names()), outputs=[a_file])
586
+ a_btn = gr.Button("πŸ“Š Full Analysis", variant="primary")
587
+ with gr.Column(scale=3):
588
+ chatbot = gr.Chatbot(label="", height=460, show_label=False)
589
+ with gr.Row():
590
+ c_in = gr.Textbox(label="", show_label=False, placeholder="Ask anything about your documents...", lines=2, scale=5)
591
+ with gr.Column(scale=1, min_width=90):
592
+ c_send = gr.Button("Send ↑", variant="primary")
593
+ c_clear = gr.Button("Clear", variant="secondary")
594
+ def chat_fn(msg, hist, focus):
595
+ new_hist, _ = do_chat(msg, hist, focus)
596
+ return new_hist, "", new_hist
597
+ c_send.click(chat_fn, inputs=[c_in, HIST, c_focus], outputs=[HIST, c_in, chatbot])
598
+ c_in.submit(chat_fn, inputs=[c_in, HIST, c_focus], outputs=[HIST, c_in, chatbot])
599
+ c_clear.click(lambda: ([], []), outputs=[HIST, chatbot])
600
+ a_btn.click(do_analyze, inputs=[a_file], outputs=[chatbot, HIST])
601
+
602
+ with gr.Tab("βœ‰οΈ Email Drafts"):
603
+ with gr.Row():
604
+ with gr.Column(scale=1):
605
+ e_tone = gr.Dropdown(label="Tone",
606
+ choices=["Formal & Executive","Professional & Warm","Concise & Direct","Diplomatic"],
607
+ value="Formal & Executive")
608
+ e_doc = gr.Dropdown(label="Reference document (optional)",
609
+ choices=["β€” None β€”"] + all_names(), value="β€” None β€”")
610
+ gr.Button("πŸ”„ Refresh", variant="secondary").click(
611
+ lambda: gr.Dropdown(choices=["β€” None β€”"] + all_names()), outputs=[e_doc])
612
+ with gr.Column(scale=2):
613
+ e_inst = gr.Textbox(label="Email instructions", placeholder="e.g. Write email to HR requesting 2 new engineers...", lines=5)
614
+ e_btn = gr.Button("βœ‰οΈ Draft Email", variant="primary")
615
+ e_out = gr.Textbox(label="Email Draft β€” copy and send", lines=20, max_lines=35)
616
+ e_btn.click(do_email, inputs=[e_inst, e_doc, e_tone], outputs=[e_out])
617
+
618
+ with gr.Tab("πŸ“‹ Tasks & Calendar"):
619
+ with gr.Row():
620
+ with gr.Column(scale=1):
621
+ gr.HTML("<div style='font-size:.95rem;font-weight:700;color:#111827;margin-bottom:10px'>πŸ“‹ Task Manager</div>")
622
+ with gr.Row():
623
+ t_txt = gr.Textbox(label="Task", placeholder="What needs to be done?", scale=3)
624
+ t_due = gr.Textbox(label="Due (YYYY-MM-DD)", placeholder="2025-12-31", scale=2)
625
+ with gr.Row():
626
+ t_pri = gr.Dropdown(label="Priority", choices=["high","medium","low"], value="medium", scale=1)
627
+ t_note = gr.Textbox(label="Note", scale=2)
628
+ with gr.Row():
629
+ t_add = gr.Button("βž• Add Task", variant="primary")
630
+ t_msg = gr.Markdown("")
631
+ t_disp = gr.HTML(tasks_html())
632
+ with gr.Row():
633
+ t_idx = gr.Textbox(label="Task #", placeholder="0", scale=1)
634
+ gr.Button("βœ… Toggle", variant="secondary", scale=1).click(toggle_task, inputs=[t_idx], outputs=[t_disp, t_msg])
635
+ gr.Button("πŸ—‘οΈ Delete", variant="secondary", scale=1).click(delete_task, inputs=[t_idx], outputs=[t_disp, t_msg])
636
+ with gr.Column(scale=1):
637
+ gr.HTML("<div style='font-size:.95rem;font-weight:700;color:#111827;margin-bottom:10px'>πŸ—“οΈ Calendar & Events</div>")
638
+ with gr.Row():
639
+ ev_t = gr.Textbox(label="Event title", scale=3)
640
+ ev_d = gr.Textbox(label="Date (YYYY-MM-DD)", scale=2)
641
+ with gr.Row():
642
+ ev_time = gr.Textbox(label="Time", placeholder="14:00", scale=1)
643
+ ev_note = gr.Textbox(label="Note", scale=2)
644
+ with gr.Row():
645
+ ev_add = gr.Button("πŸ“… Add Event", variant="primary")
646
+ ev_msg = gr.Markdown("")
647
+ ev_disp = gr.HTML(events_html())
648
+ with gr.Row():
649
+ ev_idx = gr.Textbox(label="Event # to delete", placeholder="0", scale=1)
650
+ gr.Button("πŸ—‘οΈ Delete", variant="secondary", scale=2).click(delete_event, inputs=[ev_idx], outputs=[ev_disp, ev_msg])
651
+ t_add.click(add_task, inputs=[t_txt, t_due, t_pri, t_note], outputs=[t_disp, t_msg, t_txt, t_due, t_pri, t_note])
652
+ ev_add.click(add_event, inputs=[ev_t, ev_d, ev_time, ev_note], outputs=[ev_disp, ev_msg, ev_t, ev_d, ev_time, ev_note])
653
+
654
+ with gr.Tab("πŸ“ Documents"):
655
+ with gr.Tabs():
656
+ with gr.Tab("⬆️ Upload & Index"):
657
+ gr.HTML("""<div style="background:#eff6ff;border:1px solid #bfdbfe;border-left:4px solid #1d4ed8;
658
+ border-radius:8px;padding:12px 16px;font-size:.82rem;color:#1e40af;margin-bottom:10px">
659
+ <strong>Upload your files.</strong> Supported: PDF, DOCX, XLSX, CSV, PPTX, TXT, EML.<br>
660
+ ⚠️ HF free tier = storage resets on restart. Re-upload files after restart.
661
+ </div>""")
662
+ with gr.Row():
663
+ with gr.Column(scale=1):
664
+ f_up = gr.File(label="Select files", file_count="multiple",
665
+ file_types=[".pdf",".docx",".doc",".xlsx",".xls",".csv",".txt",".pptx",".ppt",".eml",".rtf"])
666
+ f_upbtn = gr.Button("⚑ Index Uploaded Files", variant="primary")
667
+ f_clr = gr.Button("πŸ—‘οΈ Clear Index", variant="secondary")
668
+ with gr.Column(scale=1):
669
+ f_log = gr.Markdown("*Upload files then click Index.*")
670
+ f_stats = gr.Markdown(lib_stats())
671
+ f_upbtn.click(do_upload, inputs=[f_up], outputs=[f_log, f_stats])
672
+ f_clr.click(do_clear, outputs=[f_log, f_stats])
673
+ with gr.Tab("πŸ“„ Preview"):
674
+ with gr.Row():
675
+ p_sel = gr.Dropdown(label="Select document", choices=all_names(), scale=4)
676
+ p_load = gr.Button("πŸ“„ Load", variant="primary", scale=1)
677
+ gr.Button("πŸ”„", variant="secondary", scale=1).click(lambda: gr.Dropdown(choices=all_names()), outputs=[p_sel])
678
+ p_info = gr.Markdown("*Select a file and click Load.*")
679
+ p_text = gr.Textbox(label="Document content", lines=28, max_lines=60)
680
+ p_load.click(do_load, inputs=[p_sel], outputs=[p_info, p_text])
681
+
682
+ with gr.Tab("βš™οΈ Setup"):
683
+ gr.Markdown(f"""
684
+ ## Configuration
685
+
686
+ **AI Backend:** HF Inference API Β· `{HF_MODEL}` Β· **100% Free**
687
+
688
+ ### Optional: Add HF Token (removes rate limits)
689
+ 1. [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) β†’ New token β†’ Role: **Read**
690
+ 2. Space β†’ **Settings β†’ Secrets** β†’ add `HF_TOKEN`
691
+ 3. Restart Space
692
+
693
+ **Current status:** {_badge}
694
+
695
+ ### Supported Files
696
+ | Type | Extensions |
697
+ |------|-----------|
698
+ | Documents | .pdf .docx .doc .rtf |
699
+ | Spreadsheets | .xlsx .xls .csv |
700
+ | Presentations | .pptx .ppt |
701
+ | Text / Email | .txt .eml |
702
+
703
+ ### Storage Note
704
+ Free tier = ephemeral. Files reset on restart. Re-upload or connect HF Dataset for persistence.
705
+ """)
706
+
707
+ gr.HTML("""<div style="background:#fff;border-top:1px solid #e5e7eb;padding:12px 32px;text-align:center;font-size:.70rem;color:#9ca3af">
708
+ <span style="color:#1d4ed8;font-weight:700">Manager Intelligence Agent</span> Β· Free AI Β· Llama-3-8B Β· Hugging Face Spaces
709
+ </div>""")
710
+
711
+ if __name__ == "__main__":
712
+ print(f"\n{'='*50}\n Manager Intelligence Agent\n AI: {ai_status()}\n{'='*50}\n")
713
+ demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ gradio==4.44.1
2
+ huggingface_hub==0.24.7
3
+ pdfplumber==0.11.4
4
+ python-docx==1.1.2
5
+ pandas==2.2.2
6
+ openpyxl==3.1.5
7
+ numpy==1.26.4
8
+ requests==2.32.3
9
+ python-pptx==1.0.2