Souvikbasur commited on
Commit
d2bb954
Β·
verified Β·
1 Parent(s): bbcebe6

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +50 -0
  2. app.py +904 -0
  3. gitignore +42 -0
  4. index.html +1089 -0
  5. memory.json +1 -0
  6. requirements.txt +14 -0
Dockerfile ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ── Hugging Face Spaces β€” Docker Runtime ─────────────────────────────────────
2
+ # Python 3.11 slim keeps the image lean while supporting all crewai deps.
3
+ FROM python:3.11.9-slim
4
+
5
+ # Prevent .pyc files and force unbuffered stdout/stderr (crucial for HF logs)
6
+ ENV PYTHONDONTWRITEBYTECODE=1 \
7
+ PYTHONUNBUFFERED=1
8
+
9
+ WORKDIR /app
10
+
11
+ # ── System deps required by crewai / grpc / tiktoken ─────────────────────────
12
+ RUN apt-get update && apt-get install -y --no-install-recommends \
13
+ build-essential \
14
+ curl \
15
+ git \
16
+ libffi-dev \
17
+ && rm -rf /var/lib/apt/lists/*
18
+
19
+ # ── Python deps (cached layer β€” only rebuilds when requirements.txt changes) ──
20
+ COPY requirements.txt .
21
+ RUN pip install --no-cache-dir --upgrade pip \
22
+ && pip install --no-cache-dir -r requirements.txt
23
+
24
+ # ── Application files ─────────────────────────────────────────────────────────
25
+ COPY app.py .
26
+ COPY index.html .
27
+
28
+ # ── Memory file: pre-create so the container can write to it immediately ──────
29
+ # On HF Spaces the filesystem is ephemeral, but this ensures the file exists
30
+ # on first boot without any code-level FileNotFoundError.
31
+ RUN echo "[]" > /app/memory.json
32
+
33
+ # ── Hugging Face Spaces REQUIRES port 7860 ────────────────────────────────────
34
+ ENV PORT=7860
35
+ EXPOSE 7860
36
+
37
+ # ── Gunicorn config ───────────────────────────────────────────────────────────
38
+ # 1 worker + 8 threads β†’ ideal for HF single-instance Spaces
39
+ # timeout 300 β†’ multi-agent CrewAI runs can take 2-4 min
40
+ # graceful-timeout 10 β†’ clean shutdown on redeploy
41
+ CMD exec gunicorn \
42
+ --bind "0.0.0.0:7860" \
43
+ --workers 1 \
44
+ --threads 8 \
45
+ --timeout 300 \
46
+ --graceful-timeout 10 \
47
+ --log-level info \
48
+ --access-logfile - \
49
+ --error-logfile - \
50
+ app:app
app.py ADDED
@@ -0,0 +1,904 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, send_from_directory
2
+ from flask_cors import CORS
3
+ from crewai import Agent, Task, Crew, LLM
4
+ from crewai.process import Process
5
+ from crewai_tools import SerperDevTool
6
+ import os
7
+ import json
8
+ import time
9
+ import fcntl
10
+ from datetime import datetime
11
+
12
+ # ── Load .env only in local development ──────────────────────────────────────
13
+ try:
14
+ from dotenv import load_dotenv
15
+ load_dotenv()
16
+ except ImportError:
17
+ pass
18
+
19
+ app = Flask(__name__)
20
+ CORS(app)
21
+
22
+ # ══════════════════════════════════════════════════════════════════════════════
23
+ # API KEYS
24
+ # ══════════════════════════════════════════════════════════════════════════════
25
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
26
+ SERPER_API_KEY = os.getenv("SERPER_API_KEY")
27
+
28
+ if not GEMINI_API_KEY or not SERPER_API_KEY:
29
+ raise EnvironmentError(
30
+ "Missing required environment variables. "
31
+ "Set GEMINI_API_KEY and SERPER_API_KEY in the environment or .env."
32
+ )
33
+
34
+ os.environ["SERPER_API_KEY"] = SERPER_API_KEY
35
+
36
+ # ══════════════════════════════════════════════════════════════════════════════
37
+ # MODEL INITIALISATION
38
+ #
39
+ # lite_llm β†’ gemini-2.0-flash-lite fast Β· high RPM Β· low complexity
40
+ # pro_llm β†’ gemini-2.5-flash quality Β· reserved for final work
41
+ # search_llm β†’ gemini-2.0-flash-lite used for standalone search agent
42
+ # (gemma-2-9b-it removed β€” not reliably available via same key)
43
+ # ══════════════════════════════════════════════════════════════════════════════
44
+
45
+ lite_llm = LLM(
46
+ model="gemini/gemini-2.5-flash-lite",
47
+ temperature=0.2,
48
+ api_key=GEMINI_API_KEY,
49
+ )
50
+
51
+ pro_llm = LLM(
52
+ model="gemini/gemini-2.5-flash",
53
+ temperature=0.3,
54
+ api_key=GEMINI_API_KEY,
55
+ )
56
+
57
+ # Search agent uses lite to preserve pro quota
58
+ search_llm = LLM(
59
+ model="gemini/gemini-2.5-flash-lite",
60
+ temperature=0.1,
61
+ api_key=GEMINI_API_KEY,
62
+ )
63
+
64
+ # ── Tools ─────────────────────────────────────────────────────────────────────
65
+ search_tool = SerperDevTool()
66
+
67
+ # ══════════════════════════════════════════════════════════════════════════════
68
+ # MEMORY CONFIGURATION
69
+ # ══════════════════════════════════════════════════════════════════════════════
70
+
71
+ def _resolve_memory_path() -> str:
72
+ env_path = os.getenv("MEMORY_PATH")
73
+ if env_path:
74
+ print(f"[Memory] Using env-specified path: {env_path}")
75
+ return env_path
76
+
77
+ tmp_path = "/tmp/memory.json"
78
+ try:
79
+ with open(tmp_path, "a", encoding="utf-8") as f:
80
+ pass
81
+ print(f"[Memory] Using /tmp path: {tmp_path}")
82
+ return tmp_path
83
+ except IOError:
84
+ pass
85
+
86
+ base_dir = os.path.dirname(os.path.abspath(__file__))
87
+ local_path = os.path.join(base_dir, "memory.json")
88
+ print(f"[Memory] Using local path: {local_path}")
89
+ return local_path
90
+
91
+
92
+ MEMORY_FILE = _resolve_memory_path()
93
+ MAX_MEMORY_ENTRIES = 15
94
+
95
+ if not os.path.exists(MEMORY_FILE):
96
+ try:
97
+ with open(MEMORY_FILE, "w", encoding="utf-8") as f:
98
+ json.dump([], f)
99
+ print(f"[Memory] Created empty memory file at: {MEMORY_FILE}")
100
+ except IOError as e:
101
+ print(f"[Memory] WARNING: Could not pre-create memory file: {e}")
102
+
103
+
104
+ # ═════════════════════════════════════════════════════════════════════════════
105
+ # MEMORY HELPERS
106
+ # ═════════════════════════════════════════════════════════════════════════════
107
+
108
+ def load_memory() -> list:
109
+ if not os.path.exists(MEMORY_FILE):
110
+ return []
111
+ try:
112
+ with open(MEMORY_FILE, "r", encoding="utf-8") as f:
113
+ raw = f.read().strip()
114
+ if not raw:
115
+ return []
116
+ data = json.loads(raw)
117
+ entries = data if isinstance(data, list) else []
118
+ print(f"[Memory] Loaded {len(entries)} entries from {MEMORY_FILE}")
119
+ return entries
120
+ except json.JSONDecodeError as exc:
121
+ print(f"[Memory] JSON corrupt β€” resetting. Reason: {exc}")
122
+ return []
123
+ except IOError as exc:
124
+ print(f"[Memory] Cannot read {MEMORY_FILE}: {exc}")
125
+ return []
126
+
127
+
128
+ def save_memory(entry: dict) -> bool:
129
+ print(f"[Memory] Saving entry for company: {entry.get('company', 'unknown')}")
130
+
131
+ if not isinstance(entry, dict) or not entry:
132
+ print("[Memory] ERROR: entry is empty or not a dict β€” skipping.")
133
+ return False
134
+
135
+ try:
136
+ parent = os.path.dirname(MEMORY_FILE)
137
+ if parent and not os.path.exists(parent):
138
+ os.makedirs(parent, exist_ok=True)
139
+
140
+ memory = load_memory()
141
+ memory.append(entry)
142
+
143
+ if len(memory) > MAX_MEMORY_ENTRIES:
144
+ memory = memory[-MAX_MEMORY_ENTRIES:]
145
+
146
+ tmp_file = MEMORY_FILE + ".tmp"
147
+ with open(tmp_file, "w", encoding="utf-8") as f:
148
+ try:
149
+ fcntl.flock(f, fcntl.LOCK_EX)
150
+ except Exception:
151
+ pass
152
+
153
+ json.dump(memory, f, indent=2, ensure_ascii=False)
154
+ f.flush()
155
+ os.fsync(f.fileno())
156
+
157
+ try:
158
+ fcntl.flock(f, fcntl.LOCK_UN)
159
+ except Exception:
160
+ pass
161
+
162
+ os.replace(tmp_file, MEMORY_FILE)
163
+
164
+ verify = load_memory()
165
+ if len(verify) > 0:
166
+ print(f"[Memory] βœ… Saved. Total entries now: {len(verify)}")
167
+ return True
168
+ else:
169
+ print("[Memory] ❌ Write seemed OK but file is empty on verify!")
170
+ return False
171
+
172
+ except PermissionError as exc:
173
+ print(f"[Memory] ❌ PERMISSION DENIED: {MEMORY_FILE} β€” {exc}")
174
+ return False
175
+ except IOError as exc:
176
+ print(f"[Memory] ❌ IOError writing to {MEMORY_FILE}: {exc}")
177
+ return False
178
+ except Exception as exc:
179
+ print(f"[Memory] ❌ Unexpected: {type(exc).__name__}: {exc}")
180
+ return False
181
+
182
+
183
+ def get_recent_memory(n: int = 3) -> list:
184
+ entries = load_memory()
185
+ recent = entries[-n:]
186
+ print(f"[Memory] Returning {len(recent)} recent entries.")
187
+ return recent
188
+
189
+
190
+ def format_memory_context(entries: list) -> str:
191
+ if not entries:
192
+ return "No prior meeting history available."
193
+ parts = []
194
+ for i, e in enumerate(entries, 1):
195
+ ts = e.get("timestamp", "unknown time")
196
+ company = e.get("company", "N/A")
197
+ objective = e.get("objective", "N/A")
198
+ summary = e.get("summary", "N/A")
199
+ parts.append(
200
+ f"[Past Meeting {i} | {ts}]\n"
201
+ f" Company : {company}\n"
202
+ f" Objective : {objective}\n"
203
+ f" Takeaway : {summary}"
204
+ )
205
+ return "\n\n".join(parts)
206
+
207
+
208
+ # ═════════════════════════════════════════════════════════════════════════════
209
+ # DECISION PARSER
210
+ # ═════════════════════════════════════════════════════════════════════════════
211
+
212
+ def parse_decision(text: str) -> dict:
213
+ upper = text.upper()
214
+
215
+ if "SEARCH: ALWAYS" in upper:
216
+ search_mode = "ALWAYS"
217
+ elif "SEARCH: LIGHT" in upper:
218
+ search_mode = "LIGHT"
219
+ else:
220
+ search_mode = "MINIMAL"
221
+ use_search = search_mode != "MINIMAL"
222
+
223
+ use_memory = "MEMORY: NO" not in upper
224
+
225
+ if "PRIORITY: INDUSTRY" in upper:
226
+ priority = "Industry"
227
+ elif "PRIORITY: STRATEGY" in upper:
228
+ priority = "Strategy"
229
+ else:
230
+ priority = "Context"
231
+
232
+ if "DEPTH: DEEP" in upper:
233
+ depth = "DEEP"
234
+ elif "DEPTH: SHORT" in upper:
235
+ depth = "SHORT"
236
+ else:
237
+ depth = "NORMAL"
238
+
239
+ return {
240
+ "use_search": use_search,
241
+ "search_mode": search_mode,
242
+ "use_memory": use_memory,
243
+ "priority": priority,
244
+ "depth": depth,
245
+ }
246
+
247
+
248
+ # ═════════════════════════════════════════════════════════════════════════════
249
+ # SMART MODEL ROUTER β€” rate-limit detection + fallback
250
+ # ═════════════════════════════════════════════════════════════════════════════
251
+
252
+ _RATE_LIMIT_KEYWORDS = [
253
+ "429", "rate_limit", "Rate limit", "quota", "Quota",
254
+ "RESOURCE_EXHAUSTED", "503", "UNAVAILABLE", "overloaded",
255
+ "too many requests", "exceeded",
256
+ ]
257
+
258
+ def _is_rate_error(msg: str) -> bool:
259
+ return any(k.lower() in msg.lower() for k in _RATE_LIMIT_KEYWORDS)
260
+
261
+
262
+ def kickoff_with_retry(crew, retries: int = 3, base_wait: int = 12):
263
+ for attempt in range(retries):
264
+ try:
265
+ return crew.kickoff()
266
+ except Exception as e:
267
+ msg = str(e)
268
+ if _is_rate_error(msg) and attempt < retries - 1:
269
+ wait = base_wait * (2 ** attempt)
270
+ print(f"[Retry] Rate limit hit β€” waiting {wait}s "
271
+ f"(attempt {attempt + 1}/{retries})")
272
+ time.sleep(wait)
273
+ continue
274
+ raise
275
+
276
+
277
+ def kickoff_with_model_fallback(crew_builder_fn, high_quality: bool = False):
278
+ primary = pro_llm if high_quality else lite_llm
279
+ secondary = lite_llm if high_quality else None
280
+
281
+ crew = crew_builder_fn(primary)
282
+ try:
283
+ return kickoff_with_retry(crew)
284
+ except Exception as e:
285
+ if high_quality and secondary and _is_rate_error(str(e)):
286
+ print("[ModelRouter] gemini-2.5-flash rate-limited β†’ "
287
+ "falling back to gemini-2.0-flash-lite")
288
+ time.sleep(8)
289
+ crew = crew_builder_fn(secondary)
290
+ return kickoff_with_retry(crew)
291
+ raise
292
+
293
+
294
+ # ═════════════════════════════════════════════════════════════════════════════
295
+ # FLASK ROUTES
296
+ # ═════════════════════════════════════════════════════════════════════════════
297
+
298
+ @app.route("/")
299
+ def index():
300
+ return send_from_directory(".", "index.html")
301
+
302
+
303
+ @app.route("/health")
304
+ def health():
305
+ return jsonify({
306
+ "status": "ok",
307
+ "models": {
308
+ "lite": "gemini/gemini-2.5-flash-lite",
309
+ "pro": "gemini/gemini-2.5-flash",
310
+ "search": "gemini/gemini-2.0-flash-lite",
311
+ },
312
+ "timestamp": datetime.utcnow().isoformat(),
313
+ }), 200
314
+
315
+
316
+ @app.route("/debug-memory")
317
+ def debug_memory():
318
+ test_entry = {
319
+ "timestamp": datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC"),
320
+ "company": "DEBUG_TEST",
321
+ "objective": "Verify memory write works",
322
+ "summary": "Test entry to confirm save_memory() is functional.",
323
+ }
324
+ saved = save_memory(test_entry)
325
+ loaded = load_memory()
326
+ return jsonify({
327
+ "memory_file_path": MEMORY_FILE,
328
+ "file_exists": os.path.exists(MEMORY_FILE),
329
+ "is_writable": os.access(os.path.dirname(MEMORY_FILE) or ".", os.W_OK),
330
+ "save_returned": saved,
331
+ "total_entries": len(loaded),
332
+ "entries": loaded,
333
+ })
334
+
335
+
336
+ @app.route("/run-agent", methods=["POST"])
337
+ def run_agent():
338
+ data = request.get_json(force=True)
339
+
340
+ required_fields = [
341
+ "company_name", "meeting_objective",
342
+ "attendees", "meeting_duration", "focus_areas",
343
+ ]
344
+ missing = [f for f in required_fields if not data.get(f)]
345
+ if missing:
346
+ return jsonify({"error": f"Missing required fields: {', '.join(missing)}"}), 400
347
+
348
+ company_name = data["company_name"]
349
+ meeting_objective = data["meeting_objective"]
350
+ attendees = data["attendees"]
351
+ meeting_duration = int(data["meeting_duration"])
352
+ focus_areas = data["focus_areas"]
353
+
354
+ start_time = time.time()
355
+
356
+ try:
357
+ # ══════════════════════════════════════════════════════════════════════
358
+ # PHASE 0 β€” Load Memory
359
+ # ══════════════════════════════════════════════════════════════════════
360
+ recent_memory = get_recent_memory(3)
361
+ memory_context = format_memory_context(recent_memory)
362
+ has_memory = len(recent_memory) > 0
363
+
364
+ # ══════════════════════════════════════════════════════════════════════
365
+ # PHASE 1 β€” Decision Agent (lite_llm)
366
+ # ══════════════════════════════════════════════════════════════════════
367
+ def build_decision_crew(llm):
368
+ decision_agent = Agent(
369
+ role="Meeting Prep Orchestrator",
370
+ goal=(
371
+ "Decide how the downstream agents should use web search, "
372
+ "memory, and which analysis area to prioritize for this meeting."
373
+ ),
374
+ backstory=(
375
+ "You coordinate a pipeline of expert agents. You never guess. "
376
+ "You output only a strict decision block that configures search, "
377
+ "memory, and analysis depth."
378
+ ),
379
+ verbose=False,
380
+ allow_delegation=False,
381
+ llm=llm,
382
+ )
383
+
384
+ decision_task = Task(
385
+ description=f"""
386
+ You are orchestrating a multi-agent system that prepares executive meeting briefs.
387
+ MEETING REQUEST:
388
+ - Company : {company_name}
389
+ - Objective : {meeting_objective}
390
+ - Attendees : {attendees}
391
+ - Duration : {meeting_duration} minutes
392
+ - Focus areas : {focus_areas}
393
+ PAST MEETING MEMORY:
394
+ {memory_context}
395
+ You must output EXACTLY these 5 lines (no extra text):
396
+ SEARCH: ALWAYS or LIGHT or MINIMAL
397
+ MEMORY: YES or NO
398
+ PRIORITY: Context or Industry or Strategy
399
+ DEPTH: SHORT or NORMAL or DEEP
400
+ REASONING: one concise sentence explaining all decisions
401
+ DECISION RULES:
402
+ - SEARCH:
403
+ - ALWAYS β†’ new or complex company/industry; need 4–5 web searches
404
+ - LIGHT β†’ known company; 2–3 web searches are enough
405
+ - MINIMAL β†’ mostly internal topic; 1 search just to validate facts
406
+ - MEMORY:
407
+ - YES if past memory mentions this company or very similar ones
408
+ - NO if memory is empty or clearly unrelated
409
+ - PRIORITY:
410
+ - Context β†’ understanding the company is the biggest gap
411
+ - Industry β†’ market / competition / trends are most important
412
+ - Strategy β†’ agenda and talking points need most work
413
+ - DEPTH:
414
+ - SHORT β†’ meeting_duration ≀ 30 minutes
415
+ - NORMAL β†’ 31–60 minutes
416
+ - DEEP β†’ >60 minutes; brief should be very detailed
417
+ """,
418
+ agent=decision_agent,
419
+ expected_output="Exactly 5 lines: SEARCH, MEMORY, PRIORITY, DEPTH, REASONING.",
420
+ )
421
+
422
+ return Crew(
423
+ agents=[decision_agent],
424
+ tasks=[decision_task],
425
+ verbose=False,
426
+ max_rpm=9,
427
+ max_execution_time=45,
428
+ process=Process.sequential,
429
+ )
430
+
431
+ decision_result = kickoff_with_model_fallback(build_decision_crew, high_quality=False)
432
+ decision_text = str(decision_result).strip()
433
+ decision_flags = parse_decision(decision_text)
434
+
435
+ use_search = decision_flags["use_search"]
436
+ search_mode = decision_flags["search_mode"]
437
+ use_memory = decision_flags["use_memory"] and has_memory
438
+ priority = decision_flags["priority"]
439
+ depth = decision_flags["depth"]
440
+
441
+ # ── Memory injection ──────────────────────────────────────────────────
442
+ memory_injection = (
443
+ f"""
444
+ PRIOR MEETING CONTEXT (MANDATORY TO CONSIDER):
445
+ {memory_context}
446
+ You MUST:
447
+ - Reuse relevant insights from past meetings where helpful.
448
+ - Maintain continuity in recommendations.
449
+ - Avoid contradicting past decisions unless clearly justified.
450
+ """
451
+ if use_memory else ""
452
+ )
453
+
454
+ # ── Tool assignments per agent ────────────────────────────────────────
455
+ context_tools = [search_tool] if use_search else []
456
+ industry_tools = [search_tool] if (use_search and priority == "Industry") else []
457
+
458
+ # ── Search instructions based on mode ────────────────────────────────
459
+ if search_mode == "ALWAYS":
460
+ context_search_instruction = (
461
+ "MANDATORY: Use the search tool 3–5 times for company profile, "
462
+ "recent news, products, and competitors."
463
+ )
464
+ industry_search_instruction = (
465
+ "MANDATORY: Use the search tool 2–3 times for industry trends, "
466
+ "market size, and key competitors."
467
+ )
468
+ elif search_mode == "LIGHT":
469
+ context_search_instruction = (
470
+ "MANDATORY: Use the search tool at least 2 times for company "
471
+ "overview and latest news."
472
+ )
473
+ industry_search_instruction = (
474
+ "OPTIONAL: Use the search tool at most 1–2 times if needed."
475
+ )
476
+ else: # MINIMAL
477
+ context_search_instruction = (
478
+ "MANDATORY: Use the search tool exactly once to validate basic facts."
479
+ )
480
+ industry_search_instruction = (
481
+ "Do NOT perform additional web searches; rely on provided context."
482
+ )
483
+
484
+ # ══════════════════════════════════════════════════════════════════════
485
+ # PHASE 2 β€” Main 4-Agent Crew
486
+ # ══════════════════════════════════════════════════════════════════════
487
+
488
+ def build_main_crew(context_llm, industry_llm, strategy_llm, brief_llm):
489
+
490
+ context_analyzer = Agent(
491
+ role="Meeting Context Specialist",
492
+ goal="Produce a concise, factual company + meeting context summary.",
493
+ backstory=(
494
+ "You quickly understand complex business contexts and identify "
495
+ "only the most critical, verifiable information. You prefer "
496
+ "specific numbers, dates, and named entities over vague statements."
497
+ ),
498
+ verbose=False,
499
+ allow_delegation=False,
500
+ llm=context_llm,
501
+ tools=context_tools,
502
+ )
503
+
504
+ industry_insights_generator = Agent(
505
+ role="Industry Expert",
506
+ goal="Provide a short but insightful industry overview and key trends.",
507
+ backstory=(
508
+ "You are a seasoned industry analyst who spots important trends, "
509
+ "competitors, opportunities, and risks. You ground your analysis "
510
+ "in concrete facts and examples."
511
+ ),
512
+ verbose=False,
513
+ allow_delegation=False,
514
+ llm=industry_llm,
515
+ tools=industry_tools,
516
+ )
517
+
518
+ strategy_formulator = Agent(
519
+ role="Meeting Strategist",
520
+ goal="Design a tight, outcome-focused meeting strategy and agenda.",
521
+ backstory=(
522
+ "You create practical, time-boxed agendas that align with "
523
+ "strategic goals and stakeholder interests. Every recommendation "
524
+ "is specific, actionable, and time-bound."
525
+ ),
526
+ verbose=False,
527
+ allow_delegation=False,
528
+ llm=strategy_llm,
529
+ )
530
+
531
+ executive_briefing_creator = Agent(
532
+ role="Communication Specialist",
533
+ goal="Synthesize everything into a clear, actionable executive brief.",
534
+ backstory=(
535
+ "You distill complex analysis into crisp, high-impact talking points, "
536
+ "Q&A prep, and recommendations for C-level executives."
537
+ ),
538
+ verbose=False,
539
+ allow_delegation=False,
540
+ llm=brief_llm,
541
+ )
542
+
543
+ context_analysis_task = Task(
544
+ description=f"""
545
+ You are preparing for a meeting with {company_name}.
546
+ Search instruction: {context_search_instruction}
547
+ You MUST follow this search instruction exactly if the search tool is available.
548
+ Produce a context summary that covers:
549
+ - Company snapshot: what they do, scale, geography.
550
+ - 1–3 recent notable news items or strategic moves.
551
+ - Key products / services relevant to this meeting.
552
+ - 3–5 major direct competitors.
553
+ Meeting details:
554
+ - Objective : {meeting_objective}
555
+ - Attendees : {attendees}
556
+ - Duration : {meeting_duration} minutes
557
+ - Focus areas: {focus_areas}
558
+ { "PRIORITY FLAG: Context analysis is the highest priorityβ€”go deeper here." if priority == "Context" else "" }
559
+ {memory_injection}
560
+ Requirements:
561
+ - Include specific numbers (revenue, employees, etc.) where possible.
562
+ - Include dates for major events or news.
563
+ - Avoid generic phrases like "leverage synergies" or "move the needle".
564
+ - Target length: 400–700 words.
565
+ Output style: markdown, with clear headings and bullet points.
566
+ """,
567
+ agent=context_analyzer,
568
+ expected_output="A concise markdown summary of company + meeting context.",
569
+ )
570
+
571
+ industry_analysis_task = Task(
572
+ description=f"""
573
+ Based on the previous context analysis for {company_name} and the
574
+ meeting objective: "{meeting_objective}", provide an industry-level view.
575
+ Search instruction: {industry_search_instruction}
576
+ Focus on:
577
+ - 3–5 key industry or market trends relevant to this meeting.
578
+ - Competitive landscape and where {company_name} roughly fits.
579
+ - 3–5 main opportunities {company_name} could pursue.
580
+ - 3–5 main risks or threats they should watch.
581
+ { "PRIORITY FLAG: Industry analysis is the highest priorityβ€”go deeper here." if priority == "Industry" else "" }
582
+ {memory_injection}
583
+ Requirements:
584
+ - Use examples of competitors and adjacent players.
585
+ - Include any relevant regulations or technology trends.
586
+ - Target length: 400–700 words.
587
+ Output: markdown with clear headings and bullet points.
588
+ """,
589
+ agent=industry_insights_generator,
590
+ expected_output="A short, insightful markdown industry analysis.",
591
+ )
592
+
593
+ strategy_development_task = Task(
594
+ description=f"""
595
+ Using the prior analyses (context + industry), design a concrete strategy
596
+ for the {meeting_duration}-minute meeting with {company_name}.
597
+ Do NOT perform web searches.
598
+ Produce:
599
+ 1. A time-boxed agenda (section name + minutes) that sums to {meeting_duration} minutes.
600
+ 2. 3–7 key talking points the host should definitely cover.
601
+ 3. For each focus area in: "{focus_areas}", propose 1–3 concrete strategies.
602
+ { "PRIORITY FLAG: Strategy development is the highest priorityβ€”be especially detailed and actionable." if priority == "Strategy" else "" }
603
+ {memory_injection}
604
+ Requirements:
605
+ - Every agenda item must have a clear outcome or purpose.
606
+ - Every recommendation must specify WHO does WHAT by WHEN.
607
+ - Avoid vague language such as "discuss opportunities" or "align on strategy".
608
+ - Target length: 600–800 words.
609
+ Output: markdown, bullet-point heavy.
610
+ """,
611
+ agent=strategy_formulator,
612
+ expected_output=(
613
+ "A succinct markdown meeting strategy with time-boxed agenda and talking points."
614
+ ),
615
+ )
616
+
617
+ executive_brief_task = Task(
618
+ description=f"""
619
+ Synthesize EVERYTHING into a single executive brief for the meeting
620
+ with {company_name}.
621
+ You will receive prior analyses (context, industry, strategy). Use them all.
622
+ IMPORTANT: Output ONLY the final brief in markdown.
623
+ No internal reasoning, planning text, or preamble.
624
+ Required structure:
625
+ # Executive Summary
626
+ - 3–6 bullet points capturing the meeting objective and context.
627
+ ## Company & Industry Snapshot
628
+ - Short bullets on who {company_name} is and key market dynamics.
629
+ ## Meeting Goals & Success Criteria
630
+ - 3–5 clearly stated, measurable goals.
631
+ - How the host will know the meeting succeeded.
632
+ ## Recommended Agenda & Key Talking Points
633
+ - Tight recap of the time-boxed agenda (must total {meeting_duration} minutes).
634
+ - Bullet list of the most important talking points, tied to data or examples.
635
+ ## Anticipated Questions & Prepared Answers
636
+ - 5–10 likely questions from attendees based on their roles.
637
+ - 1–3 sentence answer for each, grounded in prior analysis.
638
+ ## Strategic Recommendations & Next Steps
639
+ - 3–5 actionable post-meeting recommendations.
640
+ - Suggested next steps and rough timelines.
641
+ Requirements:
642
+ - Markdown headings + bullets.
643
+ - Target length: 900–1300 words.
644
+ - Use specific numbers, dates, and names where possible.
645
+ - Maintain a professional, concise tone suitable for C-level executives.
646
+ """,
647
+ agent=executive_briefing_creator,
648
+ expected_output=(
649
+ "A complete markdown executive brief ready to share before the meeting."
650
+ ),
651
+ )
652
+
653
+ return Crew(
654
+ agents=[
655
+ context_analyzer,
656
+ industry_insights_generator,
657
+ strategy_formulator,
658
+ executive_briefing_creator,
659
+ ],
660
+ tasks=[
661
+ context_analysis_task,
662
+ industry_analysis_task,
663
+ strategy_development_task,
664
+ executive_brief_task,
665
+ ],
666
+ verbose=False,
667
+ max_rpm=4,
668
+ max_execution_time=300,
669
+ process=Process.sequential,
670
+ )
671
+
672
+ # ── Run main crew ─────────────────────────────────────────────────────
673
+ try:
674
+ main_crew = build_main_crew(
675
+ context_llm = lite_llm,
676
+ industry_llm = lite_llm,
677
+ strategy_llm = pro_llm,
678
+ brief_llm = pro_llm,
679
+ )
680
+ main_result = kickoff_with_retry(main_crew)
681
+
682
+ except Exception as e:
683
+ if _is_rate_error(str(e)):
684
+ print("[ModelRouter] gemini-2.5-flash quota hit in main crew β†’ "
685
+ "full gemini-2.0-flash-lite fallback")
686
+ time.sleep(15)
687
+ main_crew = build_main_crew(
688
+ context_llm = lite_llm,
689
+ industry_llm = lite_llm,
690
+ strategy_llm = lite_llm,
691
+ brief_llm = lite_llm,
692
+ )
693
+ main_result = kickoff_with_retry(main_crew)
694
+ else:
695
+ raise
696
+
697
+ raw_brief = str(main_result).strip()
698
+
699
+ # ══════════════════════════════════════════════════════════════════════
700
+ # PHASE 3 β€” Reflection Agent (pro_llm β†’ lite fallback)
701
+ # ══════════════════════════════════════════════════════════════════════
702
+ def build_reflection_crew(llm):
703
+ reflection_agent = Agent(
704
+ role="Executive Communications Editor",
705
+ goal=(
706
+ "Polish the executive brief so it is maximally clear, "
707
+ "detailed, and actionable β€” ready to send to a C-suite audience."
708
+ ),
709
+ backstory=(
710
+ "You are a senior communications editor for high-stakes documents. "
711
+ "You remove redundancy, sharpen vague language, ensure every "
712
+ "recommendation is specific, and guarantee logical flow."
713
+ ),
714
+ verbose=False,
715
+ allow_delegation=False,
716
+ llm=llm,
717
+ )
718
+
719
+ reflection_task = Task(
720
+ description=f"""
721
+ Review and improve the following executive meeting brief.
722
+ Editing checklist β€” apply EVERY item:
723
+ 1. Remove any duplicate or repeated information across sections.
724
+ 2. Sharpen vague language into specific, concrete statements.
725
+ 3. Make every recommendation actionable (who does what, by when).
726
+ 4. Fix any factual or logical inconsistencies between sections.
727
+ 5. Ensure the brief flows logically: Summary β†’ Context β†’ Goals β†’
728
+ Agenda β†’ Q&A β†’ Next Steps.
729
+ 6. Cut filler words and padding, but keep important detail.
730
+ 7. Keep all original markdown section headings intact.
731
+ 8. Do NOT add new sections β€” only improve existing content.
732
+ 9. Target length: 1100–1300 words (expand or compress as needed).
733
+ ORIGINAL BRIEF TO IMPROVE:
734
+ ---
735
+ {raw_brief}
736
+ ---
737
+ OUTPUT: Return ONLY the improved markdown brief.
738
+ No commentary, no preamble, no "Here is the improved version:" header.
739
+ """,
740
+ agent=reflection_agent,
741
+ expected_output="An improved, polished executive brief in clean markdown format.",
742
+ )
743
+
744
+ return Crew(
745
+ agents=[reflection_agent],
746
+ tasks=[reflection_task],
747
+ verbose=False,
748
+ max_rpm=4,
749
+ max_execution_time=120,
750
+ process=Process.sequential,
751
+ )
752
+
753
+ reflection_result = kickoff_with_model_fallback(build_reflection_crew, high_quality=True)
754
+ final_brief = str(reflection_result).strip()
755
+
756
+ # ══════════════════════════════════════════════════════════════════════
757
+ # PHASE 4 β€” Persist to Memory
758
+ # ══════════════════════════════════════════════════════════════════════
759
+ try:
760
+ summary_preview = (
761
+ final_brief[:350]
762
+ .replace("\n", " ")
763
+ .replace("#", "")
764
+ .strip()
765
+ )
766
+
767
+ saved = save_memory({
768
+ "timestamp": datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC"),
769
+ "company": company_name,
770
+ "objective": meeting_objective,
771
+ "summary": summary_preview,
772
+ })
773
+
774
+ if not saved:
775
+ print("[Memory] ⚠️ Entry was NOT saved β€” check logs above.")
776
+
777
+ except Exception as mem_exc:
778
+ print(f"[Memory] ❌ Phase 4 exception: {type(mem_exc).__name__}: {mem_exc}")
779
+
780
+ elapsed = round(time.time() - start_time, 2)
781
+
782
+ return jsonify({
783
+ "result": final_brief,
784
+ "decision": decision_text,
785
+ "memory_used": use_memory,
786
+ "flags": decision_flags,
787
+ "models": {
788
+ "lite": "gemini/gemini-2.0-flash-lite",
789
+ "pro": "gemini/gemini-2.5-flash",
790
+ "search": "gemini/gemini-2.0-flash-lite",
791
+ },
792
+ "elapsed_sec": elapsed,
793
+ }), 200
794
+
795
+ except Exception as exc:
796
+ msg = str(exc)
797
+
798
+ if _is_rate_error(msg):
799
+ return jsonify({
800
+ "error": (
801
+ "⚠️ Gemini API rate limit reached. "
802
+ "Please wait 30–60 seconds and try again."
803
+ )
804
+ }), 429
805
+
806
+ if "timed out" in msg.lower() or "timeout" in msg.lower():
807
+ return jsonify({
808
+ "error": (
809
+ "⏱️ The agent pipeline took too long to respond. "
810
+ "Try again β€” it usually succeeds on a second attempt."
811
+ )
812
+ }), 503
813
+
814
+ return jsonify({"error": f"Agent error: {msg}"}), 500
815
+
816
+
817
+ # ═════════════════════════════════════════════════════════════════════════════
818
+ # STANDALONE SEARCH AGENT β€” now with retry + elapsed time
819
+ # ═════════════════════════════════════════════════════════════════════════════
820
+
821
+ @app.route("/search-links", methods=["POST"])
822
+ def search_links():
823
+ """
824
+ Standalone link-search endpoint.
825
+ Expects JSON: { "query": "..." }
826
+ Returns: { query, links, model, elapsed_sec }
827
+ """
828
+ data = request.get_json(force=True)
829
+ query = data.get("query", "").strip()
830
+ if not query:
831
+ return jsonify({"error": "Missing or empty 'query' field."}), 400
832
+
833
+ start_time = time.time()
834
+
835
+ try:
836
+ search_agent = Agent(
837
+ role="Link Discovery Specialist",
838
+ goal=f"Find the most relevant and high-quality links for: {query}",
839
+ backstory=(
840
+ "You are an expert at navigating the web. You provide only the most "
841
+ "authoritative and useful links, avoiding spam or low-quality sources. "
842
+ "For each result you return the page title, a one-sentence description, "
843
+ "and the full URL."
844
+ ),
845
+ tools=[search_tool],
846
+ llm=search_llm, # uses lite_llm β€” preserves pro quota
847
+ verbose=False,
848
+ allow_delegation=False,
849
+ )
850
+
851
+ search_task = Task(
852
+ description=(
853
+ f"Search the web and find the top 5–8 most relevant, authoritative "
854
+ f"links related to: '{query}'.\n\n"
855
+ f"For each result provide:\n"
856
+ f"- **Title**: the page title\n"
857
+ f"- **URL**: the full link\n"
858
+ f"- **Summary**: one sentence describing what the page covers\n\n"
859
+ f"Format the output as a clean markdown list."
860
+ ),
861
+ expected_output=(
862
+ "A markdown list of 5–8 links, each with title, URL, and one-sentence summary."
863
+ ),
864
+ agent=search_agent,
865
+ )
866
+
867
+ crew = Crew(
868
+ agents=[search_agent],
869
+ tasks=[search_task],
870
+ verbose=False,
871
+ max_rpm=9,
872
+ max_execution_time=60,
873
+ process=Process.sequential,
874
+ )
875
+
876
+ result = kickoff_with_retry(crew, retries=2, base_wait=8)
877
+ elapsed = round(time.time() - start_time, 2)
878
+
879
+ return jsonify({
880
+ "query": query,
881
+ "links": str(result).strip(),
882
+ "model": "gemini/gemini-2.0-flash-lite",
883
+ "elapsed_sec": elapsed,
884
+ }), 200
885
+
886
+ except Exception as e:
887
+ msg = str(e)
888
+ elapsed = round(time.time() - start_time, 2)
889
+
890
+ if _is_rate_error(msg):
891
+ return jsonify({
892
+ "error": "⚠️ Rate limit reached. Please wait 30 seconds and try again.",
893
+ "elapsed_sec": elapsed,
894
+ }), 429
895
+
896
+ return jsonify({
897
+ "error": f"Search agent error: {msg}",
898
+ "elapsed_sec": elapsed,
899
+ }), 500
900
+
901
+
902
+ # ── Entry Point ───────────────────────────────────────────────────────────────
903
+ if __name__ == "__main__":
904
+ app.run(debug=True, use_reloader=False, host="0.0.0.0", port=7860)
gitignore ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ── Python ────────────────────────────────────────────────────────────────────
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.pyo
6
+ *.pyd
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ .eggs/
12
+ *.so
13
+
14
+ # ── Virtual environments ──────────────────────────────────────────────────────
15
+ .venv/
16
+ venv/
17
+ env/
18
+ ENV/
19
+
20
+ # ── Local secrets β€” NEVER commit these ───────────────────────────────────────
21
+ .env
22
+ .env.*
23
+ secrets.json
24
+
25
+
26
+
27
+ # ── IDE / editor ─────────────────────────────────────────────────────────────
28
+ .vscode/
29
+ .idea/
30
+ *.swp
31
+ *.swo
32
+ .DS_Store
33
+ Thumbs.db
34
+
35
+ # ── Logs ─────────────────────────────────────────────────────────────────────
36
+ *.log
37
+ logs/
38
+
39
+ # ── pytest / coverage ────────────────────────────────────────────────────────
40
+ .pytest_cache/
41
+ .coverage
42
+ htmlcov/
index.html ADDED
@@ -0,0 +1,1089 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>MeetIQ β€” Business Meeting Preparation Agent</title>
7
+
8
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
+ <link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet" />
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js"></script>
12
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
13
+
14
+ <style>
15
+ /* ── Reset & tokens ─────────────────────────────────────────────────── */
16
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
17
+ :root {
18
+ --bg: #05091a;
19
+ --surface: #0b1228;
20
+ --glass: rgba(255,255,255,0.04);
21
+ --glass-b: rgba(255,255,255,0.08);
22
+ --glass-hover: rgba(255,255,255,0.07);
23
+ --blue: #3b82f6;
24
+ --blue-glow: #60a5fa;
25
+ --blue-dim: #1d4ed8;
26
+ --cyan: #06b6d4;
27
+ --violet: #7c3aed;
28
+ --violet-light: #a78bfa;
29
+ --green: #10b981;
30
+ --yellow: #f59e0b;
31
+ --red: #f43f5e;
32
+ --txt: #f0f4ff;
33
+ --txt-m: #94a3b8;
34
+ --txt-f: #475569;
35
+ --r: 16px;
36
+ --r-sm: 10px;
37
+ }
38
+ html { scroll-behavior: smooth; }
39
+ body {
40
+ font-family: 'DM Sans', sans-serif;
41
+ background: var(--bg);
42
+ color: var(--txt);
43
+ min-height: 100vh;
44
+ overflow-x: hidden;
45
+ }
46
+
47
+ /* ── Animated background ────────────────────────────────────────────── */
48
+ .bg-canvas {
49
+ position: fixed; inset: 0; z-index: 0;
50
+ overflow: hidden; pointer-events: none;
51
+ }
52
+ .bg-canvas::before {
53
+ content: ''; position: absolute;
54
+ width: 900px; height: 900px; border-radius: 50%;
55
+ background: radial-gradient(circle, rgba(59,130,246,.13) 0%, transparent 70%);
56
+ top: -300px; left: -200px;
57
+ animation: drift1 18s ease-in-out infinite alternate;
58
+ }
59
+ .bg-canvas::after {
60
+ content: ''; position: absolute;
61
+ width: 700px; height: 700px; border-radius: 50%;
62
+ background: radial-gradient(circle, rgba(6,182,212,.08) 0%, transparent 70%);
63
+ bottom: -200px; right: -100px;
64
+ animation: drift2 22s ease-in-out infinite alternate;
65
+ }
66
+ .bg-orb3 {
67
+ position: absolute; width: 500px; height: 500px; border-radius: 50%;
68
+ background: radial-gradient(circle, rgba(124,58,237,.09) 0%, transparent 70%);
69
+ top: 50%; left: 55%; transform: translate(-50%,-50%);
70
+ animation: drift3 25s ease-in-out infinite alternate;
71
+ }
72
+ @keyframes drift1 { to { transform: translate(80px,60px) scale(1.1); } }
73
+ @keyframes drift2 { to { transform: translate(-60px,-80px) scale(1.15); } }
74
+ @keyframes drift3 { to { transform: translate(-45%,-55%) scale(.9); } }
75
+ .bg-grid {
76
+ position: absolute; inset: 0;
77
+ background-image:
78
+ linear-gradient(rgba(59,130,246,.03) 1px, transparent 1px),
79
+ linear-gradient(90deg, rgba(59,130,246,.03) 1px, transparent 1px);
80
+ background-size: 60px 60px;
81
+ }
82
+
83
+ /* ── Layout ─────────────────────────────────────────────────────────── */
84
+ .wrapper {
85
+ position: relative; z-index: 1;
86
+ max-width: 880px; margin: 0 auto;
87
+ padding: 60px 24px 100px;
88
+ }
89
+
90
+ /* ── Header ─────────────────────────────────────────────────────────── */
91
+ .header { text-align: center; margin-bottom: 52px; animation: fadeDown .7s ease both; }
92
+ .header-badge {
93
+ display: inline-flex; align-items: center; gap: 8px;
94
+ background: rgba(59,130,246,.1);
95
+ border: 1px solid rgba(59,130,246,.25);
96
+ border-radius: 999px; padding: 6px 16px;
97
+ font-size: 11px; font-weight: 500; color: var(--blue-glow);
98
+ letter-spacing: .08em; text-transform: uppercase; margin-bottom: 20px;
99
+ }
100
+ .header-badge .dot {
101
+ width: 6px; height: 6px; border-radius: 50%;
102
+ background: var(--blue-glow);
103
+ animation: pulse-dot 2s ease infinite;
104
+ }
105
+ @keyframes pulse-dot {
106
+ 0%,100% { opacity:1; transform:scale(1); }
107
+ 50% { opacity:.4; transform:scale(.7); }
108
+ }
109
+ h1 {
110
+ font-family: 'Syne', sans-serif;
111
+ font-size: clamp(2rem,5vw,3.2rem);
112
+ font-weight: 800; line-height: 1.1; letter-spacing: -.02em;
113
+ background: linear-gradient(135deg,#f0f4ff 20%,#60a5fa 60%,#06b6d4 100%);
114
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
115
+ background-clip: text; margin-bottom: 14px;
116
+ }
117
+ .header-sub {
118
+ color: var(--txt-m); font-size: 15px; font-weight: 300;
119
+ max-width: 540px; margin: 0 auto; line-height: 1.7;
120
+ }
121
+
122
+ /* ── Agent pipeline pills ────────────────────────────────────────────── */
123
+ .pipeline-pills {
124
+ display: flex; align-items: center; justify-content: center;
125
+ flex-wrap: wrap; gap: 6px; margin-top: 22px;
126
+ }
127
+ .pill {
128
+ display: inline-flex; align-items: center; gap: 5px;
129
+ background: rgba(255,255,255,.04);
130
+ border: 1px solid rgba(255,255,255,.08);
131
+ border-radius: 999px; padding: 4px 12px;
132
+ font-size: 11px; color: var(--txt-m);
133
+ }
134
+ .pill-arrow { color: var(--txt-f); font-size: 12px; }
135
+
136
+ /* ── Glass card ─────────────────────────────────────────────────────── */
137
+ .glass-card {
138
+ background: var(--glass); border: 1px solid var(--glass-b);
139
+ border-radius: var(--r); backdrop-filter: blur(20px);
140
+ -webkit-backdrop-filter: blur(20px);
141
+ padding: 36px; margin-bottom: 18px;
142
+ animation: fadeUp .6s ease both;
143
+ }
144
+ .glass-card:nth-child(2) { animation-delay: .1s; }
145
+ @keyframes fadeUp { from { opacity:0; transform:translateY(24px); } to { opacity:1; transform:translateY(0); } }
146
+ @keyframes fadeDown { from { opacity:0; transform:translateY(-16px); } to { opacity:1; transform:translateY(0); } }
147
+
148
+ .section-label {
149
+ font-family: 'Syne', sans-serif; font-size: 11px;
150
+ font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
151
+ color: var(--blue-glow); margin-bottom: 22px;
152
+ display: flex; align-items: center; gap: 10px;
153
+ }
154
+ .section-label::after {
155
+ content: ''; flex: 1; height: 1px;
156
+ background: linear-gradient(90deg, rgba(59,130,246,.3), transparent);
157
+ }
158
+
159
+ /* ── Form ───────────────────────────────────────────────────────────── */
160
+ .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
161
+ .form-grid .span-2 { grid-column: 1 / -1; }
162
+ .field { display: flex; flex-direction: column; gap: 7px; }
163
+ label { font-size: 12px; font-weight: 500; color: var(--txt-m); letter-spacing: .04em; }
164
+ input, textarea, select {
165
+ background: rgba(255,255,255,.03); border: 1px solid var(--glass-b);
166
+ border-radius: var(--r-sm); color: var(--txt);
167
+ font-family: 'DM Sans', sans-serif; font-size: 14px;
168
+ padding: 12px 16px; transition: border-color .2s, box-shadow .2s, background .2s;
169
+ outline: none; width: 100%;
170
+ }
171
+ input::placeholder, textarea::placeholder { color: var(--txt-f); }
172
+ input:focus, textarea:focus {
173
+ border-color: rgba(59,130,246,.5);
174
+ background: rgba(59,130,246,.04);
175
+ box-shadow: 0 0 0 3px rgba(59,130,246,.1);
176
+ }
177
+ textarea { resize: vertical; min-height: 88px; line-height: 1.6; }
178
+
179
+ /* duration slider */
180
+ .duration-row { display: flex; align-items: center; gap: 14px; }
181
+ .duration-row input[type="range"] {
182
+ flex: 1; -webkit-appearance: none; height: 4px;
183
+ background: linear-gradient(90deg, var(--blue) var(--pct,33%), rgba(255,255,255,.1) var(--pct,33%));
184
+ border-radius: 99px; padding: 0; border: none; box-shadow: none; cursor: pointer;
185
+ }
186
+ .duration-row input[type="range"]::-webkit-slider-thumb {
187
+ -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%;
188
+ background: white; box-shadow: 0 0 8px rgba(59,130,246,.6); transition: transform .15s;
189
+ }
190
+ .duration-row input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.2); }
191
+ .duration-badge {
192
+ min-width: 64px; text-align: center;
193
+ background: rgba(59,130,246,.12); border: 1px solid rgba(59,130,246,.25);
194
+ border-radius: 8px; padding: 8px 10px;
195
+ font-family: 'Syne', sans-serif; font-size: 14px; font-weight: 700;
196
+ color: var(--blue-glow); line-height: 1;
197
+ }
198
+
199
+ /* submit button */
200
+ .btn-submit {
201
+ width: 100%; padding: 16px 24px;
202
+ background: linear-gradient(135deg, var(--blue) 0%, var(--blue-dim) 100%);
203
+ border: none; border-radius: var(--r-sm); color: white;
204
+ font-family: 'Syne', sans-serif; font-size: 15px;
205
+ font-weight: 700; letter-spacing: .04em; cursor: pointer;
206
+ position: relative; overflow: hidden;
207
+ transition: transform .2s, box-shadow .2s; margin-top: 10px;
208
+ }
209
+ .btn-submit::before {
210
+ content: ''; position: absolute; inset: 0;
211
+ background: linear-gradient(135deg, rgba(255,255,255,.15) 0%, transparent 60%);
212
+ opacity: 0; transition: opacity .2s;
213
+ }
214
+ .btn-submit:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 8px 32px rgba(59,130,246,.4); }
215
+ .btn-submit:hover:not(:disabled)::before { opacity: 1; }
216
+ .btn-submit:active:not(:disabled) { transform: translateY(0); }
217
+ .btn-submit:disabled { opacity: .55; cursor: not-allowed; }
218
+
219
+ /* error toast */
220
+ #error-toast {
221
+ display: none; background: rgba(244,63,94,.1);
222
+ border: 1px solid rgba(244,63,94,.3); border-radius: var(--r-sm);
223
+ padding: 14px 18px; color: #fda4af; font-size: 14px;
224
+ margin-top: 16px; animation: fadeUp .3s ease both;
225
+ }
226
+ #error-toast.active { display: flex; align-items: flex-start; gap: 10px; }
227
+
228
+ /* ── Loader ─────────────────────────────────────────────────────────── */
229
+ #loader { display: none; }
230
+ #loader.active { display: block; }
231
+ .loader-card {
232
+ background: var(--glass); border: 1px solid rgba(59,130,246,.2);
233
+ border-radius: var(--r); backdrop-filter: blur(20px);
234
+ padding: 48px 36px; text-align: center; animation: fadeUp .4s ease both;
235
+ }
236
+ .orbit-wrap { position: relative; width: 100px; height: 100px; margin: 0 auto 32px; }
237
+ .orbit-ring { position: absolute; inset: 0; border-radius: 50%; border: 2px solid transparent; }
238
+ .orbit-ring-1 { border-top-color: var(--blue); border-right-color: rgba(59,130,246,.2); animation: spin 1.4s linear infinite; }
239
+ .orbit-ring-2 { inset: 12px; border-top-color: var(--cyan); border-left-color: rgba(6,182,212,.2); animation: spin 2s linear infinite reverse; }
240
+ .orbit-ring-3 { inset: 26px; border-bottom-color: rgba(255,255,255,.4); border-right-color: rgba(255,255,255,.1); animation: spin 1.8s linear infinite; }
241
+ .orbit-center {
242
+ position: absolute; inset: 38px; border-radius: 50%;
243
+ background: radial-gradient(circle, rgba(59,130,246,.5), rgba(59,130,246,.1));
244
+ animation: breathe 2s ease-in-out infinite;
245
+ }
246
+ @keyframes spin { to { transform: rotate(360deg); } }
247
+ @keyframes breathe { 0%,100% { transform:scale(1); opacity:.8; } 50% { transform:scale(1.2); opacity:1; } }
248
+
249
+ .loader-title { font-family:'Syne',sans-serif; font-size:20px; font-weight:700; margin-bottom:8px; }
250
+ .loader-sub { color: var(--txt-m); font-size:14px; margin-bottom:8px; min-height:20px; }
251
+ /* ── Live elapsed timer in loader ───────────────────────────────────── */
252
+ .loader-timer {
253
+ font-family: 'Syne', sans-serif; font-size: 12px;
254
+ color: var(--txt-f); margin-bottom: 28px;
255
+ letter-spacing: .06em;
256
+ }
257
+ .loader-timer span { color: var(--blue-glow); font-weight: 700; }
258
+
259
+ .agent-steps { display:flex; flex-direction:column; gap:8px; text-align:left; max-width:420px; margin:0 auto; }
260
+ .agent-step {
261
+ display: flex; align-items: center; gap: 12px;
262
+ padding: 10px 14px; border-radius: 10px;
263
+ background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.06);
264
+ font-size: 13px; color: var(--txt-f); transition: all .4s ease;
265
+ }
266
+ .agent-step.active { color:var(--txt); background:rgba(59,130,246,.08); border-color:rgba(59,130,246,.2); }
267
+ .agent-step.done { color:var(--green); background:rgba(16,185,129,.06); border-color:rgba(16,185,129,.15); }
268
+ .step-icon {
269
+ width:22px; height:22px; border-radius:50%;
270
+ display:flex; align-items:center; justify-content:center;
271
+ font-size:11px; flex-shrink:0;
272
+ background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.1);
273
+ }
274
+ .agent-step.active .step-icon { background:rgba(59,130,246,.2); border-color:var(--blue); animation:pulse-icon 1.5s ease infinite; }
275
+ .agent-step.done .step-icon { background:rgba(16,185,129,.15); border-color:var(--green); }
276
+ @keyframes pulse-icon { 0%,100% { box-shadow:0 0 0 0 rgba(59,130,246,.4); } 50% { box-shadow:0 0 0 6px rgba(59,130,246,0); } }
277
+
278
+ /* ── Result section ─────────────────────────────────────────────────── */
279
+ #result-section { display:none; }
280
+ #result-section.active { display:block; animation:fadeUp .6s ease both; }
281
+
282
+ .result-header {
283
+ display:flex; align-items:center; justify-content:space-between;
284
+ margin-bottom:24px; flex-wrap:wrap; gap:12px;
285
+ }
286
+ .result-title { font-family:'Syne',sans-serif; font-size:20px; font-weight:700; }
287
+ .result-title span { color:var(--blue-glow); }
288
+
289
+ /* elapsed badge in result header */
290
+ .elapsed-badge {
291
+ display: inline-flex; align-items: center; gap: 6px;
292
+ background: rgba(16,185,129,.1); border: 1px solid rgba(16,185,129,.2);
293
+ border-radius: 999px; padding: 4px 12px;
294
+ font-size: 11px; color: #34d399; font-family: 'Syne', sans-serif;
295
+ font-weight: 700; letter-spacing: .06em;
296
+ }
297
+
298
+ .result-actions { display:flex; gap:10px; flex-wrap:wrap; align-items:center; }
299
+ .btn-reset {
300
+ background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.12);
301
+ border-radius:8px; color:var(--txt-m); font-family:'DM Sans',sans-serif;
302
+ font-size:13px; padding:8px 16px; cursor:pointer; transition:all .2s;
303
+ }
304
+ .btn-reset:hover { background:rgba(255,255,255,.1); color:var(--txt); }
305
+ .btn-download {
306
+ background:linear-gradient(135deg,#10b981,#059669); border:none;
307
+ border-radius:8px; color:white; font-family:'Syne',sans-serif;
308
+ font-size:13px; font-weight:700; padding:8px 18px;
309
+ cursor:pointer; transition:transform .2s, box-shadow .2s; letter-spacing:.03em;
310
+ }
311
+ .btn-download:hover:not(:disabled) { transform:translateY(-2px); box-shadow:0 6px 20px rgba(16,185,129,.35); }
312
+ .btn-download:disabled { opacity:.55; cursor:not-allowed; }
313
+
314
+ /* ── Meta cards (Decision + Memory) ─────────────────────────────────── */
315
+ .meta-row { display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-bottom:18px; }
316
+ .meta-card {
317
+ background: var(--glass); border: 1px solid var(--glass-b);
318
+ border-radius: var(--r-sm); padding: 20px 22px;
319
+ animation: fadeUp .5s ease both;
320
+ }
321
+ .meta-card-title {
322
+ font-family:'Syne',sans-serif; font-size:10px; font-weight:700;
323
+ letter-spacing:.12em; text-transform:uppercase;
324
+ margin-bottom:14px; display:flex; align-items:center; gap:8px;
325
+ }
326
+ .meta-card-title.blue { color:var(--blue-glow); }
327
+ .meta-card-title.violet { color:var(--violet-light); }
328
+ .meta-card-title::after { content:''; flex:1; height:1px; background:currentColor; opacity:.2; }
329
+
330
+ .decision-rows { display:flex; flex-direction:column; gap:8px; }
331
+ .decision-row { display:flex; align-items:center; justify-content:space-between; font-size:13px; }
332
+ .decision-row-label { color:var(--txt-m); }
333
+ .decision-badge {
334
+ font-family:'Syne',sans-serif; font-size:11px; font-weight:700;
335
+ letter-spacing:.06em; padding:3px 10px; border-radius:999px;
336
+ }
337
+ .badge-yes { background:rgba(16,185,129,.15); color:#34d399; border:1px solid rgba(16,185,129,.3); }
338
+ .badge-no { background:rgba(100,116,139,.12); color:var(--txt-f); border:1px solid rgba(100,116,139,.2); }
339
+ .badge-purple { background:rgba(124,58,237,.15); color:var(--violet-light); border:1px solid rgba(124,58,237,.3); }
340
+ .badge-yellow { background:rgba(245,158,11,.12); color:#fcd34d; border:1px solid rgba(245,158,11,.25); }
341
+ .badge-cyan { background:rgba(6,182,212,.12); color:#67e8f9; border:1px solid rgba(6,182,212,.25); }
342
+
343
+ .reasoning-text {
344
+ margin-top:12px; font-size:12px; color:var(--txt-m);
345
+ line-height:1.6; border-top:1px solid rgba(255,255,255,.06); padding-top:10px;
346
+ font-style:italic;
347
+ }
348
+
349
+ .memory-indicator { display:flex; align-items:center; gap:12px; }
350
+ .memory-icon {
351
+ width:40px; height:40px; border-radius:10px;
352
+ display:flex; align-items:center; justify-content:center; font-size:20px;
353
+ flex-shrink:0;
354
+ }
355
+ .memory-icon.active-mem { background:rgba(16,185,129,.12); border:1px solid rgba(16,185,129,.25); }
356
+ .memory-icon.inactive-mem { background:rgba(100,116,139,.1); border:1px solid rgba(100,116,139,.2); }
357
+ .memory-status { font-family:'Syne',sans-serif; font-size:13px; font-weight:700; }
358
+ .memory-status.yes { color:var(--green); }
359
+ .memory-status.no { color:var(--txt-f); }
360
+ .memory-sub { font-size:12px; color:var(--txt-m); margin-top:3px; line-height:1.5; }
361
+
362
+ /* ── Main brief body ─────────────────────────────────────────────────── */
363
+ .result-body { color:var(--txt); line-height:1.75; font-size:15px; }
364
+ .result-body h1 {
365
+ font-family:'Syne',sans-serif; font-size:1.6rem; font-weight:800; color:#f0f4ff;
366
+ margin:28px 0 12px; padding-bottom:10px; border-bottom:1px solid rgba(255,255,255,.08);
367
+ }
368
+ .result-body h2 { font-family:'Syne',sans-serif; font-size:1.15rem; font-weight:700; color:var(--blue-glow); margin:24px 0 10px; }
369
+ .result-body h3 { font-family:'Syne',sans-serif; font-size:1rem; font-weight:600; color:var(--cyan); margin:18px 0 8px; }
370
+ .result-body p { margin-bottom:14px; }
371
+ .result-body ul, .result-body ol { padding-left:22px; margin-bottom:14px; }
372
+ .result-body li { margin-bottom:6px; }
373
+ .result-body strong { color:#e2e8f0; }
374
+ .result-body em { color:var(--txt-m); }
375
+ .result-body code { background:rgba(255,255,255,.07); border:1px solid rgba(255,255,255,.1); border-radius:5px; padding:2px 7px; font-size:.88em; color:var(--cyan); }
376
+ .result-body blockquote { border-left:3px solid var(--blue); padding:10px 16px; background:rgba(59,130,246,.06); border-radius:0 8px 8px 0; margin:16px 0; color:var(--txt-m); }
377
+ .result-body hr { border:none; border-top:1px solid rgba(255,255,255,.08); margin:24px 0; }
378
+ /* make links in brief visible and clickable */
379
+ .result-body a { color: var(--blue-glow); text-decoration: underline; word-break: break-all; }
380
+ .result-body a:hover { color: var(--cyan); }
381
+
382
+ /* ══════════════════════════════════════════════════════════════════════
383
+ SEARCH LINKS PANEL (new)
384
+ ══════════════════════════════════════════════════════════════════════ */
385
+ .search-panel {
386
+ background: var(--glass); border: 1px solid var(--glass-b);
387
+ border-radius: var(--r); backdrop-filter: blur(20px);
388
+ padding: 28px 32px; margin-bottom: 18px;
389
+ animation: fadeUp .7s ease .15s both;
390
+ }
391
+ .search-row {
392
+ display: flex; gap: 12px; align-items: flex-end;
393
+ }
394
+ .search-row .field { flex: 1; }
395
+ .btn-search {
396
+ padding: 12px 22px; white-space: nowrap;
397
+ background: linear-gradient(135deg, var(--cyan), #0891b2);
398
+ border: none; border-radius: var(--r-sm); color: #0f172a;
399
+ font-family: 'Syne', sans-serif; font-size: 13px; font-weight: 700;
400
+ cursor: pointer; transition: transform .2s, box-shadow .2s;
401
+ letter-spacing: .04em; flex-shrink: 0;
402
+ }
403
+ .btn-search:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(6,182,212,.35); }
404
+ .btn-search:disabled { opacity: .5; cursor: not-allowed; }
405
+
406
+ /* search results output */
407
+ #search-output { display: none; margin-top: 20px; }
408
+ #search-output.active { display: block; animation: fadeUp .4s ease both; }
409
+
410
+ .search-result-header {
411
+ display: flex; align-items: center; justify-content: space-between;
412
+ margin-bottom: 14px; flex-wrap: wrap; gap: 8px;
413
+ }
414
+ .search-result-title {
415
+ font-family: 'Syne', sans-serif; font-size: 12px; font-weight: 700;
416
+ letter-spacing: .1em; text-transform: uppercase; color: var(--cyan);
417
+ display: flex; align-items: center; gap: 8px;
418
+ }
419
+ .search-elapsed {
420
+ font-size: 11px; color: var(--txt-f);
421
+ font-family: 'Syne', sans-serif; letter-spacing: .06em;
422
+ }
423
+ .search-elapsed span { color: var(--cyan); font-weight: 700; }
424
+
425
+ .search-body {
426
+ background: rgba(6,182,212,.04); border: 1px solid rgba(6,182,212,.15);
427
+ border-radius: var(--r-sm); padding: 20px 22px;
428
+ font-size: 14px; line-height: 1.75; color: var(--txt);
429
+ }
430
+ /* links inside search results */
431
+ .search-body a {
432
+ color: var(--blue-glow); text-decoration: underline;
433
+ word-break: break-all;
434
+ }
435
+ .search-body a:hover { color: var(--cyan); }
436
+ .search-body h1,.search-body h2,.search-body h3 {
437
+ font-family: 'Syne', sans-serif; color: var(--blue-glow);
438
+ margin: 14px 0 8px; font-size: 1rem;
439
+ }
440
+ .search-body ul,.search-body ol { padding-left: 20px; margin-bottom: 12px; }
441
+ .search-body li { margin-bottom: 8px; }
442
+ .search-body strong { color: #e2e8f0; }
443
+
444
+ /* search error */
445
+ #search-error {
446
+ display: none; background: rgba(244,63,94,.08);
447
+ border: 1px solid rgba(244,63,94,.25); border-radius: var(--r-sm);
448
+ padding: 12px 16px; color: #fda4af; font-size: 13px;
449
+ margin-top: 14px; gap: 8px;
450
+ }
451
+ #search-error.active { display: flex; align-items: center; }
452
+
453
+ /* ── Footer ─────────────────────────────────────────────────────────── */
454
+ footer { text-align:center; padding-top:48px; color:var(--txt-f); font-size:12px; }
455
+ footer span { color:var(--txt-m); }
456
+
457
+ /* ── Responsive ─────────────────────────────────────────────────────── */
458
+ @media (max-width:640px) {
459
+ .form-grid { grid-template-columns:1fr; }
460
+ .form-grid .span-2 { grid-column:1; }
461
+ .glass-card { padding:22px 18px; }
462
+ .meta-row { grid-template-columns:1fr; }
463
+ .pipeline-pills { gap:4px; }
464
+ .search-row { flex-direction: column; }
465
+ .btn-search { width: 100%; }
466
+ }
467
+
468
+ /* ── Print / PDF ────────────────────────────────────────────────────── */
469
+ @media print {
470
+ body { background:white !important; }
471
+ .bg-canvas, .wrapper > header, #form-section, .result-header,
472
+ .meta-row, footer, #loader, .search-panel { display:none !important; }
473
+ .wrapper { padding:0 !important; }
474
+ #result-section { display:block !important; }
475
+ .glass-card { background:white !important; border:none !important; box-shadow:none !important; padding:0 !important; }
476
+ #print-area, #print-area * { visibility:visible !important; color:black !important; background:white !important; -webkit-text-fill-color:black !important; font-family:'Times New Roman',serif !important; text-shadow:none !important; }
477
+ #print-area { font-size:12pt; line-height:1.6; }
478
+ #print-area h1 { font-size:22pt; border-bottom:2px solid black; padding-bottom:8px; margin-bottom:15px; }
479
+ #print-area h2 { font-size:16pt; margin-top:20px; }
480
+ #print-area h3 { font-size:13pt; margin-top:15px; }
481
+ #print-area p, #print-area li { margin-bottom:10px; }
482
+ @page { margin:20mm; size:A4; }
483
+ }
484
+ </style>
485
+ </head>
486
+ <body>
487
+
488
+ <div class="bg-canvas">
489
+ <div class="bg-grid"></div>
490
+ <div class="bg-orb3"></div>
491
+ </div>
492
+
493
+ <div class="wrapper">
494
+
495
+ <!-- ── Header ────────────────────────────────────────────────────────── -->
496
+ <header class="header">
497
+ <div class="header-badge"><span class="dot"></span> MeetIQ Intelligence</div>
498
+ <h1>MeetIQ</h1>
499
+ <p class="header-sub">
500
+ A focused business meeting preparation agent designed to help you plan, strategize,
501
+ and execute high-impact meetings with clarity and confidence.
502
+ </p>
503
+ <div class="pipeline-pills">
504
+ <span class="pill">🎯 Objective</span>
505
+ <span class="pill-arrow">β†’</span>
506
+ <span class="pill">πŸ” Research</span>
507
+ <span class="pill-arrow">β†’</span>
508
+ <span class="pill">πŸ“Š Insights</span>
509
+ <span class="pill-arrow">β†’</span>
510
+ <span class="pill">🧠 Strategy</span>
511
+ <span class="pill-arrow">β†’</span>
512
+ <span class="pill">πŸ—‚ Agenda</span>
513
+ <span class="pill-arrow">β†’</span>
514
+ <span class="pill">βœ… Brief</span>
515
+ </div>
516
+ </header>
517
+
518
+ <!-- ── Form ──────────────────────────────────────────────────────────── -->
519
+ <div id="form-section">
520
+
521
+ <!-- Meeting input card -->
522
+ <div class="glass-card">
523
+ <div class="section-label">Meeting Details</div>
524
+ <div class="form-grid">
525
+
526
+ <div class="field">
527
+ <label for="company">Company Name</label>
528
+ <input type="text" id="company" placeholder="e.g. Acme Corporation" autocomplete="off" />
529
+ </div>
530
+
531
+ <div class="field">
532
+ <label for="objective">Meeting Objective</label>
533
+ <input type="text" id="objective" placeholder="e.g. Explore partnership opportunities" autocomplete="off" />
534
+ </div>
535
+
536
+ <div class="field span-2">
537
+ <label for="attendees">Attendees &amp; Roles <span style="color:var(--txt-f);font-weight:300">(one per line)</span></label>
538
+ <textarea id="attendees" placeholder="John Doe β€” CEO&#10;Jane Smith β€” Head of Product&#10;Alex Ray β€” Legal Counsel"></textarea>
539
+ </div>
540
+
541
+ <div class="field span-2">
542
+ <label for="duration">Meeting Duration</label>
543
+ <div class="duration-row">
544
+ <input type="range" id="duration" min="15" max="180" step="15" value="60" />
545
+ <div class="duration-badge" id="duration-label">60 min</div>
546
+ </div>
547
+ </div>
548
+
549
+ <div class="field span-2">
550
+ <label for="focus">Focus Areas &amp; Concerns</label>
551
+ <input type="text" id="focus" placeholder="e.g. Budget constraints, Q4 timeline, technical feasibility" autocomplete="off" />
552
+ </div>
553
+
554
+ </div>
555
+
556
+ <button class="btn-submit" id="submit-btn" onclick="runAgent()">
557
+ ⚑ &nbsp;Prepare My Meeting
558
+ </button>
559
+
560
+ <div id="error-toast">
561
+ <span style="font-size:16px">⚠</span>
562
+ <span id="error-msg">Something went wrong. Please try again.</span>
563
+ </div>
564
+ </div>
565
+
566
+ <!-- ── Search Links panel (always visible in form section) ──────────── -->
567
+ <div class="search-panel">
568
+ <div class="section-label">πŸ”— Quick Link Research</div>
569
+ <p style="color:var(--txt-m);font-size:13px;margin-bottom:18px;line-height:1.6;">
570
+ Use the standalone search agent to instantly fetch relevant links and references
571
+ for any topic β€” company, industry, technology, or competitor.
572
+ </p>
573
+ <div class="search-row">
574
+ <div class="field">
575
+ <label for="search-query">Search Query</label>
576
+ <input
577
+ type="text"
578
+ id="search-query"
579
+ placeholder="e.g. OpenAI latest funding round, EV battery supply chain 2024…"
580
+ autocomplete="off"
581
+ />
582
+ </div>
583
+ <button class="btn-search" id="search-btn" onclick="runSearch()">
584
+ πŸ” &nbsp;Search
585
+ </button>
586
+ </div>
587
+
588
+ <!-- error message -->
589
+ <div id="search-error">
590
+ <span>⚠</span>
591
+ <span id="search-error-msg">Search failed. Please try again.</span>
592
+ </div>
593
+
594
+ <!-- results panel -->
595
+ <div id="search-output">
596
+ <div class="search-result-header">
597
+ <div class="search-result-title">πŸ”— Search Results</div>
598
+ <div class="search-elapsed">Completed in <span id="search-elapsed-val">β€”</span>s &nbsp;Β·&nbsp; Model: gemini-2.0-flash-lite</div>
599
+ </div>
600
+ <div class="search-body" id="search-body"></div>
601
+ </div>
602
+ </div>
603
+
604
+ </div><!-- end #form-section -->
605
+
606
+ <!-- ── Loader ─────────────────────────────────────────────────────────── -->
607
+ <div id="loader">
608
+ <div class="loader-card">
609
+ <div class="orbit-wrap">
610
+ <div class="orbit-ring orbit-ring-1"></div>
611
+ <div class="orbit-ring orbit-ring-2"></div>
612
+ <div class="orbit-ring orbit-ring-3"></div>
613
+ <div class="orbit-center"></div>
614
+ </div>
615
+ <div class="loader-title">MeetIQ is preparing your meeting…</div>
616
+ <div class="loader-sub" id="loader-sub-text">Understanding your meeting objective…</div>
617
+ <div class="loader-timer">Elapsed: <span id="elapsed-clock">0s</span></div>
618
+ <div class="agent-steps">
619
+ <div class="agent-step active" id="step-1">
620
+ <div class="step-icon">🧠</div>
621
+ <span>Understanding your meeting objective…</span>
622
+ </div>
623
+ <div class="agent-step" id="step-2">
624
+ <div class="step-icon">πŸ”</div>
625
+ <span>Researching company and context…</span>
626
+ </div>
627
+ <div class="agent-step" id="step-3">
628
+ <div class="step-icon">πŸ“Š</div>
629
+ <span>Analyzing market insights…</span>
630
+ </div>
631
+ <div class="agent-step" id="step-4">
632
+ <div class="step-icon">πŸ—ΊοΈ</div>
633
+ <span>Building strategy and talking points…</span>
634
+ </div>
635
+ <div class="agent-step" id="step-5">
636
+ <div class="step-icon">πŸ“‹</div>
637
+ <span>Structuring your meeting agenda…</span>
638
+ </div>
639
+ <div class="agent-step" id="step-6">
640
+ <div class="step-icon">✨</div>
641
+ <span>Finalizing your executive brief…</span>
642
+ </div>
643
+ </div>
644
+ </div>
645
+ </div>
646
+
647
+ <!-- ── Result ─────────────────────────────────────────────────────────── -->
648
+ <div id="result-section">
649
+
650
+ <!-- Decision + Memory meta row -->
651
+ <div class="meta-row">
652
+
653
+ <!-- Decision card -->
654
+ <div class="meta-card">
655
+ <div class="meta-card-title blue">🧠 Decision Agent Output</div>
656
+ <div class="decision-rows">
657
+ <div class="decision-row">
658
+ <span class="decision-row-label">Web Search</span>
659
+ <span class="decision-badge" id="badge-search">β€”</span>
660
+ </div>
661
+ <div class="decision-row">
662
+ <span class="decision-row-label">Search Mode</span>
663
+ <span class="decision-badge badge-cyan" id="badge-search-mode">β€”</span>
664
+ </div>
665
+ <div class="decision-row">
666
+ <span class="decision-row-label">Memory Injection</span>
667
+ <span class="decision-badge" id="badge-memory-flag">β€”</span>
668
+ </div>
669
+ <div class="decision-row">
670
+ <span class="decision-row-label">Priority Focus</span>
671
+ <span class="decision-badge badge-purple" id="badge-priority">β€”</span>
672
+ </div>
673
+ <div class="decision-row">
674
+ <span class="decision-row-label">Brief Depth</span>
675
+ <span class="decision-badge badge-yellow" id="badge-depth">β€”</span>
676
+ </div>
677
+ </div>
678
+ <div class="reasoning-text" id="decision-reasoning">β€”</div>
679
+ </div>
680
+
681
+ <!-- Memory card -->
682
+ <div class="meta-card">
683
+ <div class="meta-card-title violet">πŸ—‚ Session Memory</div>
684
+ <div class="memory-indicator">
685
+ <div class="memory-icon inactive-mem" id="memory-icon">πŸ—‚</div>
686
+ <div class="memory-text-wrap">
687
+ <div class="memory-status no" id="memory-status-text">Not used</div>
688
+ <div class="memory-sub" id="memory-sub-text">
689
+ No past interactions were injected into this run.
690
+ </div>
691
+ </div>
692
+ </div>
693
+ <!-- elapsed time -->
694
+ <div style="margin-top:18px;padding-top:14px;border-top:1px solid rgba(255,255,255,.06);">
695
+ <div style="font-size:11px;color:var(--txt-f);letter-spacing:.06em;text-transform:uppercase;margin-bottom:6px;">Total Run Time</div>
696
+ <div id="result-elapsed" style="font-family:'Syne',sans-serif;font-size:18px;font-weight:700;color:var(--green);">β€”</div>
697
+ </div>
698
+ </div>
699
+
700
+ </div>
701
+
702
+ <!-- Brief card -->
703
+ <div class="glass-card">
704
+ <div class="result-header">
705
+ <div class="result-title">MeetIQ Brief<span>Ready</span> βœ“</div>
706
+ <div class="result-actions">
707
+ <button class="btn-download" id="download-btn" onclick="downloadPDF()">⬇ &nbsp;Download PDF</button>
708
+ <button class="btn-reset" onclick="resetForm()">← Start Over</button>
709
+ </div>
710
+ </div>
711
+ <div class="result-body" id="print-area"></div>
712
+ </div>
713
+
714
+ </div>
715
+
716
+ <footer>
717
+ MeetIQ Β· Business Meeting Preparation Agent
718
+ </footer>
719
+ </div>
720
+
721
+ <!-- ── JavaScript ──────────────────────────────────────────────────────── -->
722
+ <script>
723
+ /* ══════════════════════════════════════════════════════════════════════
724
+ MARKED GLOBAL RENDERER β€” all links open in a new tab
725
+ Without this, sites block loading inside the same window/iframe
726
+ and the browser shows "refused to connect".
727
+ ══════════════════════════════════════════════════════════════════════ */
728
+ (function setupMarked() {
729
+ const renderer = new marked.Renderer();
730
+ renderer.link = function(href, title, text) {
731
+ // href can be an object in newer marked versions
732
+ const url = (typeof href === 'object' && href !== null) ? (href.href || '') : (href || '');
733
+ const label = (typeof href === 'object' && href !== null) ? (href.text || text || url) : (text || url);
734
+ const tip = (typeof href === 'object' && href !== null) ? (href.title || title || '') : (title || '');
735
+ const titleAttr = tip ? ` title="${tip}"` : '';
736
+ return `<a href="${url}" target="_blank" rel="noopener noreferrer"${titleAttr}>${label}</a>`;
737
+ };
738
+ marked.setOptions({ renderer });
739
+ })();
740
+
741
+ /* ── Duration slider ──────────────────────────────────────────────────── */
742
+ const durInput = document.getElementById('duration');
743
+ const durLabel = document.getElementById('duration-label');
744
+ function updateSlider() {
745
+ const min = parseInt(durInput.min), max = parseInt(durInput.max), val = parseInt(durInput.value);
746
+ durInput.style.setProperty('--pct', ((val - min) / (max - min) * 100) + '%');
747
+ durLabel.textContent = val + ' min';
748
+ }
749
+ durInput.addEventListener('input', updateSlider);
750
+ updateSlider();
751
+
752
+ /* ══════════════════════════════════════════════════════════════════════
753
+ LOADER β€” step cycle + live clock
754
+ Each agent typically runs 20–60 s, so we tick every 25 s.
755
+ The clock is always accurate because it tracks wall time.
756
+ ══════════════════════════════════════════════════════════════════════ */
757
+ const STEP_TEXTS = [
758
+ 'Initialising decision engine…',
759
+ 'Researching company context β€” running web searches…',
760
+ 'Analysing industry trends & competitors…',
761
+ 'Drafting agenda & talking points…',
762
+ 'Composing executive brief…',
763
+ 'Reflecting & polishing final output…',
764
+ ];
765
+ const STEP_COUNT = 6;
766
+ const STEP_INTERVAL_MS = 25000; // 25 s per step β†’ 6 steps β‰ˆ 2.5 min total
767
+
768
+ let stepTimer = null;
769
+ let clockTimer = null;
770
+ let currentStep = 0;
771
+ let loaderStart = 0;
772
+
773
+ function startStepCycle() {
774
+ currentStep = 0;
775
+ loaderStart = Date.now();
776
+ resetSteps();
777
+ setStep(0);
778
+
779
+ stepTimer = setInterval(() => {
780
+ if (currentStep < STEP_COUNT - 1) {
781
+ markDone(currentStep);
782
+ currentStep++;
783
+ setStep(currentStep);
784
+ }
785
+ }, STEP_INTERVAL_MS);
786
+
787
+ // Live elapsed clock
788
+ clockTimer = setInterval(() => {
789
+ const sec = Math.floor((Date.now() - loaderStart) / 1000);
790
+ const el = document.getElementById('elapsed-clock');
791
+ if (el) el.textContent = sec < 60 ? sec + 's' : Math.floor(sec/60) + 'm ' + (sec % 60) + 's';
792
+ }, 1000);
793
+ }
794
+
795
+ function setStep(i) {
796
+ const el = document.getElementById(`step-${i + 1}`);
797
+ if (el) {
798
+ el.classList.add('active');
799
+ document.getElementById('loader-sub-text').textContent = STEP_TEXTS[i];
800
+ }
801
+ }
802
+ function markDone(i) {
803
+ const el = document.getElementById(`step-${i + 1}`);
804
+ if (el) {
805
+ el.classList.remove('active');
806
+ el.classList.add('done');
807
+ el.querySelector('.step-icon').textContent = 'βœ“';
808
+ }
809
+ }
810
+ function stopStepCycle() {
811
+ clearInterval(stepTimer);
812
+ clearInterval(clockTimer);
813
+ for (let i = 0; i < STEP_COUNT; i++) markDone(i);
814
+ }
815
+ function resetSteps() {
816
+ const icons = ['🧠','πŸ”','πŸ“Š','πŸ—ΊοΈ','πŸ“‹','✨'];
817
+ for (let i = 0; i < STEP_COUNT; i++) {
818
+ const el = document.getElementById(`step-${i + 1}`);
819
+ el.classList.remove('active','done');
820
+ el.querySelector('.step-icon').textContent = icons[i];
821
+ }
822
+ const clk = document.getElementById('elapsed-clock');
823
+ if (clk) clk.textContent = '0s';
824
+ }
825
+
826
+ /* ── UI state helpers ─────────────────────────────────────────────────── */
827
+ function showLoader() {
828
+ document.getElementById('form-section').style.display = 'none';
829
+ document.getElementById('loader').classList.add('active');
830
+ document.getElementById('result-section').classList.remove('active');
831
+ document.getElementById('error-toast').classList.remove('active');
832
+ startStepCycle();
833
+ }
834
+ function showResult(briefHtml, decisionText, memoryUsed, flags, elapsedSec) {
835
+ stopStepCycle();
836
+ document.getElementById('loader').classList.remove('active');
837
+
838
+ // Populate brief
839
+ document.getElementById('print-area').innerHTML = briefHtml;
840
+
841
+ // ── Parse + populate Decision card ───────────────────────────────────
842
+ const upper = decisionText.toUpperCase();
843
+
844
+ const searchYes = !/SEARCH\s*[:=]\s*NO/.test(upper) && !/SEARCH\s*[:=]\s*MINIMAL/.test(upper);
845
+ const memoryYes = !/MEMORY\s*[:=]\s*NO/.test(upper);
846
+
847
+ // Extract individual flags (prefer server flags if available)
848
+ const searchMode = (flags && flags.search_mode) || (
849
+ /SEARCH\s*[:=]\s*ALWAYS/.test(upper) ? 'ALWAYS' :
850
+ /SEARCH\s*[:=]\s*LIGHT/.test(upper) ? 'LIGHT' : 'MINIMAL'
851
+ );
852
+ const priority = (flags && flags.priority) || (
853
+ /PRIORITY\s*[:=]\s*INDUSTRY/.test(upper) ? 'Industry' :
854
+ /PRIORITY\s*[:=]\s*STRATEGY/.test(upper) ? 'Strategy' : 'Context'
855
+ );
856
+ const depth = (flags && flags.depth) || (
857
+ /DEPTH\s*[:=]\s*DEEP/.test(upper) ? 'DEEP' :
858
+ /DEPTH\s*[:=]\s*SHORT/.test(upper) ? 'SHORT' : 'NORMAL'
859
+ );
860
+
861
+ // Extract reasoning line
862
+ const reasonMatch = decisionText.match(/REASONING\s*[:=]\s*(.+)/i);
863
+ const reasoning = reasonMatch ? reasonMatch[1].trim() : 'β€”';
864
+
865
+ setDecisionBadge('badge-search', searchYes ? 'YES' : 'NO', searchYes ? 'badge-yes' : 'badge-no');
866
+ setDecisionBadge('badge-search-mode', searchMode, 'badge-cyan');
867
+ setDecisionBadge('badge-memory-flag', memoryUsed ? 'YES' : 'NO', memoryUsed ? 'badge-yes' : 'badge-no');
868
+ setDecisionBadge('badge-priority', priority, 'badge-purple');
869
+ setDecisionBadge('badge-depth', depth, 'badge-yellow');
870
+ document.getElementById('decision-reasoning').textContent = reasoning;
871
+
872
+ // ── Memory card ───────────────────────────────────────────────────────
873
+ const memIcon = document.getElementById('memory-icon');
874
+ const memStat = document.getElementById('memory-status-text');
875
+ const memSub = document.getElementById('memory-sub-text');
876
+ if (memoryUsed) {
877
+ memIcon.className = 'memory-icon active-mem';
878
+ memIcon.textContent = 'βœ…';
879
+ memStat.className = 'memory-status yes';
880
+ memStat.textContent = 'Memory Active';
881
+ memSub.textContent = 'Past interaction history was injected into the agents to improve context accuracy.';
882
+ } else {
883
+ memIcon.className = 'memory-icon inactive-mem';
884
+ memIcon.textContent = 'πŸ—‚';
885
+ memStat.className = 'memory-status no';
886
+ memStat.textContent = 'Not used';
887
+ memSub.textContent = 'No relevant past interactions were available or applicable for this run.';
888
+ }
889
+
890
+ // ── Elapsed time ──────────────────────────────────────────────────────
891
+ if (elapsedSec !== undefined) {
892
+ const mins = Math.floor(elapsedSec / 60);
893
+ const secs = Math.round(elapsedSec % 60);
894
+ document.getElementById('result-elapsed').textContent =
895
+ mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
896
+ }
897
+
898
+ document.getElementById('result-section').classList.add('active');
899
+ document.getElementById('result-section').scrollIntoView({ behavior: 'smooth', block: 'start' });
900
+ }
901
+ function setDecisionBadge(id, label, cls) {
902
+ const el = document.getElementById(id);
903
+ el.textContent = label;
904
+ el.className = 'decision-badge ' + cls;
905
+ }
906
+ function showForm() {
907
+ document.getElementById('form-section').style.display = 'block';
908
+ document.getElementById('loader').classList.remove('active');
909
+ document.getElementById('result-section').classList.remove('active');
910
+ document.getElementById('submit-btn').disabled = false;
911
+ }
912
+ function showError(msg) {
913
+ stopStepCycle();
914
+ document.getElementById('loader').classList.remove('active');
915
+ document.getElementById('form-section').style.display = 'block';
916
+ document.getElementById('error-toast').classList.add('active');
917
+ document.getElementById('error-msg').textContent = msg;
918
+ document.getElementById('submit-btn').disabled = false;
919
+ }
920
+
921
+ /* ── Main agent fetch ────���────────────────────────────────────────────── */
922
+ async function runAgent() {
923
+ const company = document.getElementById('company').value.trim();
924
+ const objective = document.getElementById('objective').value.trim();
925
+ const attendees = document.getElementById('attendees').value.trim();
926
+ const duration = parseInt(document.getElementById('duration').value);
927
+ const focus = document.getElementById('focus').value.trim();
928
+
929
+ if (!company || !objective || !attendees || !focus) {
930
+ document.getElementById('error-toast').classList.add('active');
931
+ document.getElementById('error-msg').textContent =
932
+ 'Please fill in all fields before preparing your meeting.';
933
+ return;
934
+ }
935
+
936
+ document.getElementById('submit-btn').disabled = true;
937
+ document.getElementById('error-toast').classList.remove('active');
938
+ showLoader();
939
+
940
+ try {
941
+ const response = await fetch('/run-agent', {
942
+ method: 'POST',
943
+ headers: { 'Content-Type': 'application/json' },
944
+ body: JSON.stringify({
945
+ company_name: company,
946
+ meeting_objective: objective,
947
+ attendees: attendees,
948
+ meeting_duration: duration,
949
+ focus_areas: focus,
950
+ }),
951
+ });
952
+ const data = await response.json();
953
+ if (!response.ok) throw new Error(data.error || `Server error ${response.status}`);
954
+
955
+ const briefHtml = marked.parse(data.result || '');
956
+ const decisionText = data.decision || '';
957
+ const memoryUsed = data.memory_used === true;
958
+ const flags = data.flags || {};
959
+ const elapsedSec = data.elapsed_sec;
960
+
961
+ showResult(briefHtml, decisionText, memoryUsed, flags, elapsedSec);
962
+ } catch (err) {
963
+ if (err.name === 'TypeError' && err.message.includes('fetch')) {
964
+ showError('Could not reach the server. Make sure the app is running.');
965
+ } else {
966
+ showError(err.message || 'Unexpected error. Please try again.');
967
+ }
968
+ }
969
+ }
970
+
971
+ /* ══════════════════════════════════════════════════════════════════════
972
+ STANDALONE SEARCH LINKS β€” calls /search-links and renders results
973
+ ══════════════════════════════════════════════════════════════════════ */
974
+ async function runSearch() {
975
+ const query = document.getElementById('search-query').value.trim();
976
+ if (!query) {
977
+ document.getElementById('search-error').classList.add('active');
978
+ document.getElementById('search-error-msg').textContent = 'Please enter a search query.';
979
+ return;
980
+ }
981
+
982
+ const btn = document.getElementById('search-btn');
983
+ btn.disabled = true;
984
+ btn.textContent = '⏳ Searching…';
985
+
986
+ document.getElementById('search-error').classList.remove('active');
987
+ document.getElementById('search-output').classList.remove('active');
988
+
989
+ try {
990
+ const response = await fetch('/search-links', {
991
+ method: 'POST',
992
+ headers: { 'Content-Type': 'application/json' },
993
+ body: JSON.stringify({ query }),
994
+ });
995
+ const data = await response.json();
996
+
997
+ if (!response.ok) throw new Error(data.error || `Server error ${response.status}`);
998
+
999
+ // Render markdown result into the search body
1000
+ const html = marked.parse(data.links || '_No results returned._');
1001
+ document.getElementById('search-body').innerHTML = html;
1002
+ document.getElementById('search-elapsed-val').textContent =
1003
+ data.elapsed_sec !== undefined ? data.elapsed_sec : 'β€”';
1004
+
1005
+ document.getElementById('search-output').classList.add('active');
1006
+ } catch (err) {
1007
+ document.getElementById('search-error').classList.add('active');
1008
+ document.getElementById('search-error-msg').textContent =
1009
+ err.message || 'Search failed. Please try again.';
1010
+ } finally {
1011
+ btn.disabled = false;
1012
+ btn.innerHTML = 'πŸ” &nbsp;Search';
1013
+ }
1014
+ }
1015
+
1016
+ /* ── PDF download ─────────────────────────────────────────────────────── */
1017
+ function downloadPDF() {
1018
+ const sourceEl = document.getElementById('print-area');
1019
+ const companyName = document.getElementById('company').value.trim() || 'Meeting';
1020
+ const wrapper = document.createElement('div');
1021
+ wrapper.style.cssText = [
1022
+ 'position:fixed','top:-9999px','left:-9999px',
1023
+ 'width:800px','padding:40px 48px',
1024
+ 'background:#ffffff','color:#1a1a2e',
1025
+ 'font-family:Georgia,serif','font-size:13pt','line-height:1.75',
1026
+ 'z-index:-1',
1027
+ ].join(';');
1028
+ wrapper.innerHTML = `
1029
+ <style>
1030
+ .pdf-clone * { box-sizing:border-box; }
1031
+ .pdf-clone { color:#1a1a2e !important; background:#fff !important; }
1032
+ .pdf-clone h1 { font-size:22pt; font-weight:800; color:#0f172a !important;
1033
+ border-bottom:2px solid #334155; padding-bottom:10px; margin:0 0 18px; }
1034
+ .pdf-clone h2 { font-size:15pt; font-weight:700; color:#1e3a5f !important; margin:24px 0 10px; }
1035
+ .pdf-clone h3 { font-size:12pt; font-weight:700; color:#1e3a5f !important; margin:18px 0 8px; }
1036
+ .pdf-clone p { color:#1a1a2e !important; margin-bottom:12px; }
1037
+ .pdf-clone ul, .pdf-clone ol { padding-left:22px; margin-bottom:12px; }
1038
+ .pdf-clone li { color:#1a1a2e !important; margin-bottom:5px; }
1039
+ .pdf-clone strong { color:#0f172a !important; font-weight:700; }
1040
+ .pdf-clone em { color:#334155 !important; }
1041
+ .pdf-clone code { background:#f1f5f9 !important; color:#0369a1 !important;
1042
+ border:1px solid #cbd5e1; border-radius:4px; padding:2px 6px; font-size:10pt; }
1043
+ .pdf-clone blockquote { border-left:3px solid #3b82f6 !important; background:#f0f7ff !important;
1044
+ color:#334155 !important; padding:10px 16px; margin:14px 0; border-radius:0 6px 6px 0; }
1045
+ .pdf-clone hr { border:none; border-top:1px solid #cbd5e1; margin:20px 0; }
1046
+ .pdf-clone * { -webkit-text-fill-color:initial !important; text-shadow:none !important; }
1047
+ </style>
1048
+ <div class="pdf-clone">${sourceEl.innerHTML}</div>`;
1049
+ document.body.appendChild(wrapper);
1050
+ const clone = wrapper.querySelector('.pdf-clone');
1051
+ const btn = document.getElementById('download-btn');
1052
+ btn.disabled = true;
1053
+ btn.textContent = '⏳ Generating…';
1054
+ html2pdf()
1055
+ .set({
1056
+ margin: [0.6,0.6,0.6,0.6],
1057
+ filename: `${companyName}_Executive_Brief.pdf`,
1058
+ image: { type:'jpeg', quality:0.98 },
1059
+ html2canvas: { scale:2, useCORS:true, backgroundColor:'#ffffff', logging:false, scrollY:0 },
1060
+ jsPDF: { unit:'in', format:'letter', orientation:'portrait' },
1061
+ })
1062
+ .from(clone)
1063
+ .save()
1064
+ .finally(() => {
1065
+ document.body.removeChild(wrapper);
1066
+ btn.disabled = false;
1067
+ btn.innerHTML = '⬇ &nbsp;Download PDF';
1068
+ });
1069
+ }
1070
+
1071
+ /* ── Reset ────────────────────────────────────────────────────────────── */
1072
+ function resetForm() {
1073
+ document.getElementById('print-area').innerHTML = '';
1074
+ showForm();
1075
+ }
1076
+
1077
+ /* ── Enter key shortcuts ──────────────────────────────────────────────── */
1078
+ ['company','objective','focus'].forEach(id => {
1079
+ document.getElementById(id).addEventListener('keydown', e => {
1080
+ if (e.key === 'Enter') runAgent();
1081
+ });
1082
+ });
1083
+ document.getElementById('search-query').addEventListener('keydown', e => {
1084
+ if (e.key === 'Enter') runSearch();
1085
+ });
1086
+ </script>
1087
+
1088
+ </body>
1089
+ </html>
memory.json ADDED
@@ -0,0 +1 @@
 
 
1
+ []
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ── Web server ───────────────────────────────────────────────────────────────
2
+ flask==3.1.0
3
+ flask-cors==5.0.0
4
+ gunicorn==23.0.0
5
+
6
+ # ── Environment / config ─────────────────────────────────────────────────────
7
+ python-dotenv==1.1.0
8
+
9
+ # ── AI agent framework ───────────────────────────────────────────────────────
10
+ crewai>=0.120.0,<0.122.0
11
+ crewai-tools>=0.38.0
12
+
13
+ # ── Tokeniser required by crewai internals ───────────────────────────────────
14
+ tiktoken==0.7.0