mnoorchenar commited on
Commit
f9b41cf
Β·
1 Parent(s): 4654a4b

Update 2026-03-22 21:26:46

Browse files
Files changed (7) hide show
  1. agents/critic.py +3 -16
  2. agents/generator.py +3 -15
  3. agents/llm_factory.py +51 -7
  4. agents/planner.py +3 -16
  5. app.py +34 -3
  6. templates/index.html +566 -321
  7. write_html.py +570 -324
agents/critic.py CHANGED
@@ -1,15 +1,11 @@
1
  """
2
  Critic agent β€” LangChain LCEL chain.
3
 
4
- Evaluates the generated answer for hallucinations and completeness against
5
- the source context. Returns a verdict dict consumed by the LangGraph node.
6
-
7
- Chain: ChatPromptTemplate | ChatOpenAI (Qwen 2.5-7B, temp 0.1) | StrOutputParser
8
  """
9
  import re
10
  from langchain_core.prompts import ChatPromptTemplate
11
  from langchain_core.output_parsers import StrOutputParser
12
-
13
  from agents.llm_factory import get_llm
14
 
15
  _SYSTEM = """You are a strict quality-control critic. Evaluate the answer for accuracy and grounding.
@@ -25,20 +21,11 @@ _prompt = ChatPromptTemplate.from_messages([
25
  ("human", "Question: {question}\nContext (first 1500 chars): {context}\nAnswer: {answer}"),
26
  ])
27
 
28
- _chain = None
29
-
30
-
31
- def _get_chain():
32
- global _chain
33
- if _chain is None:
34
- # Low temperature β†’ near-deterministic evaluation
35
- _chain = _prompt | get_llm(temperature=0.1, max_tokens=150) | StrOutputParser()
36
- return _chain
37
-
38
 
39
  def run_critic(question: str, answer: str, documents: list) -> dict:
 
40
  context = " ".join(d["page_content"] for d in documents)[:1500]
41
- raw = _get_chain().invoke({"question": question, "context": context, "answer": answer})
42
  verdict = "NEEDS_REVIEW" if re.search(r"NEEDS_REVIEW", raw, re.IGNORECASE) else "APPROVED"
43
  explanation = raw.split("\n", 1)[-1].strip() if "\n" in raw else raw
44
  return {"verdict": verdict, "explanation": explanation[:300]}
 
1
  """
2
  Critic agent β€” LangChain LCEL chain.
3
 
4
+ Chain: ChatPromptTemplate | ChatOpenAI (current model, temp 0.1) | StrOutputParser
 
 
 
5
  """
6
  import re
7
  from langchain_core.prompts import ChatPromptTemplate
8
  from langchain_core.output_parsers import StrOutputParser
 
9
  from agents.llm_factory import get_llm
10
 
11
  _SYSTEM = """You are a strict quality-control critic. Evaluate the answer for accuracy and grounding.
 
21
  ("human", "Question: {question}\nContext (first 1500 chars): {context}\nAnswer: {answer}"),
22
  ])
23
 
 
 
 
 
 
 
 
 
 
 
24
 
25
  def run_critic(question: str, answer: str, documents: list) -> dict:
26
+ chain = _prompt | get_llm(temperature=0.1, max_tokens=150) | StrOutputParser()
27
  context = " ".join(d["page_content"] for d in documents)[:1500]
28
+ raw = chain.invoke({"question": question, "context": context, "answer": answer})
29
  verdict = "NEEDS_REVIEW" if re.search(r"NEEDS_REVIEW", raw, re.IGNORECASE) else "APPROVED"
30
  explanation = raw.split("\n", 1)[-1].strip() if "\n" in raw else raw
31
  return {"verdict": verdict, "explanation": explanation[:300]}
agents/generator.py CHANGED
@@ -1,14 +1,10 @@
1
  """
2
  Generator agent β€” LangChain LCEL chain.
3
 
4
- Synthesises a cited answer from the top-graded context chunks passed in by
5
- the LangGraph orchestrator. Sources are formatted as [Source: name, p.N].
6
-
7
- Chain: ChatPromptTemplate | ChatOpenAI (Qwen 2.5-7B) | StrOutputParser
8
  """
9
  from langchain_core.prompts import ChatPromptTemplate
10
  from langchain_core.output_parsers import StrOutputParser
11
-
12
  from agents.llm_factory import get_llm
13
 
14
  _SYSTEM = (
@@ -22,15 +18,6 @@ _prompt = ChatPromptTemplate.from_messages([
22
  ("human", "Context:\n{context}\n\nQuestion: {question}"),
23
  ])
24
 
25
- _chain = None
26
-
27
-
28
- def _get_chain():
29
- global _chain
30
- if _chain is None:
31
- _chain = _prompt | get_llm(temperature=0.4, max_tokens=512) | StrOutputParser()
32
- return _chain
33
-
34
 
35
  def _format_context(documents: list) -> str:
36
  parts = [
@@ -41,5 +28,6 @@ def _format_context(documents: list) -> str:
41
 
42
 
43
  def run_generator(question: str, documents: list) -> str:
 
44
  context = _format_context(documents)
45
- return _get_chain().invoke({"context": context, "question": question})
 
1
  """
2
  Generator agent β€” LangChain LCEL chain.
3
 
4
+ Chain: ChatPromptTemplate | ChatOpenAI (current model, temp 0.4) | StrOutputParser
 
 
 
5
  """
6
  from langchain_core.prompts import ChatPromptTemplate
7
  from langchain_core.output_parsers import StrOutputParser
 
8
  from agents.llm_factory import get_llm
9
 
10
  _SYSTEM = (
 
18
  ("human", "Context:\n{context}\n\nQuestion: {question}"),
19
  ])
20
 
 
 
 
 
 
 
 
 
 
21
 
22
  def _format_context(documents: list) -> str:
23
  parts = [
 
28
 
29
 
30
  def run_generator(question: str, documents: list) -> str:
31
+ chain = _prompt | get_llm(temperature=0.4, max_tokens=512) | StrOutputParser()
32
  context = _format_context(documents)
33
+ return chain.invoke({"context": context, "question": question})
agents/llm_factory.py CHANGED
@@ -2,31 +2,75 @@
2
  LLM factory β€” returns a LangChain ChatOpenAI instance wired to the
3
  HuggingFace Router (OpenAI-compatible endpoint).
4
 
5
- Each LLM agent (planner, generator, critic) calls get_llm() and builds its
6
- own LCEL chain, keeping temperature and token budgets tuned per role.
 
7
  """
8
  import os
9
  from langchain_openai import ChatOpenAI
10
 
11
  _BASE_URL = "https://router.huggingface.co/v1"
12
- _MODEL_ID = "Qwen/Qwen2.5-7B-Instruct"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
 
15
  def get_llm(temperature: float = 0.7, max_tokens: int = 512) -> ChatOpenAI:
16
- """Return a LangChain ChatOpenAI wired to the HuggingFace Router.
 
 
 
17
 
18
- .with_retry() wraps the call with up to 2 attempts, which gracefully
19
- handles transient 429 / 503 errors from the free inference tier.
20
  """
21
  token = os.getenv("HF_TOKEN", "")
22
  if not token:
23
  raise EnvironmentError(
24
  "HF_TOKEN is not set. Add your HuggingFace token in Space secrets."
25
  )
 
26
  return ChatOpenAI(
27
  base_url=_BASE_URL,
28
  api_key=token,
29
- model=_MODEL_ID,
30
  temperature=max(temperature, 0.01),
31
  max_tokens=max_tokens,
32
  ).with_retry(stop_after_attempt=2)
 
2
  LLM factory β€” returns a LangChain ChatOpenAI instance wired to the
3
  HuggingFace Router (OpenAI-compatible endpoint).
4
 
5
+ AVAILABLE_MODELS lists every model available to the UI picker.
6
+ set_model() / get_current_model() let the Flask layer switch models
7
+ without restarting the server.
8
  """
9
  import os
10
  from langchain_openai import ChatOpenAI
11
 
12
  _BASE_URL = "https://router.huggingface.co/v1"
13
+
14
+ AVAILABLE_MODELS: dict[str, dict] = {
15
+ "qwen-7b": {
16
+ "id": "Qwen/Qwen2.5-7B-Instruct",
17
+ "label": "Qwen 2.5 Β· 7B",
18
+ "desc": "Default Β· fast & free Β· best for most queries",
19
+ "speed": 3,
20
+ "params": "7B",
21
+ "color": "#5b8ff9",
22
+ },
23
+ "mistral-nemo": {
24
+ "id": "mistralai/Mistral-Nemo-Instruct-2407",
25
+ "label": "Mistral Nemo Β· 12B",
26
+ "desc": "Stronger reasoning Β· slightly slower",
27
+ "speed": 2,
28
+ "params": "12B",
29
+ "color": "#a78bfa",
30
+ },
31
+ "phi-3-mini": {
32
+ "id": "microsoft/Phi-3.5-mini-instruct",
33
+ "label": "Phi-3.5 Mini Β· 3.8B",
34
+ "desc": "Ultra-fast Β· great for focused questions",
35
+ "speed": 3,
36
+ "params": "3.8B",
37
+ "color": "#22d47a",
38
+ },
39
+ }
40
+
41
+ _current_model: str = "qwen-7b"
42
+
43
+
44
+ def set_model(key: str) -> None:
45
+ """Switch the active model for all subsequent LLM calls."""
46
+ global _current_model
47
+ if key in AVAILABLE_MODELS:
48
+ _current_model = key
49
+
50
+
51
+ def get_current_model() -> dict:
52
+ """Return the full metadata dict for the currently selected model."""
53
+ return {"key": _current_model, **AVAILABLE_MODELS[_current_model]}
54
 
55
 
56
  def get_llm(temperature: float = 0.7, max_tokens: int = 512) -> ChatOpenAI:
57
+ """Return a LangChain ChatOpenAI using the currently selected model.
58
+
59
+ Always reads _current_model at call time so model switching takes effect
60
+ immediately β€” no stale cached chains.
61
 
62
+ .with_retry() handles transient 429/503 errors from the free HF tier.
 
63
  """
64
  token = os.getenv("HF_TOKEN", "")
65
  if not token:
66
  raise EnvironmentError(
67
  "HF_TOKEN is not set. Add your HuggingFace token in Space secrets."
68
  )
69
+ model_id = AVAILABLE_MODELS[_current_model]["id"]
70
  return ChatOpenAI(
71
  base_url=_BASE_URL,
72
  api_key=token,
73
+ model=model_id,
74
  temperature=max(temperature, 0.01),
75
  max_tokens=max_tokens,
76
  ).with_retry(stop_after_attempt=2)
agents/planner.py CHANGED
@@ -1,14 +1,10 @@
1
  """
2
  Planner agent β€” LangChain LCEL chain.
3
 
4
- Decomposes the user question into a brief research plan that guides
5
- downstream retrieval and generation steps.
6
-
7
- Chain: ChatPromptTemplate | ChatOpenAI (Qwen 2.5-7B) | StrOutputParser
8
  """
9
  from langchain_core.prompts import ChatPromptTemplate
10
  from langchain_core.output_parsers import StrOutputParser
11
-
12
  from agents.llm_factory import get_llm
13
 
14
  _SYSTEM = (
@@ -22,16 +18,7 @@ _prompt = ChatPromptTemplate.from_messages([
22
  ("human", "{question}"),
23
  ])
24
 
25
- # Lazy-initialised so HF_TOKEN is not required at import time
26
- _chain = None
27
-
28
-
29
- def _get_chain():
30
- global _chain
31
- if _chain is None:
32
- _chain = _prompt | get_llm(temperature=0.3, max_tokens=200) | StrOutputParser()
33
- return _chain
34
-
35
 
36
  def run_planner(question: str) -> str:
37
- return _get_chain().invoke({"question": question})
 
 
1
  """
2
  Planner agent β€” LangChain LCEL chain.
3
 
4
+ Chain: ChatPromptTemplate | ChatOpenAI (current model, temp 0.3) | StrOutputParser
 
 
 
5
  """
6
  from langchain_core.prompts import ChatPromptTemplate
7
  from langchain_core.output_parsers import StrOutputParser
 
8
  from agents.llm_factory import get_llm
9
 
10
  _SYSTEM = (
 
18
  ("human", "{question}"),
19
  ])
20
 
 
 
 
 
 
 
 
 
 
 
21
 
22
  def run_planner(question: str) -> str:
23
+ chain = _prompt | get_llm(temperature=0.3, max_tokens=200) | StrOutputParser()
24
+ return chain.invoke({"question": question})
app.py CHANGED
@@ -9,6 +9,7 @@ from rag.vector_store import HybridVectorStore
9
  from rag.ingestor import PDFIngestor, URLIngestor, MAX_PDF_BYTES
10
  from graph.research_graph import ResearchGraph
11
  from tracing.tracer import Tracer
 
12
 
13
  app = Flask(__name__)
14
  app.secret_key = os.getenv("SECRET_KEY", os.urandom(24).hex())
@@ -19,10 +20,10 @@ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
19
  vector_store = HybridVectorStore()
20
  tracer = Tracer()
21
  graph = ResearchGraph(vector_store, tracer)
22
- queries = {}
23
 
24
 
25
- def _clear_uploads():
26
  for f in os.listdir(UPLOAD_FOLDER):
27
  try:
28
  os.remove(os.path.join(UPLOAD_FOLDER, f))
@@ -30,6 +31,8 @@ def _clear_uploads():
30
  pass
31
 
32
 
 
 
33
  @app.route("/")
34
  def index():
35
  return render_template("index.html")
@@ -42,10 +45,33 @@ def health():
42
  "docs_indexed": vector_store.doc_count,
43
  "chunks_stored": vector_store.chunk_count,
44
  "source": vector_store.source_label,
 
45
  "token_set": bool(os.getenv("HF_TOKEN")),
46
  })
47
 
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  @app.route("/api/upload", methods=["POST"])
50
  def upload():
51
  if "file" not in request.files:
@@ -90,11 +116,16 @@ def ingest_url():
90
  def research():
91
  data = request.json or {}
92
  question = (data.get("question") or "").strip()
 
 
93
  if not question:
94
  return jsonify({"error": "Question is required."}), 400
95
  if vector_store.doc_count == 0:
96
  return jsonify({"error": "No source loaded β€” please upload a PDF or fetch a URL first."}), 400
97
- qid = str(uuid.uuid4())
 
 
 
98
  queries[qid] = {"status": "running", "result": None}
99
 
100
  def _run():
 
9
  from rag.ingestor import PDFIngestor, URLIngestor, MAX_PDF_BYTES
10
  from graph.research_graph import ResearchGraph
11
  from tracing.tracer import Tracer
12
+ from agents.llm_factory import AVAILABLE_MODELS, set_model, get_current_model
13
 
14
  app = Flask(__name__)
15
  app.secret_key = os.getenv("SECRET_KEY", os.urandom(24).hex())
 
20
  vector_store = HybridVectorStore()
21
  tracer = Tracer()
22
  graph = ResearchGraph(vector_store, tracer)
23
+ queries: dict = {}
24
 
25
 
26
+ def _clear_uploads() -> None:
27
  for f in os.listdir(UPLOAD_FOLDER):
28
  try:
29
  os.remove(os.path.join(UPLOAD_FOLDER, f))
 
31
  pass
32
 
33
 
34
+ # ── Routes ────────────────────────────────────────────────────────────────────
35
+
36
  @app.route("/")
37
  def index():
38
  return render_template("index.html")
 
45
  "docs_indexed": vector_store.doc_count,
46
  "chunks_stored": vector_store.chunk_count,
47
  "source": vector_store.source_label,
48
+ "model": get_current_model()["label"],
49
  "token_set": bool(os.getenv("HF_TOKEN")),
50
  })
51
 
52
 
53
+ @app.route("/api/models")
54
+ def api_models():
55
+ """Return available models and the currently selected one."""
56
+ current = get_current_model()
57
+ return jsonify({
58
+ "models": AVAILABLE_MODELS,
59
+ "current": current["key"],
60
+ })
61
+
62
+
63
+ @app.route("/api/set_model", methods=["POST"])
64
+ def api_set_model():
65
+ """Switch the active LLM model server-side."""
66
+ data = request.json or {}
67
+ key = (data.get("model") or "").strip()
68
+ if key not in AVAILABLE_MODELS:
69
+ return jsonify({"error": f"Unknown model key '{key}'."}), 400
70
+ set_model(key)
71
+ m = get_current_model()
72
+ return jsonify({"success": True, "model": key, "label": m["label"]})
73
+
74
+
75
  @app.route("/api/upload", methods=["POST"])
76
  def upload():
77
  if "file" not in request.files:
 
116
  def research():
117
  data = request.json or {}
118
  question = (data.get("question") or "").strip()
119
+ model = (data.get("model") or "").strip()
120
+
121
  if not question:
122
  return jsonify({"error": "Question is required."}), 400
123
  if vector_store.doc_count == 0:
124
  return jsonify({"error": "No source loaded β€” please upload a PDF or fetch a URL first."}), 400
125
+ if model and model in AVAILABLE_MODELS:
126
+ set_model(model)
127
+
128
+ qid = str(uuid.uuid4())
129
  queries[qid] = {"status": "running", "result": None}
130
 
131
  def _run():
templates/index.html CHANGED
@@ -17,275 +17,483 @@ html,body{height:100%}
17
  body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
18
  background:var(--bg);color:var(--text);font-size:14px;line-height:1.5}
19
 
20
- /* ── ANIMATIONS ────────────────────────────────── */
21
  @keyframes spin{to{transform:rotate(360deg)}}
22
- @keyframes fadeUp{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
23
- @keyframes pulse{0%,100%{opacity:1}50%{opacity:.45}}
 
 
24
  @keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}
25
 
26
- /* ── HEADER ────────────────────────────────────── */
27
- header{
28
- background:var(--surface);
29
- border-bottom:1px solid var(--border);
30
- padding:0 28px;height:56px;
31
- display:flex;align-items:center;gap:14px;
32
- position:sticky;top:0;z-index:100;
33
- }
34
- .logo{display:flex;align-items:center;gap:9px;text-decoration:none}
35
- .logo-icon{width:30px;height:30px;background:linear-gradient(135deg,var(--accent),var(--purple));
36
- border-radius:8px;display:flex;align-items:center;justify-content:center;
37
- font-size:15px;flex-shrink:0}
38
- .logo-text{font-size:1rem;font-weight:800;color:var(--text);letter-spacing:-.3px}
39
  .logo-text span{color:var(--accent)}
40
- .logo-sub{font-size:.68rem;color:var(--muted);margin-left:2px;font-weight:400;
41
- display:none} /* shown on wider screens */
42
- .hdr-badges{display:flex;gap:8px;align-items:center;margin-left:4px}
43
- .badge{font-size:.65rem;font-weight:700;padding:3px 9px;border-radius:20px;letter-spacing:.02em}
44
- .badge-model{background:rgba(91,143,249,.12);border:1px solid rgba(91,143,249,.25);color:var(--accent)}
45
- .badge-src{background:rgba(113,128,160,.1);border:1px solid var(--border);color:var(--muted);
46
- max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
47
- .badge-src.loaded{background:rgba(34,212,122,.1);border-color:rgba(34,212,122,.3);color:var(--green)}
48
- #hdr-src{margin-left:auto}
49
-
50
- /* ── LAYOUT ─────────────────────────────────────── */
51
- main{display:grid;grid-template-columns:320px 1fr;height:calc(100vh - 56px);overflow:hidden}
52
- .panel{padding:24px 22px;overflow-y:auto;height:100%}
53
- .panel-left{border-right:1px solid var(--border);background:var(--surface)}
 
 
54
  .panel-right{background:var(--bg)}
55
 
56
- /* ── SECTION HEADERS ────────────────────────────── */
57
- .sec-head{display:flex;align-items:center;gap:8px;margin-bottom:16px}
58
- .sec-icon{width:26px;height:26px;border-radius:7px;display:flex;align-items:center;
59
- justify-content:center;font-size:13px;flex-shrink:0}
60
  .sec-icon-blue{background:rgba(91,143,249,.15)}
 
 
61
  .sec-icon-purple{background:rgba(167,139,250,.15)}
62
- .sec-title{font-size:.75rem;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:var(--sub)}
63
-
64
- /* ── TABS ────────────────────────────────────────── */
65
- .tabs{display:flex;gap:2px;background:rgba(255,255,255,.03);border-radius:9px;
66
- padding:3px;margin-bottom:18px;border:1px solid var(--border)}
67
- .tab-btn{flex:1;background:none;border:none;color:var(--muted);font-size:.78rem;
68
- font-weight:600;padding:7px 10px;border-radius:7px;cursor:pointer;
69
- transition:all .18s;font-family:inherit;display:flex;align-items:center;
70
- justify-content:center;gap:5px}
71
- .tab-btn.active{background:var(--card2);color:var(--text);
72
- box-shadow:0 1px 6px rgba(0,0,0,.35)}
73
 
74
- /* ── DROPZONE ────────────────────────────────────── */
75
- .dropzone{
76
- border:2px dashed var(--border2);border-radius:12px;padding:28px 16px;
77
- text-align:center;cursor:pointer;transition:all .22s;position:relative;overflow:hidden;
78
- }
79
- .dropzone::before{
80
- content:"";position:absolute;inset:0;
81
- background:linear-gradient(90deg,transparent 0%,rgba(91,143,249,.04) 50%,transparent 100%);
82
- background-size:200% 100%;opacity:0;transition:opacity .3s;
83
- }
84
- .dropzone:hover{border-color:var(--accent);background:rgba(91,143,249,.04)}
85
- .dropzone:hover::before{opacity:1;animation:shimmer 2s infinite}
86
- .dropzone.drag-over{border-color:var(--accent);background:rgba(91,143,249,.08);transform:scale(1.01)}
87
- .dz-icon{font-size:2.2rem;margin-bottom:10px;display:block}
88
- .dz-label{font-size:.84rem;color:var(--sub);line-height:1.5}
 
 
 
 
89
  .dz-label strong{color:var(--accent)}
90
- .dz-hint{font-size:.72rem;color:var(--muted);margin-top:5px}
91
-
92
- /* ── URL INPUT ────────────────────────────────────── */
93
- .url-row{display:flex;gap:8px;margin-bottom:10px}
94
- .url-row input{flex:1;background:var(--card);border:1px solid var(--border2);
95
- border-radius:8px;padding:9px 12px;color:var(--text);font-size:.84rem;
96
- font-family:inherit;outline:none;transition:border-color .18s}
97
- .url-row input:focus{border-color:var(--accent)}
98
-
99
- /* ── SOURCE LOADED CARD ──────────────────────────── */
100
- #source-card{
101
- display:none;margin-top:16px;background:var(--card);
102
- border:1px solid rgba(34,212,122,.2);border-radius:10px;padding:13px 14px;
103
- animation:fadeUp .25s ease;
104
- }
105
- .sc-row{display:flex;align-items:flex-start;gap:10px}
106
- .sc-icon{width:34px;height:34px;background:rgba(34,212,122,.12);border-radius:8px;
107
- flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:16px}
108
- .sc-info{flex:1;min-width:0}
109
- .sc-name{font-size:.82rem;font-weight:600;color:var(--text);
110
- overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-bottom:3px}
111
  .sc-meta{font-size:.72rem;color:var(--teal)}
112
- .sc-ready{display:inline-flex;align-items:center;gap:5px;margin-top:8px;
113
- font-size:.7rem;font-weight:700;color:var(--green);
114
- background:rgba(34,212,122,.08);border:1px solid rgba(34,212,122,.18);
115
- border-radius:5px;padding:3px 8px}
116
-
117
- /* ── MESSAGES ────────────────────────────────────── */
118
- .msg{border-radius:8px;padding:10px 13px;font-size:.79rem;
119
- margin-top:10px;line-height:1.55;animation:fadeUp .2s ease}
120
- .msg-ok{background:rgba(34,212,122,.07);border:1px solid rgba(34,212,122,.2);color:var(--green)}
121
- .msg-err{background:rgba(240,92,92,.07);border:1px solid rgba(240,92,92,.2);color:var(--red)}
122
- .msg-info{color:var(--muted);font-size:.76rem;margin-top:8px}
123
-
124
- /* ── BUTTONS ─────────────────────────────────────── */
125
- .btn{display:inline-flex;align-items:center;gap:7px;padding:9px 20px;
126
- border-radius:8px;border:none;font-size:.84rem;font-weight:600;
127
- cursor:pointer;transition:all .18s;font-family:inherit}
128
- .btn-primary{background:linear-gradient(135deg,var(--accent),var(--accent2));
129
- color:#fff;box-shadow:0 2px 8px rgba(91,143,249,.25)}
130
- .btn-primary:hover{transform:translateY(-1px);box-shadow:0 4px 14px rgba(91,143,249,.35)}
131
- .btn-primary:active{transform:none}
132
- .btn-primary:disabled{opacity:.5;cursor:default;transform:none;box-shadow:none}
133
- .btn-sm{padding:7px 14px;font-size:.78rem}
134
- .btn-ghost{background:rgba(255,255,255,.06);border:1px solid var(--border2);
135
- color:var(--sub);font-size:.74rem;padding:5px 12px;border-radius:6px}
136
- .btn-ghost:hover{background:rgba(255,255,255,.1);color:var(--text)}
137
- .spinner{width:13px;height:13px;border:2px solid rgba(255,255,255,.3);
138
- border-top-color:#fff;border-radius:50%;animation:spin .65s linear infinite;flex-shrink:0}
139
-
140
- /* ── QUESTION AREA ───────────────────────────────── */
141
- .q-card{background:var(--card);border:1px solid var(--border2);
142
- border-radius:11px;padding:16px;margin-bottom:16px}
143
- .q-label{font-size:.7rem;font-weight:700;color:var(--muted);text-transform:uppercase;
144
- letter-spacing:.06em;margin-bottom:8px}
145
- textarea{width:100%;background:transparent;border:none;color:var(--text);
146
- font-size:.9rem;font-family:inherit;outline:none;resize:none;
147
- line-height:1.6;min-height:72px}
148
- textarea::placeholder{color:var(--muted)}
149
- .q-footer{display:flex;align-items:center;gap:10px;padding-top:12px;
150
- border-top:1px solid var(--border)}
151
- #q-err{font-size:.77rem;color:var(--red)}
152
-
153
- /* ── PIPELINE ────────────────────────────────────── */
154
- #pipeline{display:none;margin-bottom:14px;animation:fadeUp .2s ease}
155
- .pipe-row{display:flex;align-items:center;gap:0;background:var(--card);
156
- border:1px solid var(--border2);border-radius:10px;padding:10px 14px;
157
- overflow-x:auto}
158
- .pipe-step{display:flex;align-items:center;gap:5px;font-size:.72rem;font-weight:600;
159
- color:var(--muted);white-space:nowrap;padding:4px 8px;border-radius:6px;
160
- transition:all .2s}
161
- .pipe-step.active{color:var(--accent);background:rgba(91,143,249,.12)}
162
- .pipe-step.done{color:var(--green)}
163
- .pipe-step .step-dot{width:6px;height:6px;border-radius:50%;background:currentColor;flex-shrink:0}
164
- .pipe-step.active .step-dot{animation:pulse .9s ease infinite}
165
- .pipe-arrow{color:var(--border2);font-size:.8rem;padding:0 2px;flex-shrink:0}
166
-
167
- /* ── TRACE LOG ───────────────────────────────────── */
168
- #trace-wrap{display:none;margin-bottom:16px;animation:fadeUp .2s ease}
169
- .trace-hdr{display:flex;align-items:center;gap:8px;margin-bottom:8px}
170
- .trace-title{font-size:.7rem;font-weight:700;text-transform:uppercase;
171
- letter-spacing:.07em;color:var(--muted)}
172
- .trace-box{background:var(--card);border:1px solid var(--border2);border-radius:10px;
173
- padding:4px 0;max-height:220px;overflow-y:auto}
174
- .t-step{display:flex;align-items:flex-start;gap:10px;padding:8px 14px;
175
- border-bottom:1px solid var(--border);animation:fadeUp .18s ease;transition:background .15s}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  .t-step:last-child{border-bottom:none}
177
- .t-step:hover{background:rgba(255,255,255,.02)}
178
- .t-icon{font-size:13px;flex-shrink:0;margin-top:1px;width:18px;text-align:center}
179
- .t-agent{font-size:.64rem;font-weight:800;text-transform:uppercase;letter-spacing:.05em;
180
- padding:2px 7px;border-radius:4px;flex-shrink:0;white-space:nowrap}
181
- .a-planner {background:rgba(91,143,249,.14);color:var(--accent)}
182
- .a-retriever{background:rgba(41,198,212,.14);color:var(--teal)}
183
- .a-grader {background:rgba(245,166,35,.14);color:var(--gold)}
184
- .a-generator{background:rgba(34,212,122,.14);color:var(--green)}
185
- .a-critic {background:rgba(167,139,250,.14);color:var(--purple)}
186
- .a-error {background:rgba(240,92,92,.14);color:var(--red)}
187
- .t-msg{flex:1;font-size:.78rem;color:var(--sub);line-height:1.5}
188
- .t-lat{font-size:.65rem;color:rgba(128,136,160,.5);flex-shrink:0;margin-top:2px}
189
-
190
- /* ── ANSWER CARD ─────────────────────────────────── */
191
- #answer-wrap{display:none;background:var(--card);border:1px solid var(--border2);
192
- border-radius:12px;overflow:hidden;animation:fadeUp .3s ease}
193
- .ans-header{display:flex;align-items:center;justify-content:space-between;
194
- padding:13px 18px 12px;border-bottom:1px solid var(--border);
195
- background:rgba(255,255,255,.02)}
196
- .ans-title{display:flex;align-items:center;gap:8px;font-size:.72rem;font-weight:800;
197
- text-transform:uppercase;letter-spacing:.08em;color:var(--sub)}
198
- .ans-title-icon{width:22px;height:22px;background:rgba(34,212,122,.15);border-radius:6px;
199
- display:flex;align-items:center;justify-content:center;font-size:11px}
200
- .ans-actions{display:flex;align-items:center;gap:8px}
201
- #verdict-badge{font-size:.68rem;font-weight:700;padding:3px 10px;border-radius:20px}
202
- .v-ok {background:rgba(34,212,122,.1);border:1px solid rgba(34,212,122,.25);color:var(--green)}
203
- .v-warn{background:rgba(245,166,35,.1);border:1px solid rgba(245,166,35,.25);color:var(--gold)}
204
- .ans-body{padding:18px}
205
- #answer-text{font-size:.9rem;line-height:1.78;color:var(--text);
206
- white-space:pre-wrap;word-break:break-word}
207
-
208
- /* ── RESPONSIVE ──────────────────────────────────── */
209
- @media(min-width:1100px){.logo-sub{display:block}}
210
- @media(max-width:768px){
211
- main{grid-template-columns:1fr;height:auto;overflow:visible}
212
- .panel{height:auto}
213
  .panel-left{border-right:none;border-bottom:1px solid var(--border)}
 
 
 
214
  }
215
  </style>
216
  </head>
217
  <body>
218
 
219
- <!-- ── HEADER ── -->
220
  <header>
221
  <a class="logo" href="#">
222
  <div class="logo-icon">&#129504;</div>
223
  <span class="logo-text">Doc<span>Mind</span></span>
224
  </a>
225
- <span class="logo-sub">AI Document Research</span>
226
- <div class="hdr-badges">
227
- <span class="badge badge-model">Qwen 2.5 7B</span>
 
 
228
  </div>
229
- <span class="badge badge-src" id="hdr-src">No source loaded</span>
 
 
 
 
230
  </header>
231
 
232
  <main>
233
- <!-- ── LEFT PANEL: KNOWLEDGE BASE ── -->
234
  <div class="panel panel-left">
235
- <div class="sec-head">
236
- <div class="sec-icon sec-icon-blue">&#128218;</div>
237
- <span class="sec-title">Knowledge Base</span>
238
- </div>
239
-
240
- <div class="tabs">
241
- <button class="tab-btn active" onclick="switchTab(this,'pdf')">&#128196;&ensp;Upload PDF</button>
242
- <button class="tab-btn" onclick="switchTab(this,'url')">&#127760;&ensp;Paste URL</button>
243
- </div>
244
 
245
- <!-- PDF TAB -->
246
- <div id="tab-pdf">
247
- <div class="dropzone" id="dz"
248
- onclick="document.getElementById('fi').click()"
249
- ondragover="dg(event,true)" ondragleave="dg(event,false)" ondrop="dp(event)">
250
- <span class="dz-icon">&#128196;</span>
251
- <div class="dz-label"><strong>Click to browse</strong> or drag &amp; drop</div>
252
- <div class="dz-hint">PDF only &middot; max 10 MB</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  </div>
254
- <input type="file" id="fi" accept=".pdf" style="display:none" onchange="fc(event)"/>
255
- <div id="pdf-msg"></div>
256
  </div>
257
 
258
- <!-- URL TAB -->
259
- <div id="tab-url" style="display:none">
260
- <div class="url-row">
261
- <input type="url" id="url-inp" placeholder="https://en.wikipedia.org/wiki/..."
262
- onkeydown="if(event.key==='Enter')fetchURL()"/>
263
- <button class="btn btn-primary btn-sm" id="url-btn" onclick="fetchURL()">Fetch</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  </div>
265
- <div class="msg-info" style="margin-bottom:4px">Wikipedia, government sites, and docs work best. Some sites block automated access.</div>
266
- <div id="url-msg"></div>
267
  </div>
268
 
269
- <!-- SOURCE LOADED -->
270
- <div id="source-card">
271
- <div class="sc-row">
272
- <div class="sc-icon" id="sc-icon">&#128196;</div>
273
- <div class="sc-info">
274
- <div class="sc-name" id="source-name"></div>
275
- <div class="sc-meta" id="source-chunks"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  </div>
277
  </div>
278
- <div class="sc-ready">&#10003;&ensp;Ready for questions</div>
279
  </div>
 
280
  </div>
281
 
282
- <!-- ── RIGHT PANEL: RESEARCH ── -->
283
  <div class="panel panel-right">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  <div class="sec-head">
285
  <div class="sec-icon sec-icon-purple">&#128269;</div>
286
  <span class="sec-title">Research Query</span>
287
  </div>
288
-
289
  <div class="q-card">
290
  <div class="q-label">Your Question</div>
291
  <textarea id="q-inp" rows="3"
@@ -297,10 +505,10 @@ textarea::placeholder{color:var(--muted)}
297
  </div>
298
  </div>
299
 
300
- <!-- AGENT PIPELINE -->
301
  <div id="pipeline">
302
  <div class="pipe-row">
303
- <div class="pipe-step" id="ps-planner"><span class="step-dot"></span>&#129518;&ensp;Planner</div>
304
  <div class="pipe-arrow">&#8594;</div>
305
  <div class="pipe-step" id="ps-retriever"><span class="step-dot"></span>&#128269;&ensp;Retriever</div>
306
  <div class="pipe-arrow">&#8594;</div>
@@ -317,35 +525,29 @@ textarea::placeholder{color:var(--muted)}
317
  <div class="trace-hdr">
318
  <span class="trace-title">&#128240;&ensp;Agent Trace</span>
319
  </div>
320
- <div class="trace-box" id="trace-log"></div>
321
  </div>
322
 
323
  <!-- ANSWER -->
324
  <div id="answer-wrap">
325
  <div class="ans-header">
326
- <div class="ans-title">
327
- <div class="ans-title-icon">&#128161;</div>
328
- Answer
329
- </div>
330
  <div class="ans-actions">
331
- <span id="verdict-badge"></span>
332
- <button class="btn btn-ghost" id="copy-btn" onclick="copyAns()">&#128203;&ensp;Copy</button>
333
  </div>
334
  </div>
335
- <div class="ans-body">
336
- <div id="answer-text"></div>
337
- </div>
338
  </div>
 
339
  </div>
340
  </main>
341
 
342
  <script>
343
- let pollTimer=null,seen=0;
344
  const esc=s=>String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
 
345
 
346
- const AGENT_ICONS={planner:"&#129518;",retriever:"&#128269;",grader:"&#9878;",generator:"&#9997;",critic:"&#128300;"};
347
-
348
- /* ── TABS ── */
349
  function switchTab(btn,name){
350
  document.querySelectorAll(".tab-btn").forEach(b=>b.classList.remove("active"));
351
  btn.classList.add("active");
@@ -353,64 +555,114 @@ function switchTab(btn,name){
353
  document.getElementById("tab-url").style.display=name==="url"?"":"none";
354
  }
355
 
356
- /* ── DRAG & DROP ── */
357
  function dg(e,over){e.preventDefault();document.getElementById("dz").classList[over?"add":"remove"]("drag-over");}
358
  function dp(e){e.preventDefault();document.getElementById("dz").classList.remove("drag-over");const f=e.dataTransfer.files[0];if(f)up(f);}
359
  function fc(e){if(e.target.files[0])up(e.target.files[0]);}
360
 
361
- /* ── UPLOAD PDF ── */
362
  async function up(file){
363
- if(!file.name.toLowerCase().endsWith(".pdf")){showMsg("pdf-msg","error","Only PDF files are supported.");return;}
364
- showMsg("pdf-msg","info","Uploading "+file.name+"...");
365
  const fd=new FormData();fd.append("file",file);
366
  try{
367
  const r=await fetch("/api/upload",{method:"POST",body:fd});
368
  const d=await r.json();
369
- if(d.error){showMsg("pdf-msg","error",d.error);return;}
370
  setSource(d.filename,d.chunks,"pdf");
371
- showMsg("pdf-msg","ok","&#10003;&ensp;"+d.chunks+" chunks indexed from "+d.filename);
372
- }catch(e){showMsg("pdf-msg","error","Upload failed: "+e.message);}
373
  }
374
 
375
- /* ── FETCH URL ── */
376
  async function fetchURL(){
377
  const url=document.getElementById("url-inp").value.trim();
378
- if(!url){showMsg("url-msg","error","Please enter a URL.");return;}
379
- const btn=document.getElementById("url-btn");
380
- btn.disabled=true;btn.textContent="Fetching...";
381
- showMsg("url-msg","info","Fetching and indexing page...");
382
  try{
383
  const r=await fetch("/api/ingest_url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url})});
384
  const d=await r.json();
385
- if(d.error){showMsg("url-msg","error",d.error);return;}
386
  setSource(d.url,d.chunks,"url");
387
- showMsg("url-msg","ok","&#10003;&ensp;"+d.chunks+" chunks indexed");
388
- }catch(e){showMsg("url-msg","error","Failed: "+e.message);}
389
- finally{btn.disabled=false;btn.textContent="Fetch";}
390
  }
391
 
392
- /* ── SOURCE LOADED ── */
393
  function setSource(name,chunks,type){
394
  document.getElementById("source-name").textContent=name;
395
  document.getElementById("source-chunks").textContent=chunks+" chunks indexed";
396
  document.getElementById("sc-icon").textContent=type==="pdf"?"πŸ“„":"🌐";
397
  document.getElementById("source-card").style.display="block";
398
  const p=document.getElementById("hdr-src");
399
- p.textContent=name.length>30?name.slice(0,30)+"...":name;
400
  p.classList.add("loaded");
401
  }
402
 
403
- /* ── PIPELINE ── */
404
- const PIPE_AGENTS=["planner","retriever","grader","generator","critic"];
405
- function resetPipeline(){PIPE_AGENTS.forEach(a=>{const el=document.getElementById("ps-"+a);if(el){el.classList.remove("active","done");}});}
406
- function setAgent(name,done){
407
- const el=document.getElementById("ps-"+name);
408
- if(!el)return;
409
- if(done){el.classList.remove("active");el.classList.add("done");}
410
- else{el.classList.remove("done");el.classList.add("active");}
 
 
411
  }
412
 
413
- /* ── ASK ── */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  function qk(e){if(e.key==="Enter"&&!e.shiftKey){e.preventDefault();ask();}}
415
 
416
  async function ask(){
@@ -420,101 +672,94 @@ async function ask(){
420
 
421
  const btn=document.getElementById("ask-btn");
422
  btn.disabled=true;
423
- btn.innerHTML="<span class='spinner'></span>&ensp;Thinking...";
424
 
425
  document.getElementById("pipeline").style.display="block";
426
  document.getElementById("trace-wrap").style.display="block";
427
- document.getElementById("trace-log").innerHTML="<div class='t-step'><span class='t-msg' style='color:var(--muted);font-style:italic'>Initialising agents...</span></div>";
 
428
  document.getElementById("answer-wrap").style.display="none";
429
- resetPipeline();
430
- seen=0;clearInterval(pollTimer);
431
 
432
  try{
433
- const r=await fetch("/api/research",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({question:q})});
 
 
 
434
  const d=await r.json();
435
- if(d.error){traceErr(d.error);resetAskBtn();return;}
436
  pollTimer=setInterval(()=>poll(d.query_id),1500);
437
- }catch(e){traceErr("Network error: "+e.message);resetAskBtn();}
438
  }
439
 
440
- function resetAskBtn(){
441
  const btn=document.getElementById("ask-btn");
442
  btn.disabled=false;
443
- btn.innerHTML="&#9889;&ensp;Ask";
444
  }
445
 
446
- /* ── POLL ── */
447
  async function poll(qid){
448
  try{
449
  const r=await fetch("/api/trace/"+qid);
450
- if(!r.ok){traceErr("Server error "+r.status);clearInterval(pollTimer);resetAskBtn();return;}
451
  const d=await r.json();
452
  renderTrace(d.trace||[]);
453
  if(["complete","error"].includes(d.status)){
454
- clearInterval(pollTimer);
455
- resetAskBtn();
456
- if(d.status==="complete"&&d.result)renderAnswer(d.result);
457
- else if(d.status==="error"&&d.result)traceErr(d.result.error||"An error occurred.");
458
  }
459
- }catch(e){traceErr("Poll error: "+e.message);clearInterval(pollTimer);resetAskBtn();}
 
 
 
 
 
 
 
460
  }
461
 
462
- /* ── TRACE ── */
463
  function renderTrace(steps){
464
  if(!steps.length)return;
465
  const log=document.getElementById("trace-log");
466
  if(seen===0)log.innerHTML="";
467
  for(let i=seen;i<steps.length;i++){
468
  const s=steps[i];
469
- const icon=AGENT_ICONS[s.agent]||"&#9679;";
470
- const lat=s.latency_ms>0?"<span class='t-lat'>"+s.latency_ms+"ms</span>":"";
471
- const cls="a-"+(["planner","retriever","grader","generator","critic"].includes(s.agent)?s.agent:"error");
472
- log.innerHTML+="<div class='t-step'>"
473
- +"<span class='t-icon'>"+icon+"</span>"
474
- +"<span class='t-agent "+cls+"'>"+s.agent+"</span>"
475
- +"<span class='t-msg'>"+esc(s.message)+"</span>"
476
- +lat+"</div>";
477
- if(s.status==="running")setAgent(s.agent,false);
478
- else if(s.status==="complete")setAgent(s.agent,true);
479
  }
480
- seen=steps.length;
481
- log.scrollTop=log.scrollHeight;
482
- }
483
-
484
- function traceErr(msg){
485
- const log=document.getElementById("trace-log");
486
- log.innerHTML+="<div class='t-step'><span class='t-icon'>&#10060;</span><span class='t-agent a-error'>error</span><span class='t-msg' style='color:var(--red)'>"+esc(msg)+"</span></div>";
487
- log.scrollTop=log.scrollHeight;
488
  }
489
 
490
- /* ── ANSWER ── */
491
  function renderAnswer(result){
492
  document.getElementById("answer-wrap").style.display="block";
493
  document.getElementById("answer-text").textContent=result.generation||"No answer generated.";
494
- const vb=document.getElementById("verdict-badge");
495
- if(result.verdict==="APPROVED"){vb.className="v-ok";vb.textContent="&#10003; High confidence";}
496
- else if(result.verdict){vb.className="v-warn";vb.textContent="&#9888; Verify with source";}
497
- else{vb.textContent="";}
498
- document.getElementById("answer-wrap").scrollIntoView({behavior:"smooth",block:"start"});
499
  }
500
 
501
- /* ── COPY ── */
502
- async function copyAns(){
503
  const text=document.getElementById("answer-text").textContent;
504
- try{
505
- await navigator.clipboard.writeText(text);
506
- const btn=document.getElementById("copy-btn");
507
- btn.innerHTML="&#10003;&ensp;Copied!";
508
- setTimeout(()=>{btn.innerHTML="&#128203;&ensp;Copy";},2000);
509
- }catch(e){}
510
  }
511
 
512
- /* ── MSG HELPER ── */
513
- function showMsg(id,type,msg){
514
  const el=document.getElementById(id);
515
- if(type==="ok")el.innerHTML="<div class='msg msg-ok'>"+msg+"</div>";
516
- else if(type==="error")el.innerHTML="<div class='msg msg-err'>"+esc(msg)+"</div>";
517
- else el.innerHTML="<div class='msg-info'>"+esc(msg)+"</div>";
518
  }
519
  </script>
520
  </body>
 
17
  body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
18
  background:var(--bg);color:var(--text);font-size:14px;line-height:1.5}
19
 
20
+ /* ── ANIMATIONS ── */
21
  @keyframes spin{to{transform:rotate(360deg)}}
22
+ @keyframes fadeUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
23
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
24
+ @keyframes nodeGlow{0%,100%{box-shadow:0 0 0 rgba(91,143,249,0)}50%{box-shadow:0 0 14px rgba(91,143,249,.6)}}
25
+ @keyframes flowLine{0%{stroke-dashoffset:20}100%{stroke-dashoffset:0}}
26
  @keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}
27
 
28
+ /* ── HEADER ── */
29
+ header{background:var(--surface);border-bottom:1px solid var(--border);
30
+ padding:0 20px;height:54px;display:flex;align-items:center;gap:10px;
31
+ position:sticky;top:0;z-index:100;overflow:hidden}
32
+ .logo{display:flex;align-items:center;gap:8px;text-decoration:none;flex-shrink:0}
33
+ .logo-icon{width:28px;height:28px;background:linear-gradient(135deg,var(--accent),var(--purple));
34
+ border-radius:7px;display:flex;align-items:center;justify-content:center;font-size:14px}
35
+ .logo-text{font-size:.95rem;font-weight:800;color:var(--text);letter-spacing:-.3px}
 
 
 
 
 
36
  .logo-text span{color:var(--accent)}
37
+ .hdr-stack{display:flex;gap:5px;align-items:center;margin-left:6px;flex-wrap:nowrap;overflow:hidden}
38
+ .hs-badge{font-size:.62rem;font-weight:700;padding:2px 8px;border-radius:12px;
39
+ border:1px solid;white-space:nowrap;letter-spacing:.02em}
40
+ .hs-lg{background:rgba(91,143,249,.1);border-color:rgba(91,143,249,.3);color:var(--accent)}
41
+ .hs-lc{background:rgba(245,166,35,.1);border-color:rgba(245,166,35,.3);color:var(--gold)}
42
+ .hs-fb{background:rgba(41,198,212,.1);border-color:rgba(41,198,212,.3);color:var(--teal)}
43
+ .hs-qw{background:rgba(34,212,122,.1);border-color:rgba(34,212,122,.3);color:var(--green);cursor:pointer}
44
+ .hs-qw:hover{background:rgba(34,212,122,.18)}
45
+ #hdr-src{margin-left:auto;font-size:.68rem;padding:3px 10px;border-radius:14px;flex-shrink:0;
46
+ background:rgba(120,128,160,.08);border:1px solid var(--border);color:var(--muted)}
47
+ #hdr-src.loaded{background:rgba(34,212,122,.08);border-color:rgba(34,212,122,.25);color:var(--green)}
48
+
49
+ /* ── LAYOUT ── */
50
+ main{display:grid;grid-template-columns:310px 1fr;height:calc(100vh - 54px);overflow:hidden}
51
+ .panel{padding:18px 16px;overflow-y:auto;height:100%}
52
+ .panel-left{border-right:1px solid var(--border);background:var(--surface);display:flex;flex-direction:column;gap:18px}
53
  .panel-right{background:var(--bg)}
54
 
55
+ /* ── SECTION HEADERS ── */
56
+ .sec-head{display:flex;align-items:center;gap:8px;margin-bottom:12px}
57
+ .sec-icon{width:24px;height:24px;border-radius:6px;display:flex;align-items:center;
58
+ justify-content:center;font-size:12px;flex-shrink:0}
59
  .sec-icon-blue{background:rgba(91,143,249,.15)}
60
+ .sec-icon-green{background:rgba(34,212,122,.15)}
61
+ .sec-icon-gold{background:rgba(245,166,35,.15)}
62
  .sec-icon-purple{background:rgba(167,139,250,.15)}
63
+ .sec-title{font-size:.68rem;font-weight:800;text-transform:uppercase;letter-spacing:.09em;color:var(--sub)}
 
 
 
 
 
 
 
 
 
 
64
 
65
+ /* ── TABS ── */
66
+ .tabs{display:flex;gap:2px;background:rgba(255,255,255,.03);border-radius:8px;
67
+ padding:3px;margin-bottom:14px;border:1px solid var(--border)}
68
+ .tab-btn{flex:1;background:none;border:none;color:var(--muted);font-size:.76rem;
69
+ font-weight:600;padding:6px 8px;border-radius:6px;cursor:pointer;
70
+ transition:all .15s;font-family:inherit;display:flex;align-items:center;
71
+ justify-content:center;gap:5px}
72
+ .tab-btn.active{background:var(--card2);color:var(--text);box-shadow:0 1px 5px rgba(0,0,0,.3)}
73
+
74
+ /* ── DROPZONE ── */
75
+ .dropzone{border:2px dashed var(--border2);border-radius:10px;padding:22px 14px;
76
+ text-align:center;cursor:pointer;transition:all .22s;position:relative;overflow:hidden}
77
+ .dropzone::before{content:"";position:absolute;inset:0;
78
+ background:linear-gradient(90deg,transparent,rgba(91,143,249,.06),transparent);
79
+ background-size:200% 100%;opacity:0;transition:opacity .3s}
80
+ .dropzone:hover::before,.dropzone.drag-over::before{opacity:1;animation:shimmer 1.6s linear infinite}
81
+ .dropzone:hover,.dropzone.drag-over{border-color:var(--accent);background:rgba(91,143,249,.04)}
82
+ .dz-icon{font-size:1.8rem;margin-bottom:6px;display:block;line-height:1}
83
+ .dz-label{font-size:.8rem;color:var(--sub);margin-bottom:3px}
84
  .dz-label strong{color:var(--accent)}
85
+ .dz-hint{font-size:.72rem;color:var(--muted)}
86
+
87
+ /* ── URL ── */
88
+ .url-row{display:flex;gap:8px;margin-bottom:6px}
89
+ input[type=url]{flex:1;background:rgba(255,255,255,.04);border:1px solid var(--border);
90
+ border-radius:7px;padding:8px 11px;color:var(--text);font-size:.82rem;
91
+ font-family:inherit;outline:none;transition:border-color .2s}
92
+ input[type=url]:focus{border-color:var(--accent)}
93
+
94
+ /* ── SOURCE CARD ── */
95
+ #source-card{display:none;background:rgba(34,212,122,.06);border:1px solid rgba(34,212,122,.2);
96
+ border-radius:9px;padding:10px 12px;animation:fadeUp .3s ease}
97
+ .sc-row{display:flex;align-items:center;gap:10px;margin-bottom:5px}
98
+ .sc-icon{font-size:1.4rem;flex-shrink:0}
99
+ .sc-name{font-size:.82rem;font-weight:700;color:var(--text);overflow:hidden;
100
+ text-overflow:ellipsis;white-space:nowrap;max-width:200px}
 
 
 
 
 
101
  .sc-meta{font-size:.72rem;color:var(--teal)}
102
+ .sc-ready{font-size:.72rem;color:var(--green);font-weight:600}
103
+
104
+ /* ── MODEL SELECTOR ── */
105
+ .model-list{display:flex;flex-direction:column;gap:7px}
106
+ .model-card{border:1px solid var(--border2);border-radius:9px;padding:10px 12px;
107
+ cursor:pointer;transition:all .18s;display:flex;align-items:center;gap:10px}
108
+ .model-card:hover{border-color:rgba(91,143,249,.35);background:rgba(91,143,249,.04);
109
+ transform:translateX(2px)}
110
+ .model-card.selected{border-color:var(--accent);background:rgba(91,143,249,.08)}
111
+ .mc-color{width:10px;height:10px;border-radius:50%;flex-shrink:0}
112
+ .mc-body{flex:1;min-width:0}
113
+ .mc-name{font-size:.8rem;font-weight:700;color:var(--text);margin-bottom:1px}
114
+ .mc-desc{font-size:.69rem;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
115
+ .mc-right{display:flex;flex-direction:column;align-items:flex-end;gap:3px;flex-shrink:0}
116
+ .mc-params{font-size:.65rem;font-weight:700;padding:1px 6px;border-radius:5px;
117
+ background:rgba(255,255,255,.06);color:var(--sub)}
118
+ .mc-speed{font-size:.65rem;letter-spacing:1px}
119
+ .mc-check{font-size:.75rem;color:var(--green);opacity:0;transition:opacity .15s}
120
+ .model-card.selected .mc-check{opacity:1}
121
+
122
+ /* ── TECH STACK GRID ── */
123
+ .tech-grid{display:grid;grid-template-columns:1fr 1fr;gap:5px}
124
+ .tg-badge{border-radius:7px;padding:7px 9px;border:1px solid;display:flex;
125
+ align-items:center;gap:6px;transition:.15s}
126
+ .tg-badge:hover{transform:translateY(-1px)}
127
+ .tg-icon{font-size:.85rem;flex-shrink:0}
128
+ .tg-body{}
129
+ .tg-name{font-size:.7rem;font-weight:700;line-height:1.2}
130
+ .tg-sub{font-size:.6rem;color:var(--muted);line-height:1.2}
131
+ .tg-lg{background:rgba(91,143,249,.07);border-color:rgba(91,143,249,.2);color:var(--accent)}
132
+ .tg-lc{background:rgba(245,166,35,.07);border-color:rgba(245,166,35,.2);color:var(--gold)}
133
+ .tg-fb{background:rgba(41,198,212,.07);border-color:rgba(41,198,212,.2);color:var(--teal)}
134
+ .tg-emb{background:rgba(34,212,122,.07);border-color:rgba(34,212,122,.2);color:var(--green)}
135
+ .tg-fl{background:rgba(239,68,68,.07);border-color:rgba(239,68,68,.2);color:#f87171}
136
+ .tg-dk{background:rgba(6,182,212,.07);border-color:rgba(6,182,212,.2);color:#38bdf8}
137
+
138
+ /* ── ARCHITECTURE DIAGRAM ── */
139
+ .arch-card{background:var(--card);border:1px solid var(--border);border-radius:12px;
140
+ padding:14px 16px;margin-bottom:16px}
141
+ .arch-card-hdr{display:flex;align-items:center;gap:8px;margin-bottom:12px}
142
+ .arch-legend{display:flex;gap:10px;flex-wrap:wrap;margin-left:auto}
143
+ .arch-legend-item{font-size:.6rem;color:var(--muted);display:flex;align-items:center;gap:4px}
144
+ .arch-legend-dot{width:7px;height:7px;border-radius:50%}
145
+
146
+ /* ingestion row */
147
+ .arch-ingest-row{display:flex;align-items:center;gap:6px;margin-bottom:8px;flex-wrap:wrap}
148
+ /* pipeline row */
149
+ .arch-pipe-row{display:flex;align-items:center;gap:4px;flex-wrap:wrap}
150
+ .arch-node{border-radius:8px;padding:7px 10px;border:1px solid;text-align:center;
151
+ min-width:74px;transition:all .3s;position:relative}
152
+ .arch-node-icon{font-size:.95rem;display:block;margin-bottom:2px;line-height:1}
153
+ .arch-node-name{font-size:.68rem;font-weight:700;line-height:1.2}
154
+ .arch-node-sub{font-size:.57rem;color:var(--muted);line-height:1.3;margin-top:1px}
155
+ /* node types */
156
+ .an-io{background:rgba(41,198,212,.07);border-color:rgba(41,198,212,.25);color:var(--teal)}
157
+ .an-chunker{background:rgba(245,166,35,.07);border-color:rgba(245,166,35,.25);color:var(--gold)}
158
+ .an-index{background:rgba(41,198,212,.07);border-color:rgba(41,198,212,.25);color:var(--teal)}
159
+ .an-llm{background:rgba(91,143,249,.07);border-color:rgba(91,143,249,.25);color:var(--accent)}
160
+ .an-local{background:rgba(34,212,122,.07);border-color:rgba(34,212,122,.25);color:var(--green)}
161
+ .an-score{background:rgba(245,166,35,.07);border-color:rgba(245,166,35,.25);color:var(--gold)}
162
+ .an-out{background:rgba(167,139,250,.07);border-color:rgba(167,139,250,.25);color:var(--purple)}
163
+ /* active / done states */
164
+ .arch-node.an-running{animation:nodeGlow .9s ease-in-out infinite;border-width:2px}
165
+ .arch-node.an-done{opacity:.55}
166
+ .arch-arr{color:var(--muted);font-size:.75rem;flex-shrink:0;padding:0 1px}
167
+ .arch-vconn{display:flex;align-items:center;justify-content:flex-start;
168
+ padding-left:36px;margin:4px 0;color:var(--muted);font-size:.75rem}
169
+
170
+ /* ── QUESTION CARD ── */
171
+ .q-card{background:var(--card);border:1px solid var(--border);border-radius:10px;
172
+ padding:14px;margin-bottom:14px}
173
+ .q-label{font-size:.68rem;font-weight:700;color:var(--muted);text-transform:uppercase;
174
+ letter-spacing:.07em;margin-bottom:8px}
175
+ textarea{width:100%;background:rgba(255,255,255,.03);border:1px solid var(--border);
176
+ border-radius:7px;padding:9px 11px;color:var(--text);font-size:.84rem;
177
+ font-family:inherit;outline:none;resize:vertical;min-height:72px;
178
+ transition:border-color .2s;line-height:1.5}
179
+ textarea:focus{border-color:var(--accent)}
180
+ .q-footer{display:flex;align-items:center;gap:10px;margin-top:10px}
181
+
182
+ /* ── BUTTONS ── */
183
+ .btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:7px;
184
+ border:none;font-size:.82rem;font-weight:600;cursor:pointer;
185
+ transition:all .15s;font-family:inherit}
186
+ .btn-primary{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;
187
+ box-shadow:0 2px 10px rgba(91,143,249,.3)}
188
+ .btn-primary:hover{transform:translateY(-1px);box-shadow:0 4px 16px rgba(91,143,249,.4)}
189
+ .btn-primary:disabled{opacity:.45;cursor:default;transform:none;box-shadow:none}
190
+ .btn-sm{padding:6px 12px;font-size:.76rem}
191
+ .btn-ghost{background:rgba(255,255,255,.05);color:var(--sub);border:1px solid var(--border)}
192
+ .btn-ghost:hover{background:rgba(255,255,255,.09);color:var(--text)}
193
+ .spinner{width:14px;height:14px;border:2px solid rgba(255,255,255,.3);
194
+ border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite}
195
+
196
+ /* ── PIPELINE ── */
197
+ #pipeline{display:none;margin-bottom:12px}
198
+ .pipe-row{display:flex;align-items:center;gap:4px;flex-wrap:wrap;
199
+ background:var(--card);border:1px solid var(--border);
200
+ border-radius:9px;padding:9px 12px}
201
+ .pipe-step{display:flex;align-items:center;gap:5px;font-size:.75rem;font-weight:600;
202
+ color:var(--muted);padding:4px 8px;border-radius:6px;transition:all .2s}
203
+ .pipe-step.ps-active{color:var(--accent);background:rgba(91,143,249,.1)}
204
+ .pipe-step.ps-done{color:var(--green);opacity:.7}
205
+ .step-dot{width:7px;height:7px;border-radius:50%;background:currentColor;flex-shrink:0}
206
+ .ps-active .step-dot{animation:pulse .6s ease-in-out infinite}
207
+ .pipe-arrow{color:var(--border2);font-size:.65rem}
208
+
209
+ /* ── MESSAGES ── */
210
+ .msg{border-radius:7px;padding:8px 12px;font-size:.78rem;margin-top:8px;line-height:1.5;
211
+ animation:fadeUp .25s ease}
212
+ .msg-ok{background:rgba(34,212,122,.08);border:1px solid rgba(34,212,122,.2);color:var(--green)}
213
+ .msg-err{background:rgba(240,92,92,.08);border:1px solid rgba(240,92,92,.2);color:var(--red)}
214
+ .msg-info{color:var(--muted);font-size:.74rem;margin-top:6px}
215
+
216
+ /* ── TRACE ── */
217
+ #trace-wrap{display:none;margin-bottom:14px}
218
+ .trace-hdr{display:flex;align-items:center;gap:8px;padding:8px 12px;
219
+ background:var(--card);border:1px solid var(--border);
220
+ border-radius:9px 9px 0 0;border-bottom-color:transparent}
221
+ .trace-title{font-size:.72rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em}
222
+ #trace-log{background:rgba(0,0,0,.2);border:1px solid var(--border);border-top:none;
223
+ border-radius:0 0 9px 9px;padding:8px 10px;max-height:200px;overflow-y:auto}
224
+ .t-step{display:flex;align-items:flex-start;gap:8px;font-size:.75rem;
225
+ padding:5px 0;border-bottom:1px solid rgba(255,255,255,.04);animation:fadeUp .2s ease}
226
  .t-step:last-child{border-bottom:none}
227
+ .t-badge{font-size:.58rem;font-weight:800;text-transform:uppercase;padding:2px 6px;
228
+ border-radius:4px;flex-shrink:0;margin-top:1px;letter-spacing:.03em}
229
+ .b-planner{background:rgba(91,143,249,.15);color:var(--accent)}
230
+ .b-retriever{background:rgba(41,198,212,.15);color:var(--teal)}
231
+ .b-grader{background:rgba(245,166,35,.15);color:var(--gold)}
232
+ .b-generator{background:rgba(34,212,122,.15);color:var(--green)}
233
+ .b-critic{background:rgba(167,139,250,.15);color:var(--purple)}
234
+ .b-error{background:rgba(240,92,92,.15);color:var(--red)}
235
+ .t-msg{flex:1;color:var(--sub);line-height:1.45}
236
+ .t-lat{color:rgba(120,128,160,.5);font-size:.62rem;white-space:nowrap;margin-left:4px}
237
+
238
+ /* ── ANSWER ── */
239
+ #answer-wrap{display:none;background:var(--card);border:1px solid var(--border);
240
+ border-radius:10px;overflow:hidden;animation:fadeUp .35s ease}
241
+ .ans-header{display:flex;align-items:center;gap:10px;padding:12px 16px;
242
+ border-bottom:1px solid var(--border)}
243
+ .ans-label{font-size:.68rem;font-weight:800;text-transform:uppercase;letter-spacing:.07em;color:var(--muted)}
244
+ .ans-actions{margin-left:auto;display:flex;gap:6px;align-items:center}
245
+ #answer-text{padding:16px;font-size:.88rem;line-height:1.75;white-space:pre-wrap;
246
+ word-break:break-word;color:var(--text)}
247
+ #verdict{margin:0 16px 12px;font-size:.72rem;font-weight:700;padding:4px 12px;
248
+ border-radius:5px;display:inline-block}
249
+ .v-ok{background:rgba(34,212,122,.12);color:var(--green)}
250
+ .v-warn{background:rgba(245,166,35,.12);color:var(--gold)}
251
+
252
+ /* ── Q ERROR ── */
253
+ #q-err{font-size:.76rem;color:var(--red)}
254
+
255
+ @media(max-width:800px){
256
+ main{grid-template-columns:1fr;height:auto}
 
 
 
 
 
 
257
  .panel-left{border-right:none;border-bottom:1px solid var(--border)}
258
+ .hdr-stack{display:none}
259
+ .arch-pipe-row{gap:3px}
260
+ .arch-node{min-width:60px;padding:5px 6px}
261
  }
262
  </style>
263
  </head>
264
  <body>
265
 
 
266
  <header>
267
  <a class="logo" href="#">
268
  <div class="logo-icon">&#129504;</div>
269
  <span class="logo-text">Doc<span>Mind</span></span>
270
  </a>
271
+ <div class="hdr-stack">
272
+ <span class="hs-badge hs-lg">&#128279; LangGraph</span>
273
+ <span class="hs-badge hs-lc">&#9961; LangChain LCEL</span>
274
+ <span class="hs-badge hs-fb">&#128451; FAISS+BM25</span>
275
+ <span class="hs-badge hs-qw" id="hdr-model-badge">&#9889; Qwen 2.5&#xB7;7B</span>
276
  </div>
277
+ <span class="badge" id="hdr-src"
278
+ style="margin-left:auto;font-size:.68rem;padding:3px 10px;border-radius:14px;
279
+ background:rgba(120,128,160,.08);border:1px solid #252836;color:#7880a0">
280
+ No source loaded
281
+ </span>
282
  </header>
283
 
284
  <main>
285
+ <!-- ══════════ LEFT PANEL ══════════ -->
286
  <div class="panel panel-left">
 
 
 
 
 
 
 
 
 
287
 
288
+ <!-- KNOWLEDGE BASE -->
289
+ <div>
290
+ <div class="sec-head">
291
+ <div class="sec-icon sec-icon-blue">&#128218;</div>
292
+ <span class="sec-title">Knowledge Base</span>
293
+ </div>
294
+ <div class="tabs">
295
+ <button class="tab-btn active" onclick="switchTab(this,'pdf')">&#128196;&ensp;Upload PDF</button>
296
+ <button class="tab-btn" onclick="switchTab(this,'url')">&#127760;&ensp;Paste URL</button>
297
+ </div>
298
+ <div id="tab-pdf">
299
+ <div class="dropzone" id="dz"
300
+ onclick="document.getElementById('fi').click()"
301
+ ondragover="dg(event,true)" ondragleave="dg(event,false)" ondrop="dp(event)">
302
+ <span class="dz-icon">&#128196;</span>
303
+ <div class="dz-label"><strong>Click to browse</strong> or drag &amp; drop</div>
304
+ <div class="dz-hint">PDF only &middot; max 10 MB</div>
305
+ </div>
306
+ <input type="file" id="fi" accept=".pdf" style="display:none" onchange="fc(event)"/>
307
+ <div id="pdf-msg"></div>
308
+ </div>
309
+ <div id="tab-url" style="display:none">
310
+ <div class="url-row">
311
+ <input type="url" id="url-inp" placeholder="https://en.wikipedia.org/wiki/..."
312
+ onkeydown="if(event.key==='Enter')fetchURL()"/>
313
+ <button class="btn btn-primary btn-sm" id="url-btn" onclick="fetchURL()">Fetch</button>
314
+ </div>
315
+ <div class="msg-info" style="margin-bottom:4px">Wikipedia, gov sites &amp; docs work best.</div>
316
+ <div id="url-msg"></div>
317
+ </div>
318
+ <div id="source-card">
319
+ <div class="sc-row">
320
+ <div class="sc-icon" id="sc-icon">&#128196;</div>
321
+ <div class="sc-info">
322
+ <div class="sc-name" id="source-name"></div>
323
+ <div class="sc-meta" id="source-chunks"></div>
324
+ </div>
325
+ </div>
326
+ <div class="sc-ready">&#10003;&ensp;Ready for questions</div>
327
  </div>
 
 
328
  </div>
329
 
330
+ <!-- MODEL SELECTOR -->
331
+ <div>
332
+ <div class="sec-head">
333
+ <div class="sec-icon sec-icon-gold">&#129302;</div>
334
+ <span class="sec-title">Select Model</span>
335
+ </div>
336
+ <div class="model-list" id="model-list">
337
+ <div class="model-card selected" onclick="selectModel('qwen-7b',this)"
338
+ data-model="qwen-7b" data-label="Qwen 2.5&#xB7;7B">
339
+ <div class="mc-color" style="background:#5b8ff9"></div>
340
+ <div class="mc-body">
341
+ <div class="mc-name">Qwen 2.5 &middot; 7B</div>
342
+ <div class="mc-desc">Default &middot; fast &amp; free</div>
343
+ </div>
344
+ <div class="mc-right">
345
+ <span class="mc-params">7B</span>
346
+ <span class="mc-speed" style="color:#f5a623">&#9889;&#9889;&#9889;</span>
347
+ </div>
348
+ <span class="mc-check">&#10003;</span>
349
+ </div>
350
+ <div class="model-card" onclick="selectModel('mistral-nemo',this)"
351
+ data-model="mistral-nemo" data-label="Mistral Nemo&#xB7;12B">
352
+ <div class="mc-color" style="background:#a78bfa"></div>
353
+ <div class="mc-body">
354
+ <div class="mc-name">Mistral Nemo &middot; 12B</div>
355
+ <div class="mc-desc">Stronger reasoning</div>
356
+ </div>
357
+ <div class="mc-right">
358
+ <span class="mc-params">12B</span>
359
+ <span class="mc-speed" style="color:#f5a623">&#9889;&#9889;</span>
360
+ </div>
361
+ <span class="mc-check">&#10003;</span>
362
+ </div>
363
+ <div class="model-card" onclick="selectModel('phi-3-mini',this)"
364
+ data-model="phi-3-mini" data-label="Phi-3.5 Mini&#xB7;3.8B">
365
+ <div class="mc-color" style="background:#22d47a"></div>
366
+ <div class="mc-body">
367
+ <div class="mc-name">Phi-3.5 Mini &middot; 3.8B</div>
368
+ <div class="mc-desc">Ultra-fast &amp; focused</div>
369
+ </div>
370
+ <div class="mc-right">
371
+ <span class="mc-params">3.8B</span>
372
+ <span class="mc-speed" style="color:#f5a623">&#9889;&#9889;&#9889;</span>
373
+ </div>
374
+ <span class="mc-check">&#10003;</span>
375
+ </div>
376
  </div>
 
 
377
  </div>
378
 
379
+ <!-- TECH STACK -->
380
+ <div>
381
+ <div class="sec-head">
382
+ <div class="sec-icon sec-icon-purple">&#128736;</div>
383
+ <span class="sec-title">Powered By</span>
384
+ </div>
385
+ <div class="tech-grid">
386
+ <div class="tg-badge tg-lg">
387
+ <span class="tg-icon">&#128279;</span>
388
+ <div class="tg-body"><div class="tg-name">LangGraph 0.2</div><div class="tg-sub">StateGraph Β· 5 nodes</div></div>
389
+ </div>
390
+ <div class="tg-badge tg-lc">
391
+ <span class="tg-icon">&#9961;</span>
392
+ <div class="tg-body"><div class="tg-name">LangChain LCEL</div><div class="tg-sub">prompt | llm | parser</div></div>
393
+ </div>
394
+ <div class="tg-badge tg-fb">
395
+ <span class="tg-icon">&#128451;</span>
396
+ <div class="tg-body"><div class="tg-name">FAISS + BM25</div><div class="tg-sub">RRF hybrid retrieval</div></div>
397
+ </div>
398
+ <div class="tg-badge tg-emb">
399
+ <span class="tg-icon">&#129688;</span>
400
+ <div class="tg-body"><div class="tg-name">HF Embeddings</div><div class="tg-sub">bge-small-en-v1.5</div></div>
401
+ </div>
402
+ <div class="tg-badge tg-fl">
403
+ <span class="tg-icon">&#127381;</span>
404
+ <div class="tg-body"><div class="tg-name">Flask 3.1</div><div class="tg-sub">+ Gunicorn WSGI</div></div>
405
+ </div>
406
+ <div class="tg-badge tg-dk">
407
+ <span class="tg-icon">&#128058;</span>
408
+ <div class="tg-body"><div class="tg-name">Docker</div><div class="tg-sub">HuggingFace Spaces</div></div>
409
  </div>
410
  </div>
 
411
  </div>
412
+
413
  </div>
414
 
415
+ <!-- ══════════ RIGHT PANEL ══════════ -->
416
  <div class="panel panel-right">
417
+
418
+ <!-- ARCHITECTURE DIAGRAM -->
419
+ <div class="arch-card">
420
+ <div class="arch-card-hdr">
421
+ <div class="sec-icon sec-icon-blue" style="width:20px;height:20px;font-size:11px">&#128202;</div>
422
+ <span class="sec-title">Pipeline Architecture</span>
423
+ <div class="arch-legend">
424
+ <span class="arch-legend-item"><span class="arch-legend-dot" style="background:#5b8ff9"></span>LLM (Qwen)</span>
425
+ <span class="arch-legend-item"><span class="arch-legend-dot" style="background:#22d47a"></span>Local</span>
426
+ <span class="arch-legend-item"><span class="arch-legend-dot" style="background:#f5a623"></span>Score-based</span>
427
+ </div>
428
+ </div>
429
+
430
+ <!-- Ingestion row -->
431
+ <div class="arch-ingest-row">
432
+ <div class="arch-node an-io">
433
+ <span class="arch-node-icon">&#128196;</span>
434
+ <div class="arch-node-name">Source</div>
435
+ <div class="arch-node-sub">PDF &middot; URL</div>
436
+ </div>
437
+ <span class="arch-arr">&#8594;</span>
438
+ <div class="arch-node an-chunker">
439
+ <span class="arch-node-icon">&#9986;</span>
440
+ <div class="arch-node-name">Chunker</div>
441
+ <div class="arch-node-sub">RCTextSplitter</div>
442
+ </div>
443
+ <span class="arch-arr">&#8594;</span>
444
+ <div class="arch-node an-index">
445
+ <span class="arch-node-icon">&#128451;</span>
446
+ <div class="arch-node-name">Hybrid Index</div>
447
+ <div class="arch-node-sub">FAISS + BM25</div>
448
+ </div>
449
+ <span class="arch-arr" style="font-size:.65rem;color:#29c6d4">&#8595; hybrid_search</span>
450
+ </div>
451
+
452
+ <!-- Agent pipeline row -->
453
+ <div class="arch-pipe-row">
454
+ <div class="arch-node an-llm" data-arch="planner" id="anode-planner">
455
+ <span class="arch-node-icon">&#127919;</span>
456
+ <div class="arch-node-name">Planner</div>
457
+ <div class="arch-node-sub">LLM &middot; 0.3</div>
458
+ </div>
459
+ <span class="arch-arr">&#8594;</span>
460
+ <div class="arch-node an-local" data-arch="retriever" id="anode-retriever">
461
+ <span class="arch-node-icon">&#128269;</span>
462
+ <div class="arch-node-name">Retriever</div>
463
+ <div class="arch-node-sub">Local &middot; RRF</div>
464
+ </div>
465
+ <span class="arch-arr">&#8594;</span>
466
+ <div class="arch-node an-score" data-arch="grader" id="anode-grader">
467
+ <span class="arch-node-icon">&#9878;</span>
468
+ <div class="arch-node-name">Grader</div>
469
+ <div class="arch-node-sub">Score &middot; 0ms</div>
470
+ </div>
471
+ <span class="arch-arr">&#8594;</span>
472
+ <div class="arch-node an-llm" data-arch="generator" id="anode-generator">
473
+ <span class="arch-node-icon">&#9997;</span>
474
+ <div class="arch-node-name">Generator</div>
475
+ <div class="arch-node-sub">LLM &middot; 0.4</div>
476
+ </div>
477
+ <span class="arch-arr">&#8594;</span>
478
+ <div class="arch-node an-llm" data-arch="critic" id="anode-critic">
479
+ <span class="arch-node-icon">&#128300;</span>
480
+ <div class="arch-node-name">Critic</div>
481
+ <div class="arch-node-sub">LLM &middot; 0.1</div>
482
+ </div>
483
+ <span class="arch-arr">&#8594;</span>
484
+ <div class="arch-node an-out" id="anode-answer">
485
+ <span class="arch-node-icon">&#128203;</span>
486
+ <div class="arch-node-name">Answer</div>
487
+ <div class="arch-node-sub">Cited &middot; Verified</div>
488
+ </div>
489
+ </div>
490
+ </div>
491
+
492
+ <!-- RESEARCH QUESTION -->
493
  <div class="sec-head">
494
  <div class="sec-icon sec-icon-purple">&#128269;</div>
495
  <span class="sec-title">Research Query</span>
496
  </div>
 
497
  <div class="q-card">
498
  <div class="q-label">Your Question</div>
499
  <textarea id="q-inp" rows="3"
 
505
  </div>
506
  </div>
507
 
508
+ <!-- AGENT PIPELINE PROGRESS -->
509
  <div id="pipeline">
510
  <div class="pipe-row">
511
+ <div class="pipe-step" id="ps-planner"><span class="step-dot"></span>&#127919;&ensp;Planner</div>
512
  <div class="pipe-arrow">&#8594;</div>
513
  <div class="pipe-step" id="ps-retriever"><span class="step-dot"></span>&#128269;&ensp;Retriever</div>
514
  <div class="pipe-arrow">&#8594;</div>
 
525
  <div class="trace-hdr">
526
  <span class="trace-title">&#128240;&ensp;Agent Trace</span>
527
  </div>
528
+ <div id="trace-log"></div>
529
  </div>
530
 
531
  <!-- ANSWER -->
532
  <div id="answer-wrap">
533
  <div class="ans-header">
534
+ <span class="ans-label">&#129504;&ensp;Answer</span>
 
 
 
535
  <div class="ans-actions">
536
+ <button class="btn btn-ghost btn-sm" onclick="copyAns(this)">&#128203;&ensp;Copy</button>
 
537
  </div>
538
  </div>
539
+ <div id="answer-text"></div>
540
+ <div id="verdict"></div>
 
541
  </div>
542
+
543
  </div>
544
  </main>
545
 
546
  <script>
 
547
  const esc=s=>String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
548
+ let pollTimer=null, seen=0, selectedModel="qwen-7b";
549
 
550
+ // ── Tab switching ───────────────────────────────────────────────────────────
 
 
551
  function switchTab(btn,name){
552
  document.querySelectorAll(".tab-btn").forEach(b=>b.classList.remove("active"));
553
  btn.classList.add("active");
 
555
  document.getElementById("tab-url").style.display=name==="url"?"":"none";
556
  }
557
 
558
+ // ── Drag & drop ─────────────────────────────────────────────────────────────
559
  function dg(e,over){e.preventDefault();document.getElementById("dz").classList[over?"add":"remove"]("drag-over");}
560
  function dp(e){e.preventDefault();document.getElementById("dz").classList.remove("drag-over");const f=e.dataTransfer.files[0];if(f)up(f);}
561
  function fc(e){if(e.target.files[0])up(e.target.files[0]);}
562
 
563
+ // ── PDF upload ───────────────────────────────────────────────────────────────
564
  async function up(file){
565
+ if(!file.name.toLowerCase().endsWith(".pdf")){sm("pdf-msg","error","Only PDF files are supported.");return;}
566
+ sm("pdf-msg","info","Uploading "+file.name+"…");
567
  const fd=new FormData();fd.append("file",file);
568
  try{
569
  const r=await fetch("/api/upload",{method:"POST",body:fd});
570
  const d=await r.json();
571
+ if(d.error){sm("pdf-msg","error",d.error);return;}
572
  setSource(d.filename,d.chunks,"pdf");
573
+ sm("pdf-msg","ok","&#10003;&ensp;Indexed "+d.chunks+" chunks from \""+d.filename+"\"");
574
+ }catch(e){sm("pdf-msg","error","Upload failed: "+e.message);}
575
  }
576
 
577
+ // ── URL fetch ────────────────────────────────────────────────────────────────
578
  async function fetchURL(){
579
  const url=document.getElementById("url-inp").value.trim();
580
+ if(!url){sm("url-msg","error","Please enter a URL.");return;}
581
+ document.getElementById("url-btn").disabled=true;
582
+ sm("url-msg","info","Fetching page…");
 
583
  try{
584
  const r=await fetch("/api/ingest_url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url})});
585
  const d=await r.json();
586
+ if(d.error){sm("url-msg","error",d.error);return;}
587
  setSource(d.url,d.chunks,"url");
588
+ sm("url-msg","ok","&#10003;&ensp;Indexed "+d.chunks+" chunks");
589
+ }catch(e){sm("url-msg","error","Failed: "+e.message);}
590
+ finally{document.getElementById("url-btn").disabled=false;}
591
  }
592
 
593
+ // ── Source card ──────────────────────────────────────────────────────────────
594
  function setSource(name,chunks,type){
595
  document.getElementById("source-name").textContent=name;
596
  document.getElementById("source-chunks").textContent=chunks+" chunks indexed";
597
  document.getElementById("sc-icon").textContent=type==="pdf"?"πŸ“„":"🌐";
598
  document.getElementById("source-card").style.display="block";
599
  const p=document.getElementById("hdr-src");
600
+ p.textContent=name.length>28?name.slice(0,28)+"…":name;
601
  p.classList.add("loaded");
602
  }
603
 
604
+ // ── Model selector ───────────────────────────────────────────────────────────
605
+ function selectModel(key,card){
606
+ document.querySelectorAll(".model-card").forEach(c=>c.classList.remove("selected"));
607
+ card.classList.add("selected");
608
+ selectedModel=key;
609
+ const label=card.dataset.label||key;
610
+ document.getElementById("hdr-model-badge").textContent="⚑ "+label;
611
+ // persist to server (best-effort, also passed per-request)
612
+ fetch("/api/set_model",{method:"POST",headers:{"Content-Type":"application/json"},
613
+ body:JSON.stringify({model:key})}).catch(()=>{});
614
  }
615
 
616
+ // ── Architecture node states ─────────────────────────────────────────────────
617
+ const ARCH_AGENTS=["planner","retriever","grader","generator","critic"];
618
+ function archReset(){
619
+ ARCH_AGENTS.forEach(a=>{
620
+ const n=document.getElementById("anode-"+a);
621
+ if(n){n.classList.remove("an-running","an-done");}
622
+ });
623
+ const out=document.getElementById("anode-answer");
624
+ if(out)out.classList.remove("an-running","an-done");
625
+ }
626
+ function archSetActive(agent){
627
+ ARCH_AGENTS.forEach(a=>{
628
+ const n=document.getElementById("anode-"+a);
629
+ if(!n)return;
630
+ if(a===agent){n.classList.add("an-running");n.classList.remove("an-done");}
631
+ else if(n.classList.contains("an-running")){n.classList.remove("an-running");n.classList.add("an-done");}
632
+ });
633
+ }
634
+ function archAllDone(){
635
+ ARCH_AGENTS.forEach(a=>{
636
+ const n=document.getElementById("anode-"+a);
637
+ if(n){n.classList.remove("an-running");n.classList.add("an-done");}
638
+ });
639
+ const out=document.getElementById("anode-answer");
640
+ if(out){out.classList.add("an-running");setTimeout(()=>{out.classList.remove("an-running");},1200);}
641
+ }
642
+
643
+ // ── Pipeline step states ─────────────────────────────────────────────────────
644
+ function pipeReset(){
645
+ ARCH_AGENTS.forEach(a=>{
646
+ const el=document.getElementById("ps-"+a);
647
+ if(el)el.className="pipe-step";
648
+ });
649
+ }
650
+ function pipeSetActive(agent){
651
+ ARCH_AGENTS.forEach(a=>{
652
+ const el=document.getElementById("ps-"+a);
653
+ if(!el)return;
654
+ if(a===agent)el.className="pipe-step ps-active";
655
+ else if(el.classList.contains("ps-active")){el.className="pipe-step ps-done";}
656
+ });
657
+ }
658
+ function pipeAllDone(){
659
+ ARCH_AGENTS.forEach(a=>{
660
+ const el=document.getElementById("ps-"+a);
661
+ if(el)el.className="pipe-step ps-done";
662
+ });
663
+ }
664
+
665
+ // ── Question submit ──────────────────────────────────────────────────────────
666
  function qk(e){if(e.key==="Enter"&&!e.shiftKey){e.preventDefault();ask();}}
667
 
668
  async function ask(){
 
672
 
673
  const btn=document.getElementById("ask-btn");
674
  btn.disabled=true;
675
+ btn.innerHTML='<span class="spinner"></span>&ensp;Thinking…';
676
 
677
  document.getElementById("pipeline").style.display="block";
678
  document.getElementById("trace-wrap").style.display="block";
679
+ document.getElementById("trace-log").innerHTML=
680
+ '<div class="t-step"><span class="t-msg" style="color:var(--muted)">Starting agents…</span></div>';
681
  document.getElementById("answer-wrap").style.display="none";
682
+ pipeReset(); archReset(); seen=0; clearInterval(pollTimer);
 
683
 
684
  try{
685
+ const r=await fetch("/api/research",{
686
+ method:"POST",headers:{"Content-Type":"application/json"},
687
+ body:JSON.stringify({question:q,model:selectedModel})
688
+ });
689
  const d=await r.json();
690
+ if(d.error){traceErr(d.error);resetBtn();return;}
691
  pollTimer=setInterval(()=>poll(d.query_id),1500);
692
+ }catch(e){traceErr("Network error: "+e.message);resetBtn();}
693
  }
694
 
695
+ function resetBtn(){
696
  const btn=document.getElementById("ask-btn");
697
  btn.disabled=false;
698
+ btn.innerHTML='&#9889;&ensp;Ask';
699
  }
700
 
701
+ // ── Polling ──────────────────────────────────────────────────────────────────
702
  async function poll(qid){
703
  try{
704
  const r=await fetch("/api/trace/"+qid);
705
+ if(!r.ok){traceErr("Server error "+r.status);clearInterval(pollTimer);resetBtn();return;}
706
  const d=await r.json();
707
  renderTrace(d.trace||[]);
708
  if(["complete","error"].includes(d.status)){
709
+ clearInterval(pollTimer);resetBtn();
710
+ if(d.status==="complete"&&d.result){renderAnswer(d.result);archAllDone();pipeAllDone();}
711
+ else if(d.status==="error"&&d.result){traceErr(d.result.error||"An error occurred.");}
 
712
  }
713
+ }catch(e){traceErr("Poll error: "+e.message);clearInterval(pollTimer);resetBtn();}
714
+ }
715
+
716
+ // ── Trace rendering ──────────────────────────────────────────────────────────
717
+ function traceErr(msg){
718
+ const log=document.getElementById("trace-log");
719
+ log.innerHTML+='<div class="t-step"><span class="t-badge b-error">error</span><span class="t-msg" style="color:var(--red)">'+esc(msg)+'</span></div>';
720
+ log.scrollTop=log.scrollHeight;
721
  }
722
 
 
723
  function renderTrace(steps){
724
  if(!steps.length)return;
725
  const log=document.getElementById("trace-log");
726
  if(seen===0)log.innerHTML="";
727
  for(let i=seen;i<steps.length;i++){
728
  const s=steps[i];
729
+ archSetActive(s.agent);
730
+ pipeSetActive(s.agent);
731
+ const lat=s.latency_ms>0?'<span class="t-lat">'+s.latency_ms+'ms</span>':"";
732
+ log.innerHTML+='<div class="t-step"><span class="t-badge b-'+s.agent+'">'+s.agent+'</span><span class="t-msg">'+esc(s.message)+'</span>'+lat+'</div>';
 
 
 
 
 
 
733
  }
734
+ seen=steps.length;log.scrollTop=log.scrollHeight;
 
 
 
 
 
 
 
735
  }
736
 
737
+ // ── Answer rendering ─────────────────────────────────────────────────────────
738
  function renderAnswer(result){
739
  document.getElementById("answer-wrap").style.display="block";
740
  document.getElementById("answer-text").textContent=result.generation||"No answer generated.";
741
+ const v=document.getElementById("verdict");
742
+ if(result.verdict==="APPROVED"){v.className="v-ok";v.textContent="βœ“ High confidence";}
743
+ else if(result.verdict){v.className="v-warn";v.textContent="⚠ Low confidence β€” verify with source";}
744
+ else{v.textContent="";}
745
+ document.getElementById("answer-wrap").scrollIntoView({behavior:"smooth",block:"nearest"});
746
  }
747
 
748
+ // ── Copy answer ──────────────────────────────────────────────────────────────
749
+ function copyAns(btn){
750
  const text=document.getElementById("answer-text").textContent;
751
+ navigator.clipboard.writeText(text).then(()=>{
752
+ btn.textContent="βœ“ Copied!";
753
+ setTimeout(()=>{btn.innerHTML="&#128203;&ensp;Copy";},1800);
754
+ });
 
 
755
  }
756
 
757
+ // ── Show message ─────────────────────────────────────────────────────────────
758
+ function sm(id,type,msg){
759
  const el=document.getElementById(id);
760
+ if(type==="ok")el.innerHTML='<div class="msg msg-ok">'+msg+'</div>';
761
+ else if(type==="error")el.innerHTML='<div class="msg msg-err">'+esc(msg)+'</div>';
762
+ else el.innerHTML='<div class="msg-info">'+esc(msg)+'</div>';
763
  }
764
  </script>
765
  </body>
write_html.py CHANGED
@@ -20,275 +20,483 @@ html,body{height:100%}
20
  body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
21
  background:var(--bg);color:var(--text);font-size:14px;line-height:1.5}
22
 
23
- /* ── ANIMATIONS ────────────────────────────────── */
24
  @keyframes spin{to{transform:rotate(360deg)}}
25
- @keyframes fadeUp{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
26
- @keyframes pulse{0%,100%{opacity:1}50%{opacity:.45}}
 
 
27
  @keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}
28
 
29
- /* ── HEADER ────────────────────────────────────── */
30
- header{
31
- background:var(--surface);
32
- border-bottom:1px solid var(--border);
33
- padding:0 28px;height:56px;
34
- display:flex;align-items:center;gap:14px;
35
- position:sticky;top:0;z-index:100;
36
- }
37
- .logo{display:flex;align-items:center;gap:9px;text-decoration:none}
38
- .logo-icon{width:30px;height:30px;background:linear-gradient(135deg,var(--accent),var(--purple));
39
- border-radius:8px;display:flex;align-items:center;justify-content:center;
40
- font-size:15px;flex-shrink:0}
41
- .logo-text{font-size:1rem;font-weight:800;color:var(--text);letter-spacing:-.3px}
42
  .logo-text span{color:var(--accent)}
43
- .logo-sub{font-size:.68rem;color:var(--muted);margin-left:2px;font-weight:400;
44
- display:none} /* shown on wider screens */
45
- .hdr-badges{display:flex;gap:8px;align-items:center;margin-left:4px}
46
- .badge{font-size:.65rem;font-weight:700;padding:3px 9px;border-radius:20px;letter-spacing:.02em}
47
- .badge-model{background:rgba(91,143,249,.12);border:1px solid rgba(91,143,249,.25);color:var(--accent)}
48
- .badge-src{background:rgba(113,128,160,.1);border:1px solid var(--border);color:var(--muted);
49
- max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
50
- .badge-src.loaded{background:rgba(34,212,122,.1);border-color:rgba(34,212,122,.3);color:var(--green)}
51
- #hdr-src{margin-left:auto}
52
-
53
- /* ── LAYOUT ─────────────────────────────────────── */
54
- main{display:grid;grid-template-columns:320px 1fr;height:calc(100vh - 56px);overflow:hidden}
55
- .panel{padding:24px 22px;overflow-y:auto;height:100%}
56
- .panel-left{border-right:1px solid var(--border);background:var(--surface)}
 
 
57
  .panel-right{background:var(--bg)}
58
 
59
- /* ── SECTION HEADERS ────────────────────────────── */
60
- .sec-head{display:flex;align-items:center;gap:8px;margin-bottom:16px}
61
- .sec-icon{width:26px;height:26px;border-radius:7px;display:flex;align-items:center;
62
- justify-content:center;font-size:13px;flex-shrink:0}
63
  .sec-icon-blue{background:rgba(91,143,249,.15)}
 
 
64
  .sec-icon-purple{background:rgba(167,139,250,.15)}
65
- .sec-title{font-size:.75rem;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:var(--sub)}
66
-
67
- /* ── TABS ────────────────────────────────────────── */
68
- .tabs{display:flex;gap:2px;background:rgba(255,255,255,.03);border-radius:9px;
69
- padding:3px;margin-bottom:18px;border:1px solid var(--border)}
70
- .tab-btn{flex:1;background:none;border:none;color:var(--muted);font-size:.78rem;
71
- font-weight:600;padding:7px 10px;border-radius:7px;cursor:pointer;
72
- transition:all .18s;font-family:inherit;display:flex;align-items:center;
73
- justify-content:center;gap:5px}
74
- .tab-btn.active{background:var(--card2);color:var(--text);
75
- box-shadow:0 1px 6px rgba(0,0,0,.35)}
76
 
77
- /* ── DROPZONE ────────────────────────────────────── */
78
- .dropzone{
79
- border:2px dashed var(--border2);border-radius:12px;padding:28px 16px;
80
- text-align:center;cursor:pointer;transition:all .22s;position:relative;overflow:hidden;
81
- }
82
- .dropzone::before{
83
- content:"";position:absolute;inset:0;
84
- background:linear-gradient(90deg,transparent 0%,rgba(91,143,249,.04) 50%,transparent 100%);
85
- background-size:200% 100%;opacity:0;transition:opacity .3s;
86
- }
87
- .dropzone:hover{border-color:var(--accent);background:rgba(91,143,249,.04)}
88
- .dropzone:hover::before{opacity:1;animation:shimmer 2s infinite}
89
- .dropzone.drag-over{border-color:var(--accent);background:rgba(91,143,249,.08);transform:scale(1.01)}
90
- .dz-icon{font-size:2.2rem;margin-bottom:10px;display:block}
91
- .dz-label{font-size:.84rem;color:var(--sub);line-height:1.5}
 
 
 
 
92
  .dz-label strong{color:var(--accent)}
93
- .dz-hint{font-size:.72rem;color:var(--muted);margin-top:5px}
94
-
95
- /* ── URL INPUT ────────────────────────────────────── */
96
- .url-row{display:flex;gap:8px;margin-bottom:10px}
97
- .url-row input{flex:1;background:var(--card);border:1px solid var(--border2);
98
- border-radius:8px;padding:9px 12px;color:var(--text);font-size:.84rem;
99
- font-family:inherit;outline:none;transition:border-color .18s}
100
- .url-row input:focus{border-color:var(--accent)}
101
-
102
- /* ── SOURCE LOADED CARD ──────────────────────────── */
103
- #source-card{
104
- display:none;margin-top:16px;background:var(--card);
105
- border:1px solid rgba(34,212,122,.2);border-radius:10px;padding:13px 14px;
106
- animation:fadeUp .25s ease;
107
- }
108
- .sc-row{display:flex;align-items:flex-start;gap:10px}
109
- .sc-icon{width:34px;height:34px;background:rgba(34,212,122,.12);border-radius:8px;
110
- flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:16px}
111
- .sc-info{flex:1;min-width:0}
112
- .sc-name{font-size:.82rem;font-weight:600;color:var(--text);
113
- overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-bottom:3px}
114
  .sc-meta{font-size:.72rem;color:var(--teal)}
115
- .sc-ready{display:inline-flex;align-items:center;gap:5px;margin-top:8px;
116
- font-size:.7rem;font-weight:700;color:var(--green);
117
- background:rgba(34,212,122,.08);border:1px solid rgba(34,212,122,.18);
118
- border-radius:5px;padding:3px 8px}
119
-
120
- /* ── MESSAGES ────────────────────────────────────── */
121
- .msg{border-radius:8px;padding:10px 13px;font-size:.79rem;
122
- margin-top:10px;line-height:1.55;animation:fadeUp .2s ease}
123
- .msg-ok{background:rgba(34,212,122,.07);border:1px solid rgba(34,212,122,.2);color:var(--green)}
124
- .msg-err{background:rgba(240,92,92,.07);border:1px solid rgba(240,92,92,.2);color:var(--red)}
125
- .msg-info{color:var(--muted);font-size:.76rem;margin-top:8px}
126
-
127
- /* ── BUTTONS ─────────────────────────────────────── */
128
- .btn{display:inline-flex;align-items:center;gap:7px;padding:9px 20px;
129
- border-radius:8px;border:none;font-size:.84rem;font-weight:600;
130
- cursor:pointer;transition:all .18s;font-family:inherit}
131
- .btn-primary{background:linear-gradient(135deg,var(--accent),var(--accent2));
132
- color:#fff;box-shadow:0 2px 8px rgba(91,143,249,.25)}
133
- .btn-primary:hover{transform:translateY(-1px);box-shadow:0 4px 14px rgba(91,143,249,.35)}
134
- .btn-primary:active{transform:none}
135
- .btn-primary:disabled{opacity:.5;cursor:default;transform:none;box-shadow:none}
136
- .btn-sm{padding:7px 14px;font-size:.78rem}
137
- .btn-ghost{background:rgba(255,255,255,.06);border:1px solid var(--border2);
138
- color:var(--sub);font-size:.74rem;padding:5px 12px;border-radius:6px}
139
- .btn-ghost:hover{background:rgba(255,255,255,.1);color:var(--text)}
140
- .spinner{width:13px;height:13px;border:2px solid rgba(255,255,255,.3);
141
- border-top-color:#fff;border-radius:50%;animation:spin .65s linear infinite;flex-shrink:0}
142
-
143
- /* ── QUESTION AREA ───────────────────────────────── */
144
- .q-card{background:var(--card);border:1px solid var(--border2);
145
- border-radius:11px;padding:16px;margin-bottom:16px}
146
- .q-label{font-size:.7rem;font-weight:700;color:var(--muted);text-transform:uppercase;
147
- letter-spacing:.06em;margin-bottom:8px}
148
- textarea{width:100%;background:transparent;border:none;color:var(--text);
149
- font-size:.9rem;font-family:inherit;outline:none;resize:none;
150
- line-height:1.6;min-height:72px}
151
- textarea::placeholder{color:var(--muted)}
152
- .q-footer{display:flex;align-items:center;gap:10px;padding-top:12px;
153
- border-top:1px solid var(--border)}
154
- #q-err{font-size:.77rem;color:var(--red)}
155
-
156
- /* ── PIPELINE ────────────────────────────────────── */
157
- #pipeline{display:none;margin-bottom:14px;animation:fadeUp .2s ease}
158
- .pipe-row{display:flex;align-items:center;gap:0;background:var(--card);
159
- border:1px solid var(--border2);border-radius:10px;padding:10px 14px;
160
- overflow-x:auto}
161
- .pipe-step{display:flex;align-items:center;gap:5px;font-size:.72rem;font-weight:600;
162
- color:var(--muted);white-space:nowrap;padding:4px 8px;border-radius:6px;
163
- transition:all .2s}
164
- .pipe-step.active{color:var(--accent);background:rgba(91,143,249,.12)}
165
- .pipe-step.done{color:var(--green)}
166
- .pipe-step .step-dot{width:6px;height:6px;border-radius:50%;background:currentColor;flex-shrink:0}
167
- .pipe-step.active .step-dot{animation:pulse .9s ease infinite}
168
- .pipe-arrow{color:var(--border2);font-size:.8rem;padding:0 2px;flex-shrink:0}
169
-
170
- /* ── TRACE LOG ───────────────────────────────────── */
171
- #trace-wrap{display:none;margin-bottom:16px;animation:fadeUp .2s ease}
172
- .trace-hdr{display:flex;align-items:center;gap:8px;margin-bottom:8px}
173
- .trace-title{font-size:.7rem;font-weight:700;text-transform:uppercase;
174
- letter-spacing:.07em;color:var(--muted)}
175
- .trace-box{background:var(--card);border:1px solid var(--border2);border-radius:10px;
176
- padding:4px 0;max-height:220px;overflow-y:auto}
177
- .t-step{display:flex;align-items:flex-start;gap:10px;padding:8px 14px;
178
- border-bottom:1px solid var(--border);animation:fadeUp .18s ease;transition:background .15s}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  .t-step:last-child{border-bottom:none}
180
- .t-step:hover{background:rgba(255,255,255,.02)}
181
- .t-icon{font-size:13px;flex-shrink:0;margin-top:1px;width:18px;text-align:center}
182
- .t-agent{font-size:.64rem;font-weight:800;text-transform:uppercase;letter-spacing:.05em;
183
- padding:2px 7px;border-radius:4px;flex-shrink:0;white-space:nowrap}
184
- .a-planner {background:rgba(91,143,249,.14);color:var(--accent)}
185
- .a-retriever{background:rgba(41,198,212,.14);color:var(--teal)}
186
- .a-grader {background:rgba(245,166,35,.14);color:var(--gold)}
187
- .a-generator{background:rgba(34,212,122,.14);color:var(--green)}
188
- .a-critic {background:rgba(167,139,250,.14);color:var(--purple)}
189
- .a-error {background:rgba(240,92,92,.14);color:var(--red)}
190
- .t-msg{flex:1;font-size:.78rem;color:var(--sub);line-height:1.5}
191
- .t-lat{font-size:.65rem;color:rgba(128,136,160,.5);flex-shrink:0;margin-top:2px}
192
-
193
- /* ── ANSWER CARD ─────────────────────────────────── */
194
- #answer-wrap{display:none;background:var(--card);border:1px solid var(--border2);
195
- border-radius:12px;overflow:hidden;animation:fadeUp .3s ease}
196
- .ans-header{display:flex;align-items:center;justify-content:space-between;
197
- padding:13px 18px 12px;border-bottom:1px solid var(--border);
198
- background:rgba(255,255,255,.02)}
199
- .ans-title{display:flex;align-items:center;gap:8px;font-size:.72rem;font-weight:800;
200
- text-transform:uppercase;letter-spacing:.08em;color:var(--sub)}
201
- .ans-title-icon{width:22px;height:22px;background:rgba(34,212,122,.15);border-radius:6px;
202
- display:flex;align-items:center;justify-content:center;font-size:11px}
203
- .ans-actions{display:flex;align-items:center;gap:8px}
204
- #verdict-badge{font-size:.68rem;font-weight:700;padding:3px 10px;border-radius:20px}
205
- .v-ok {background:rgba(34,212,122,.1);border:1px solid rgba(34,212,122,.25);color:var(--green)}
206
- .v-warn{background:rgba(245,166,35,.1);border:1px solid rgba(245,166,35,.25);color:var(--gold)}
207
- .ans-body{padding:18px}
208
- #answer-text{font-size:.9rem;line-height:1.78;color:var(--text);
209
- white-space:pre-wrap;word-break:break-word}
210
-
211
- /* ── RESPONSIVE ──────────────────────────────────── */
212
- @media(min-width:1100px){.logo-sub{display:block}}
213
- @media(max-width:768px){
214
- main{grid-template-columns:1fr;height:auto;overflow:visible}
215
- .panel{height:auto}
216
  .panel-left{border-right:none;border-bottom:1px solid var(--border)}
 
 
 
217
  }
218
  </style>
219
  </head>
220
  <body>
221
 
222
- <!-- ── HEADER ── -->
223
  <header>
224
  <a class="logo" href="#">
225
  <div class="logo-icon">&#129504;</div>
226
  <span class="logo-text">Doc<span>Mind</span></span>
227
  </a>
228
- <span class="logo-sub">AI Document Research</span>
229
- <div class="hdr-badges">
230
- <span class="badge badge-model">Qwen 2.5 7B</span>
 
 
231
  </div>
232
- <span class="badge badge-src" id="hdr-src">No source loaded</span>
 
 
 
 
233
  </header>
234
 
235
  <main>
236
- <!-- ── LEFT PANEL: KNOWLEDGE BASE ── -->
237
  <div class="panel panel-left">
238
- <div class="sec-head">
239
- <div class="sec-icon sec-icon-blue">&#128218;</div>
240
- <span class="sec-title">Knowledge Base</span>
241
- </div>
242
-
243
- <div class="tabs">
244
- <button class="tab-btn active" onclick="switchTab(this,'pdf')">&#128196;&ensp;Upload PDF</button>
245
- <button class="tab-btn" onclick="switchTab(this,'url')">&#127760;&ensp;Paste URL</button>
246
- </div>
247
 
248
- <!-- PDF TAB -->
249
- <div id="tab-pdf">
250
- <div class="dropzone" id="dz"
251
- onclick="document.getElementById('fi').click()"
252
- ondragover="dg(event,true)" ondragleave="dg(event,false)" ondrop="dp(event)">
253
- <span class="dz-icon">&#128196;</span>
254
- <div class="dz-label"><strong>Click to browse</strong> or drag &amp; drop</div>
255
- <div class="dz-hint">PDF only &middot; max 10 MB</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  </div>
257
- <input type="file" id="fi" accept=".pdf" style="display:none" onchange="fc(event)"/>
258
- <div id="pdf-msg"></div>
259
  </div>
260
 
261
- <!-- URL TAB -->
262
- <div id="tab-url" style="display:none">
263
- <div class="url-row">
264
- <input type="url" id="url-inp" placeholder="https://en.wikipedia.org/wiki/..."
265
- onkeydown="if(event.key==='Enter')fetchURL()"/>
266
- <button class="btn btn-primary btn-sm" id="url-btn" onclick="fetchURL()">Fetch</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  </div>
268
- <div class="msg-info" style="margin-bottom:4px">Wikipedia, government sites, and docs work best. Some sites block automated access.</div>
269
- <div id="url-msg"></div>
270
  </div>
271
 
272
- <!-- SOURCE LOADED -->
273
- <div id="source-card">
274
- <div class="sc-row">
275
- <div class="sc-icon" id="sc-icon">&#128196;</div>
276
- <div class="sc-info">
277
- <div class="sc-name" id="source-name"></div>
278
- <div class="sc-meta" id="source-chunks"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  </div>
280
  </div>
281
- <div class="sc-ready">&#10003;&ensp;Ready for questions</div>
282
  </div>
 
283
  </div>
284
 
285
- <!-- ── RIGHT PANEL: RESEARCH ── -->
286
  <div class="panel panel-right">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  <div class="sec-head">
288
  <div class="sec-icon sec-icon-purple">&#128269;</div>
289
  <span class="sec-title">Research Query</span>
290
  </div>
291
-
292
  <div class="q-card">
293
  <div class="q-label">Your Question</div>
294
  <textarea id="q-inp" rows="3"
@@ -300,10 +508,10 @@ textarea::placeholder{color:var(--muted)}
300
  </div>
301
  </div>
302
 
303
- <!-- AGENT PIPELINE -->
304
  <div id="pipeline">
305
  <div class="pipe-row">
306
- <div class="pipe-step" id="ps-planner"><span class="step-dot"></span>&#129518;&ensp;Planner</div>
307
  <div class="pipe-arrow">&#8594;</div>
308
  <div class="pipe-step" id="ps-retriever"><span class="step-dot"></span>&#128269;&ensp;Retriever</div>
309
  <div class="pipe-arrow">&#8594;</div>
@@ -320,35 +528,29 @@ textarea::placeholder{color:var(--muted)}
320
  <div class="trace-hdr">
321
  <span class="trace-title">&#128240;&ensp;Agent Trace</span>
322
  </div>
323
- <div class="trace-box" id="trace-log"></div>
324
  </div>
325
 
326
  <!-- ANSWER -->
327
  <div id="answer-wrap">
328
  <div class="ans-header">
329
- <div class="ans-title">
330
- <div class="ans-title-icon">&#128161;</div>
331
- Answer
332
- </div>
333
  <div class="ans-actions">
334
- <span id="verdict-badge"></span>
335
- <button class="btn btn-ghost" id="copy-btn" onclick="copyAns()">&#128203;&ensp;Copy</button>
336
  </div>
337
  </div>
338
- <div class="ans-body">
339
- <div id="answer-text"></div>
340
- </div>
341
  </div>
 
342
  </div>
343
  </main>
344
 
345
  <script>
346
- let pollTimer=null,seen=0;
347
  const esc=s=>String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
 
348
 
349
- const AGENT_ICONS={planner:"&#129518;",retriever:"&#128269;",grader:"&#9878;",generator:"&#9997;",critic:"&#128300;"};
350
-
351
- /* ── TABS ── */
352
  function switchTab(btn,name){
353
  document.querySelectorAll(".tab-btn").forEach(b=>b.classList.remove("active"));
354
  btn.classList.add("active");
@@ -356,64 +558,114 @@ function switchTab(btn,name){
356
  document.getElementById("tab-url").style.display=name==="url"?"":"none";
357
  }
358
 
359
- /* ── DRAG & DROP ── */
360
  function dg(e,over){e.preventDefault();document.getElementById("dz").classList[over?"add":"remove"]("drag-over");}
361
  function dp(e){e.preventDefault();document.getElementById("dz").classList.remove("drag-over");const f=e.dataTransfer.files[0];if(f)up(f);}
362
  function fc(e){if(e.target.files[0])up(e.target.files[0]);}
363
 
364
- /* ── UPLOAD PDF ── */
365
  async function up(file){
366
- if(!file.name.toLowerCase().endsWith(".pdf")){showMsg("pdf-msg","error","Only PDF files are supported.");return;}
367
- showMsg("pdf-msg","info","Uploading "+file.name+"...");
368
  const fd=new FormData();fd.append("file",file);
369
  try{
370
  const r=await fetch("/api/upload",{method:"POST",body:fd});
371
  const d=await r.json();
372
- if(d.error){showMsg("pdf-msg","error",d.error);return;}
373
  setSource(d.filename,d.chunks,"pdf");
374
- showMsg("pdf-msg","ok","&#10003;&ensp;"+d.chunks+" chunks indexed from "+d.filename);
375
- }catch(e){showMsg("pdf-msg","error","Upload failed: "+e.message);}
376
  }
377
 
378
- /* ── FETCH URL ── */
379
  async function fetchURL(){
380
  const url=document.getElementById("url-inp").value.trim();
381
- if(!url){showMsg("url-msg","error","Please enter a URL.");return;}
382
- const btn=document.getElementById("url-btn");
383
- btn.disabled=true;btn.textContent="Fetching...";
384
- showMsg("url-msg","info","Fetching and indexing page...");
385
  try{
386
  const r=await fetch("/api/ingest_url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url})});
387
  const d=await r.json();
388
- if(d.error){showMsg("url-msg","error",d.error);return;}
389
  setSource(d.url,d.chunks,"url");
390
- showMsg("url-msg","ok","&#10003;&ensp;"+d.chunks+" chunks indexed");
391
- }catch(e){showMsg("url-msg","error","Failed: "+e.message);}
392
- finally{btn.disabled=false;btn.textContent="Fetch";}
393
  }
394
 
395
- /* ── SOURCE LOADED ── */
396
  function setSource(name,chunks,type){
397
  document.getElementById("source-name").textContent=name;
398
  document.getElementById("source-chunks").textContent=chunks+" chunks indexed";
399
- document.getElementById("sc-icon").textContent=type==="pdf"?"&#128196;":"&#127760;";
400
  document.getElementById("source-card").style.display="block";
401
  const p=document.getElementById("hdr-src");
402
- p.textContent=name.length>30?name.slice(0,30)+"...":name;
403
  p.classList.add("loaded");
404
  }
405
 
406
- /* ── PIPELINE ── */
407
- const PIPE_AGENTS=["planner","retriever","grader","generator","critic"];
408
- function resetPipeline(){PIPE_AGENTS.forEach(a=>{const el=document.getElementById("ps-"+a);if(el){el.classList.remove("active","done");}});}
409
- function setAgent(name,done){
410
- const el=document.getElementById("ps-"+name);
411
- if(!el)return;
412
- if(done){el.classList.remove("active");el.classList.add("done");}
413
- else{el.classList.remove("done");el.classList.add("active");}
 
 
414
  }
415
 
416
- /* ── ASK ── */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  function qk(e){if(e.key==="Enter"&&!e.shiftKey){e.preventDefault();ask();}}
418
 
419
  async function ask(){
@@ -423,106 +675,100 @@ async function ask(){
423
 
424
  const btn=document.getElementById("ask-btn");
425
  btn.disabled=true;
426
- btn.innerHTML="<span class='spinner'></span>&ensp;Thinking...";
427
 
428
  document.getElementById("pipeline").style.display="block";
429
  document.getElementById("trace-wrap").style.display="block";
430
- document.getElementById("trace-log").innerHTML="<div class='t-step'><span class='t-msg' style='color:var(--muted);font-style:italic'>Initialising agents...</span></div>";
 
431
  document.getElementById("answer-wrap").style.display="none";
432
- resetPipeline();
433
- seen=0;clearInterval(pollTimer);
434
 
435
  try{
436
- const r=await fetch("/api/research",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({question:q})});
 
 
 
437
  const d=await r.json();
438
- if(d.error){traceErr(d.error);resetAskBtn();return;}
439
  pollTimer=setInterval(()=>poll(d.query_id),1500);
440
- }catch(e){traceErr("Network error: "+e.message);resetAskBtn();}
441
  }
442
 
443
- function resetAskBtn(){
444
  const btn=document.getElementById("ask-btn");
445
  btn.disabled=false;
446
- btn.innerHTML="&#9889;&ensp;Ask";
447
  }
448
 
449
- /* ── POLL ── */
450
  async function poll(qid){
451
  try{
452
  const r=await fetch("/api/trace/"+qid);
453
- if(!r.ok){traceErr("Server error "+r.status);clearInterval(pollTimer);resetAskBtn();return;}
454
  const d=await r.json();
455
  renderTrace(d.trace||[]);
456
  if(["complete","error"].includes(d.status)){
457
- clearInterval(pollTimer);
458
- resetAskBtn();
459
- if(d.status==="complete"&&d.result)renderAnswer(d.result);
460
- else if(d.status==="error"&&d.result)traceErr(d.result.error||"An error occurred.");
461
  }
462
- }catch(e){traceErr("Poll error: "+e.message);clearInterval(pollTimer);resetAskBtn();}
 
 
 
 
 
 
 
463
  }
464
 
465
- /* ── TRACE ── */
466
  function renderTrace(steps){
467
  if(!steps.length)return;
468
  const log=document.getElementById("trace-log");
469
  if(seen===0)log.innerHTML="";
470
  for(let i=seen;i<steps.length;i++){
471
  const s=steps[i];
472
- const icon=AGENT_ICONS[s.agent]||"&#9679;";
473
- const lat=s.latency_ms>0?"<span class='t-lat'>"+s.latency_ms+"ms</span>":"";
474
- const cls="a-"+(["planner","retriever","grader","generator","critic"].includes(s.agent)?s.agent:"error");
475
- log.innerHTML+="<div class='t-step'>"
476
- +"<span class='t-icon'>"+icon+"</span>"
477
- +"<span class='t-agent "+cls+"'>"+s.agent+"</span>"
478
- +"<span class='t-msg'>"+esc(s.message)+"</span>"
479
- +lat+"</div>";
480
- if(s.status==="running")setAgent(s.agent,false);
481
- else if(s.status==="complete")setAgent(s.agent,true);
482
  }
483
- seen=steps.length;
484
- log.scrollTop=log.scrollHeight;
485
- }
486
-
487
- function traceErr(msg){
488
- const log=document.getElementById("trace-log");
489
- log.innerHTML+="<div class='t-step'><span class='t-icon'>&#10060;</span><span class='t-agent a-error'>error</span><span class='t-msg' style='color:var(--red)'>"+esc(msg)+"</span></div>";
490
- log.scrollTop=log.scrollHeight;
491
  }
492
 
493
- /* ── ANSWER ── */
494
  function renderAnswer(result){
495
  document.getElementById("answer-wrap").style.display="block";
496
  document.getElementById("answer-text").textContent=result.generation||"No answer generated.";
497
- const vb=document.getElementById("verdict-badge");
498
- if(result.verdict==="APPROVED"){vb.className="v-ok";vb.textContent="&#10003; High confidence";}
499
- else if(result.verdict){vb.className="v-warn";vb.textContent="&#9888; Verify with source";}
500
- else{vb.textContent="";}
501
- document.getElementById("answer-wrap").scrollIntoView({behavior:"smooth",block:"start"});
502
  }
503
 
504
- /* ── COPY ── */
505
- async function copyAns(){
506
  const text=document.getElementById("answer-text").textContent;
507
- try{
508
- await navigator.clipboard.writeText(text);
509
- const btn=document.getElementById("copy-btn");
510
- btn.innerHTML="&#10003;&ensp;Copied!";
511
- setTimeout(()=>{btn.innerHTML="&#128203;&ensp;Copy";},2000);
512
- }catch(e){}
513
  }
514
 
515
- /* ── MSG HELPER ── */
516
- function showMsg(id,type,msg){
517
  const el=document.getElementById(id);
518
- if(type==="ok")el.innerHTML="<div class='msg msg-ok'>"+msg+"</div>";
519
- else if(type==="error")el.innerHTML="<div class='msg msg-err'>"+esc(msg)+"</div>";
520
- else el.innerHTML="<div class='msg-info'>"+esc(msg)+"</div>";
521
  }
522
  </script>
523
  </body>
524
  </html>
525
  '''
526
 
527
- pathlib.Path('E:/HuggingFace/docmind/templates/index.html').write_text(HTML, encoding='utf-8')
528
- print('Done, size:', len(HTML))
 
 
20
  body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
21
  background:var(--bg);color:var(--text);font-size:14px;line-height:1.5}
22
 
23
+ /* ── ANIMATIONS ── */
24
  @keyframes spin{to{transform:rotate(360deg)}}
25
+ @keyframes fadeUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
26
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
27
+ @keyframes nodeGlow{0%,100%{box-shadow:0 0 0 rgba(91,143,249,0)}50%{box-shadow:0 0 14px rgba(91,143,249,.6)}}
28
+ @keyframes flowLine{0%{stroke-dashoffset:20}100%{stroke-dashoffset:0}}
29
  @keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}
30
 
31
+ /* ── HEADER ── */
32
+ header{background:var(--surface);border-bottom:1px solid var(--border);
33
+ padding:0 20px;height:54px;display:flex;align-items:center;gap:10px;
34
+ position:sticky;top:0;z-index:100;overflow:hidden}
35
+ .logo{display:flex;align-items:center;gap:8px;text-decoration:none;flex-shrink:0}
36
+ .logo-icon{width:28px;height:28px;background:linear-gradient(135deg,var(--accent),var(--purple));
37
+ border-radius:7px;display:flex;align-items:center;justify-content:center;font-size:14px}
38
+ .logo-text{font-size:.95rem;font-weight:800;color:var(--text);letter-spacing:-.3px}
 
 
 
 
 
39
  .logo-text span{color:var(--accent)}
40
+ .hdr-stack{display:flex;gap:5px;align-items:center;margin-left:6px;flex-wrap:nowrap;overflow:hidden}
41
+ .hs-badge{font-size:.62rem;font-weight:700;padding:2px 8px;border-radius:12px;
42
+ border:1px solid;white-space:nowrap;letter-spacing:.02em}
43
+ .hs-lg{background:rgba(91,143,249,.1);border-color:rgba(91,143,249,.3);color:var(--accent)}
44
+ .hs-lc{background:rgba(245,166,35,.1);border-color:rgba(245,166,35,.3);color:var(--gold)}
45
+ .hs-fb{background:rgba(41,198,212,.1);border-color:rgba(41,198,212,.3);color:var(--teal)}
46
+ .hs-qw{background:rgba(34,212,122,.1);border-color:rgba(34,212,122,.3);color:var(--green);cursor:pointer}
47
+ .hs-qw:hover{background:rgba(34,212,122,.18)}
48
+ #hdr-src{margin-left:auto;font-size:.68rem;padding:3px 10px;border-radius:14px;flex-shrink:0;
49
+ background:rgba(120,128,160,.08);border:1px solid var(--border);color:var(--muted)}
50
+ #hdr-src.loaded{background:rgba(34,212,122,.08);border-color:rgba(34,212,122,.25);color:var(--green)}
51
+
52
+ /* ── LAYOUT ── */
53
+ main{display:grid;grid-template-columns:310px 1fr;height:calc(100vh - 54px);overflow:hidden}
54
+ .panel{padding:18px 16px;overflow-y:auto;height:100%}
55
+ .panel-left{border-right:1px solid var(--border);background:var(--surface);display:flex;flex-direction:column;gap:18px}
56
  .panel-right{background:var(--bg)}
57
 
58
+ /* ── SECTION HEADERS ── */
59
+ .sec-head{display:flex;align-items:center;gap:8px;margin-bottom:12px}
60
+ .sec-icon{width:24px;height:24px;border-radius:6px;display:flex;align-items:center;
61
+ justify-content:center;font-size:12px;flex-shrink:0}
62
  .sec-icon-blue{background:rgba(91,143,249,.15)}
63
+ .sec-icon-green{background:rgba(34,212,122,.15)}
64
+ .sec-icon-gold{background:rgba(245,166,35,.15)}
65
  .sec-icon-purple{background:rgba(167,139,250,.15)}
66
+ .sec-title{font-size:.68rem;font-weight:800;text-transform:uppercase;letter-spacing:.09em;color:var(--sub)}
 
 
 
 
 
 
 
 
 
 
67
 
68
+ /* ── TABS ── */
69
+ .tabs{display:flex;gap:2px;background:rgba(255,255,255,.03);border-radius:8px;
70
+ padding:3px;margin-bottom:14px;border:1px solid var(--border)}
71
+ .tab-btn{flex:1;background:none;border:none;color:var(--muted);font-size:.76rem;
72
+ font-weight:600;padding:6px 8px;border-radius:6px;cursor:pointer;
73
+ transition:all .15s;font-family:inherit;display:flex;align-items:center;
74
+ justify-content:center;gap:5px}
75
+ .tab-btn.active{background:var(--card2);color:var(--text);box-shadow:0 1px 5px rgba(0,0,0,.3)}
76
+
77
+ /* ── DROPZONE ── */
78
+ .dropzone{border:2px dashed var(--border2);border-radius:10px;padding:22px 14px;
79
+ text-align:center;cursor:pointer;transition:all .22s;position:relative;overflow:hidden}
80
+ .dropzone::before{content:"";position:absolute;inset:0;
81
+ background:linear-gradient(90deg,transparent,rgba(91,143,249,.06),transparent);
82
+ background-size:200% 100%;opacity:0;transition:opacity .3s}
83
+ .dropzone:hover::before,.dropzone.drag-over::before{opacity:1;animation:shimmer 1.6s linear infinite}
84
+ .dropzone:hover,.dropzone.drag-over{border-color:var(--accent);background:rgba(91,143,249,.04)}
85
+ .dz-icon{font-size:1.8rem;margin-bottom:6px;display:block;line-height:1}
86
+ .dz-label{font-size:.8rem;color:var(--sub);margin-bottom:3px}
87
  .dz-label strong{color:var(--accent)}
88
+ .dz-hint{font-size:.72rem;color:var(--muted)}
89
+
90
+ /* ── URL ── */
91
+ .url-row{display:flex;gap:8px;margin-bottom:6px}
92
+ input[type=url]{flex:1;background:rgba(255,255,255,.04);border:1px solid var(--border);
93
+ border-radius:7px;padding:8px 11px;color:var(--text);font-size:.82rem;
94
+ font-family:inherit;outline:none;transition:border-color .2s}
95
+ input[type=url]:focus{border-color:var(--accent)}
96
+
97
+ /* ── SOURCE CARD ── */
98
+ #source-card{display:none;background:rgba(34,212,122,.06);border:1px solid rgba(34,212,122,.2);
99
+ border-radius:9px;padding:10px 12px;animation:fadeUp .3s ease}
100
+ .sc-row{display:flex;align-items:center;gap:10px;margin-bottom:5px}
101
+ .sc-icon{font-size:1.4rem;flex-shrink:0}
102
+ .sc-name{font-size:.82rem;font-weight:700;color:var(--text);overflow:hidden;
103
+ text-overflow:ellipsis;white-space:nowrap;max-width:200px}
 
 
 
 
 
104
  .sc-meta{font-size:.72rem;color:var(--teal)}
105
+ .sc-ready{font-size:.72rem;color:var(--green);font-weight:600}
106
+
107
+ /* ── MODEL SELECTOR ── */
108
+ .model-list{display:flex;flex-direction:column;gap:7px}
109
+ .model-card{border:1px solid var(--border2);border-radius:9px;padding:10px 12px;
110
+ cursor:pointer;transition:all .18s;display:flex;align-items:center;gap:10px}
111
+ .model-card:hover{border-color:rgba(91,143,249,.35);background:rgba(91,143,249,.04);
112
+ transform:translateX(2px)}
113
+ .model-card.selected{border-color:var(--accent);background:rgba(91,143,249,.08)}
114
+ .mc-color{width:10px;height:10px;border-radius:50%;flex-shrink:0}
115
+ .mc-body{flex:1;min-width:0}
116
+ .mc-name{font-size:.8rem;font-weight:700;color:var(--text);margin-bottom:1px}
117
+ .mc-desc{font-size:.69rem;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
118
+ .mc-right{display:flex;flex-direction:column;align-items:flex-end;gap:3px;flex-shrink:0}
119
+ .mc-params{font-size:.65rem;font-weight:700;padding:1px 6px;border-radius:5px;
120
+ background:rgba(255,255,255,.06);color:var(--sub)}
121
+ .mc-speed{font-size:.65rem;letter-spacing:1px}
122
+ .mc-check{font-size:.75rem;color:var(--green);opacity:0;transition:opacity .15s}
123
+ .model-card.selected .mc-check{opacity:1}
124
+
125
+ /* ── TECH STACK GRID ── */
126
+ .tech-grid{display:grid;grid-template-columns:1fr 1fr;gap:5px}
127
+ .tg-badge{border-radius:7px;padding:7px 9px;border:1px solid;display:flex;
128
+ align-items:center;gap:6px;transition:.15s}
129
+ .tg-badge:hover{transform:translateY(-1px)}
130
+ .tg-icon{font-size:.85rem;flex-shrink:0}
131
+ .tg-body{}
132
+ .tg-name{font-size:.7rem;font-weight:700;line-height:1.2}
133
+ .tg-sub{font-size:.6rem;color:var(--muted);line-height:1.2}
134
+ .tg-lg{background:rgba(91,143,249,.07);border-color:rgba(91,143,249,.2);color:var(--accent)}
135
+ .tg-lc{background:rgba(245,166,35,.07);border-color:rgba(245,166,35,.2);color:var(--gold)}
136
+ .tg-fb{background:rgba(41,198,212,.07);border-color:rgba(41,198,212,.2);color:var(--teal)}
137
+ .tg-emb{background:rgba(34,212,122,.07);border-color:rgba(34,212,122,.2);color:var(--green)}
138
+ .tg-fl{background:rgba(239,68,68,.07);border-color:rgba(239,68,68,.2);color:#f87171}
139
+ .tg-dk{background:rgba(6,182,212,.07);border-color:rgba(6,182,212,.2);color:#38bdf8}
140
+
141
+ /* ── ARCHITECTURE DIAGRAM ── */
142
+ .arch-card{background:var(--card);border:1px solid var(--border);border-radius:12px;
143
+ padding:14px 16px;margin-bottom:16px}
144
+ .arch-card-hdr{display:flex;align-items:center;gap:8px;margin-bottom:12px}
145
+ .arch-legend{display:flex;gap:10px;flex-wrap:wrap;margin-left:auto}
146
+ .arch-legend-item{font-size:.6rem;color:var(--muted);display:flex;align-items:center;gap:4px}
147
+ .arch-legend-dot{width:7px;height:7px;border-radius:50%}
148
+
149
+ /* ingestion row */
150
+ .arch-ingest-row{display:flex;align-items:center;gap:6px;margin-bottom:8px;flex-wrap:wrap}
151
+ /* pipeline row */
152
+ .arch-pipe-row{display:flex;align-items:center;gap:4px;flex-wrap:wrap}
153
+ .arch-node{border-radius:8px;padding:7px 10px;border:1px solid;text-align:center;
154
+ min-width:74px;transition:all .3s;position:relative}
155
+ .arch-node-icon{font-size:.95rem;display:block;margin-bottom:2px;line-height:1}
156
+ .arch-node-name{font-size:.68rem;font-weight:700;line-height:1.2}
157
+ .arch-node-sub{font-size:.57rem;color:var(--muted);line-height:1.3;margin-top:1px}
158
+ /* node types */
159
+ .an-io{background:rgba(41,198,212,.07);border-color:rgba(41,198,212,.25);color:var(--teal)}
160
+ .an-chunker{background:rgba(245,166,35,.07);border-color:rgba(245,166,35,.25);color:var(--gold)}
161
+ .an-index{background:rgba(41,198,212,.07);border-color:rgba(41,198,212,.25);color:var(--teal)}
162
+ .an-llm{background:rgba(91,143,249,.07);border-color:rgba(91,143,249,.25);color:var(--accent)}
163
+ .an-local{background:rgba(34,212,122,.07);border-color:rgba(34,212,122,.25);color:var(--green)}
164
+ .an-score{background:rgba(245,166,35,.07);border-color:rgba(245,166,35,.25);color:var(--gold)}
165
+ .an-out{background:rgba(167,139,250,.07);border-color:rgba(167,139,250,.25);color:var(--purple)}
166
+ /* active / done states */
167
+ .arch-node.an-running{animation:nodeGlow .9s ease-in-out infinite;border-width:2px}
168
+ .arch-node.an-done{opacity:.55}
169
+ .arch-arr{color:var(--muted);font-size:.75rem;flex-shrink:0;padding:0 1px}
170
+ .arch-vconn{display:flex;align-items:center;justify-content:flex-start;
171
+ padding-left:36px;margin:4px 0;color:var(--muted);font-size:.75rem}
172
+
173
+ /* ── QUESTION CARD ── */
174
+ .q-card{background:var(--card);border:1px solid var(--border);border-radius:10px;
175
+ padding:14px;margin-bottom:14px}
176
+ .q-label{font-size:.68rem;font-weight:700;color:var(--muted);text-transform:uppercase;
177
+ letter-spacing:.07em;margin-bottom:8px}
178
+ textarea{width:100%;background:rgba(255,255,255,.03);border:1px solid var(--border);
179
+ border-radius:7px;padding:9px 11px;color:var(--text);font-size:.84rem;
180
+ font-family:inherit;outline:none;resize:vertical;min-height:72px;
181
+ transition:border-color .2s;line-height:1.5}
182
+ textarea:focus{border-color:var(--accent)}
183
+ .q-footer{display:flex;align-items:center;gap:10px;margin-top:10px}
184
+
185
+ /* ── BUTTONS ── */
186
+ .btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:7px;
187
+ border:none;font-size:.82rem;font-weight:600;cursor:pointer;
188
+ transition:all .15s;font-family:inherit}
189
+ .btn-primary{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;
190
+ box-shadow:0 2px 10px rgba(91,143,249,.3)}
191
+ .btn-primary:hover{transform:translateY(-1px);box-shadow:0 4px 16px rgba(91,143,249,.4)}
192
+ .btn-primary:disabled{opacity:.45;cursor:default;transform:none;box-shadow:none}
193
+ .btn-sm{padding:6px 12px;font-size:.76rem}
194
+ .btn-ghost{background:rgba(255,255,255,.05);color:var(--sub);border:1px solid var(--border)}
195
+ .btn-ghost:hover{background:rgba(255,255,255,.09);color:var(--text)}
196
+ .spinner{width:14px;height:14px;border:2px solid rgba(255,255,255,.3);
197
+ border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite}
198
+
199
+ /* ── PIPELINE ── */
200
+ #pipeline{display:none;margin-bottom:12px}
201
+ .pipe-row{display:flex;align-items:center;gap:4px;flex-wrap:wrap;
202
+ background:var(--card);border:1px solid var(--border);
203
+ border-radius:9px;padding:9px 12px}
204
+ .pipe-step{display:flex;align-items:center;gap:5px;font-size:.75rem;font-weight:600;
205
+ color:var(--muted);padding:4px 8px;border-radius:6px;transition:all .2s}
206
+ .pipe-step.ps-active{color:var(--accent);background:rgba(91,143,249,.1)}
207
+ .pipe-step.ps-done{color:var(--green);opacity:.7}
208
+ .step-dot{width:7px;height:7px;border-radius:50%;background:currentColor;flex-shrink:0}
209
+ .ps-active .step-dot{animation:pulse .6s ease-in-out infinite}
210
+ .pipe-arrow{color:var(--border2);font-size:.65rem}
211
+
212
+ /* ── MESSAGES ── */
213
+ .msg{border-radius:7px;padding:8px 12px;font-size:.78rem;margin-top:8px;line-height:1.5;
214
+ animation:fadeUp .25s ease}
215
+ .msg-ok{background:rgba(34,212,122,.08);border:1px solid rgba(34,212,122,.2);color:var(--green)}
216
+ .msg-err{background:rgba(240,92,92,.08);border:1px solid rgba(240,92,92,.2);color:var(--red)}
217
+ .msg-info{color:var(--muted);font-size:.74rem;margin-top:6px}
218
+
219
+ /* ── TRACE ── */
220
+ #trace-wrap{display:none;margin-bottom:14px}
221
+ .trace-hdr{display:flex;align-items:center;gap:8px;padding:8px 12px;
222
+ background:var(--card);border:1px solid var(--border);
223
+ border-radius:9px 9px 0 0;border-bottom-color:transparent}
224
+ .trace-title{font-size:.72rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em}
225
+ #trace-log{background:rgba(0,0,0,.2);border:1px solid var(--border);border-top:none;
226
+ border-radius:0 0 9px 9px;padding:8px 10px;max-height:200px;overflow-y:auto}
227
+ .t-step{display:flex;align-items:flex-start;gap:8px;font-size:.75rem;
228
+ padding:5px 0;border-bottom:1px solid rgba(255,255,255,.04);animation:fadeUp .2s ease}
229
  .t-step:last-child{border-bottom:none}
230
+ .t-badge{font-size:.58rem;font-weight:800;text-transform:uppercase;padding:2px 6px;
231
+ border-radius:4px;flex-shrink:0;margin-top:1px;letter-spacing:.03em}
232
+ .b-planner{background:rgba(91,143,249,.15);color:var(--accent)}
233
+ .b-retriever{background:rgba(41,198,212,.15);color:var(--teal)}
234
+ .b-grader{background:rgba(245,166,35,.15);color:var(--gold)}
235
+ .b-generator{background:rgba(34,212,122,.15);color:var(--green)}
236
+ .b-critic{background:rgba(167,139,250,.15);color:var(--purple)}
237
+ .b-error{background:rgba(240,92,92,.15);color:var(--red)}
238
+ .t-msg{flex:1;color:var(--sub);line-height:1.45}
239
+ .t-lat{color:rgba(120,128,160,.5);font-size:.62rem;white-space:nowrap;margin-left:4px}
240
+
241
+ /* ── ANSWER ── */
242
+ #answer-wrap{display:none;background:var(--card);border:1px solid var(--border);
243
+ border-radius:10px;overflow:hidden;animation:fadeUp .35s ease}
244
+ .ans-header{display:flex;align-items:center;gap:10px;padding:12px 16px;
245
+ border-bottom:1px solid var(--border)}
246
+ .ans-label{font-size:.68rem;font-weight:800;text-transform:uppercase;letter-spacing:.07em;color:var(--muted)}
247
+ .ans-actions{margin-left:auto;display:flex;gap:6px;align-items:center}
248
+ #answer-text{padding:16px;font-size:.88rem;line-height:1.75;white-space:pre-wrap;
249
+ word-break:break-word;color:var(--text)}
250
+ #verdict{margin:0 16px 12px;font-size:.72rem;font-weight:700;padding:4px 12px;
251
+ border-radius:5px;display:inline-block}
252
+ .v-ok{background:rgba(34,212,122,.12);color:var(--green)}
253
+ .v-warn{background:rgba(245,166,35,.12);color:var(--gold)}
254
+
255
+ /* ── Q ERROR ── */
256
+ #q-err{font-size:.76rem;color:var(--red)}
257
+
258
+ @media(max-width:800px){
259
+ main{grid-template-columns:1fr;height:auto}
 
 
 
 
 
 
260
  .panel-left{border-right:none;border-bottom:1px solid var(--border)}
261
+ .hdr-stack{display:none}
262
+ .arch-pipe-row{gap:3px}
263
+ .arch-node{min-width:60px;padding:5px 6px}
264
  }
265
  </style>
266
  </head>
267
  <body>
268
 
 
269
  <header>
270
  <a class="logo" href="#">
271
  <div class="logo-icon">&#129504;</div>
272
  <span class="logo-text">Doc<span>Mind</span></span>
273
  </a>
274
+ <div class="hdr-stack">
275
+ <span class="hs-badge hs-lg">&#128279; LangGraph</span>
276
+ <span class="hs-badge hs-lc">&#9961; LangChain LCEL</span>
277
+ <span class="hs-badge hs-fb">&#128451; FAISS+BM25</span>
278
+ <span class="hs-badge hs-qw" id="hdr-model-badge">&#9889; Qwen 2.5&#xB7;7B</span>
279
  </div>
280
+ <span class="badge" id="hdr-src"
281
+ style="margin-left:auto;font-size:.68rem;padding:3px 10px;border-radius:14px;
282
+ background:rgba(120,128,160,.08);border:1px solid #252836;color:#7880a0">
283
+ No source loaded
284
+ </span>
285
  </header>
286
 
287
  <main>
288
+ <!-- ══════════ LEFT PANEL ══════════ -->
289
  <div class="panel panel-left">
 
 
 
 
 
 
 
 
 
290
 
291
+ <!-- KNOWLEDGE BASE -->
292
+ <div>
293
+ <div class="sec-head">
294
+ <div class="sec-icon sec-icon-blue">&#128218;</div>
295
+ <span class="sec-title">Knowledge Base</span>
296
+ </div>
297
+ <div class="tabs">
298
+ <button class="tab-btn active" onclick="switchTab(this,\'pdf\')">&#128196;&ensp;Upload PDF</button>
299
+ <button class="tab-btn" onclick="switchTab(this,\'url\')">&#127760;&ensp;Paste URL</button>
300
+ </div>
301
+ <div id="tab-pdf">
302
+ <div class="dropzone" id="dz"
303
+ onclick="document.getElementById(\'fi\').click()"
304
+ ondragover="dg(event,true)" ondragleave="dg(event,false)" ondrop="dp(event)">
305
+ <span class="dz-icon">&#128196;</span>
306
+ <div class="dz-label"><strong>Click to browse</strong> or drag &amp; drop</div>
307
+ <div class="dz-hint">PDF only &middot; max 10 MB</div>
308
+ </div>
309
+ <input type="file" id="fi" accept=".pdf" style="display:none" onchange="fc(event)"/>
310
+ <div id="pdf-msg"></div>
311
+ </div>
312
+ <div id="tab-url" style="display:none">
313
+ <div class="url-row">
314
+ <input type="url" id="url-inp" placeholder="https://en.wikipedia.org/wiki/..."
315
+ onkeydown="if(event.key===\'Enter\')fetchURL()"/>
316
+ <button class="btn btn-primary btn-sm" id="url-btn" onclick="fetchURL()">Fetch</button>
317
+ </div>
318
+ <div class="msg-info" style="margin-bottom:4px">Wikipedia, gov sites &amp; docs work best.</div>
319
+ <div id="url-msg"></div>
320
+ </div>
321
+ <div id="source-card">
322
+ <div class="sc-row">
323
+ <div class="sc-icon" id="sc-icon">&#128196;</div>
324
+ <div class="sc-info">
325
+ <div class="sc-name" id="source-name"></div>
326
+ <div class="sc-meta" id="source-chunks"></div>
327
+ </div>
328
+ </div>
329
+ <div class="sc-ready">&#10003;&ensp;Ready for questions</div>
330
  </div>
 
 
331
  </div>
332
 
333
+ <!-- MODEL SELECTOR -->
334
+ <div>
335
+ <div class="sec-head">
336
+ <div class="sec-icon sec-icon-gold">&#129302;</div>
337
+ <span class="sec-title">Select Model</span>
338
+ </div>
339
+ <div class="model-list" id="model-list">
340
+ <div class="model-card selected" onclick="selectModel(\'qwen-7b\',this)"
341
+ data-model="qwen-7b" data-label="Qwen 2.5&#xB7;7B">
342
+ <div class="mc-color" style="background:#5b8ff9"></div>
343
+ <div class="mc-body">
344
+ <div class="mc-name">Qwen 2.5 &middot; 7B</div>
345
+ <div class="mc-desc">Default &middot; fast &amp; free</div>
346
+ </div>
347
+ <div class="mc-right">
348
+ <span class="mc-params">7B</span>
349
+ <span class="mc-speed" style="color:#f5a623">&#9889;&#9889;&#9889;</span>
350
+ </div>
351
+ <span class="mc-check">&#10003;</span>
352
+ </div>
353
+ <div class="model-card" onclick="selectModel(\'mistral-nemo\',this)"
354
+ data-model="mistral-nemo" data-label="Mistral Nemo&#xB7;12B">
355
+ <div class="mc-color" style="background:#a78bfa"></div>
356
+ <div class="mc-body">
357
+ <div class="mc-name">Mistral Nemo &middot; 12B</div>
358
+ <div class="mc-desc">Stronger reasoning</div>
359
+ </div>
360
+ <div class="mc-right">
361
+ <span class="mc-params">12B</span>
362
+ <span class="mc-speed" style="color:#f5a623">&#9889;&#9889;</span>
363
+ </div>
364
+ <span class="mc-check">&#10003;</span>
365
+ </div>
366
+ <div class="model-card" onclick="selectModel(\'phi-3-mini\',this)"
367
+ data-model="phi-3-mini" data-label="Phi-3.5 Mini&#xB7;3.8B">
368
+ <div class="mc-color" style="background:#22d47a"></div>
369
+ <div class="mc-body">
370
+ <div class="mc-name">Phi-3.5 Mini &middot; 3.8B</div>
371
+ <div class="mc-desc">Ultra-fast &amp; focused</div>
372
+ </div>
373
+ <div class="mc-right">
374
+ <span class="mc-params">3.8B</span>
375
+ <span class="mc-speed" style="color:#f5a623">&#9889;&#9889;&#9889;</span>
376
+ </div>
377
+ <span class="mc-check">&#10003;</span>
378
+ </div>
379
  </div>
 
 
380
  </div>
381
 
382
+ <!-- TECH STACK -->
383
+ <div>
384
+ <div class="sec-head">
385
+ <div class="sec-icon sec-icon-purple">&#128736;</div>
386
+ <span class="sec-title">Powered By</span>
387
+ </div>
388
+ <div class="tech-grid">
389
+ <div class="tg-badge tg-lg">
390
+ <span class="tg-icon">&#128279;</span>
391
+ <div class="tg-body"><div class="tg-name">LangGraph 0.2</div><div class="tg-sub">StateGraph Β· 5 nodes</div></div>
392
+ </div>
393
+ <div class="tg-badge tg-lc">
394
+ <span class="tg-icon">&#9961;</span>
395
+ <div class="tg-body"><div class="tg-name">LangChain LCEL</div><div class="tg-sub">prompt | llm | parser</div></div>
396
+ </div>
397
+ <div class="tg-badge tg-fb">
398
+ <span class="tg-icon">&#128451;</span>
399
+ <div class="tg-body"><div class="tg-name">FAISS + BM25</div><div class="tg-sub">RRF hybrid retrieval</div></div>
400
+ </div>
401
+ <div class="tg-badge tg-emb">
402
+ <span class="tg-icon">&#129688;</span>
403
+ <div class="tg-body"><div class="tg-name">HF Embeddings</div><div class="tg-sub">bge-small-en-v1.5</div></div>
404
+ </div>
405
+ <div class="tg-badge tg-fl">
406
+ <span class="tg-icon">&#127381;</span>
407
+ <div class="tg-body"><div class="tg-name">Flask 3.1</div><div class="tg-sub">+ Gunicorn WSGI</div></div>
408
+ </div>
409
+ <div class="tg-badge tg-dk">
410
+ <span class="tg-icon">&#128058;</span>
411
+ <div class="tg-body"><div class="tg-name">Docker</div><div class="tg-sub">HuggingFace Spaces</div></div>
412
  </div>
413
  </div>
 
414
  </div>
415
+
416
  </div>
417
 
418
+ <!-- ══════════ RIGHT PANEL ══════════ -->
419
  <div class="panel panel-right">
420
+
421
+ <!-- ARCHITECTURE DIAGRAM -->
422
+ <div class="arch-card">
423
+ <div class="arch-card-hdr">
424
+ <div class="sec-icon sec-icon-blue" style="width:20px;height:20px;font-size:11px">&#128202;</div>
425
+ <span class="sec-title">Pipeline Architecture</span>
426
+ <div class="arch-legend">
427
+ <span class="arch-legend-item"><span class="arch-legend-dot" style="background:#5b8ff9"></span>LLM (Qwen)</span>
428
+ <span class="arch-legend-item"><span class="arch-legend-dot" style="background:#22d47a"></span>Local</span>
429
+ <span class="arch-legend-item"><span class="arch-legend-dot" style="background:#f5a623"></span>Score-based</span>
430
+ </div>
431
+ </div>
432
+
433
+ <!-- Ingestion row -->
434
+ <div class="arch-ingest-row">
435
+ <div class="arch-node an-io">
436
+ <span class="arch-node-icon">&#128196;</span>
437
+ <div class="arch-node-name">Source</div>
438
+ <div class="arch-node-sub">PDF &middot; URL</div>
439
+ </div>
440
+ <span class="arch-arr">&#8594;</span>
441
+ <div class="arch-node an-chunker">
442
+ <span class="arch-node-icon">&#9986;</span>
443
+ <div class="arch-node-name">Chunker</div>
444
+ <div class="arch-node-sub">RCTextSplitter</div>
445
+ </div>
446
+ <span class="arch-arr">&#8594;</span>
447
+ <div class="arch-node an-index">
448
+ <span class="arch-node-icon">&#128451;</span>
449
+ <div class="arch-node-name">Hybrid Index</div>
450
+ <div class="arch-node-sub">FAISS + BM25</div>
451
+ </div>
452
+ <span class="arch-arr" style="font-size:.65rem;color:#29c6d4">&#8595; hybrid_search</span>
453
+ </div>
454
+
455
+ <!-- Agent pipeline row -->
456
+ <div class="arch-pipe-row">
457
+ <div class="arch-node an-llm" data-arch="planner" id="anode-planner">
458
+ <span class="arch-node-icon">&#127919;</span>
459
+ <div class="arch-node-name">Planner</div>
460
+ <div class="arch-node-sub">LLM &middot; 0.3</div>
461
+ </div>
462
+ <span class="arch-arr">&#8594;</span>
463
+ <div class="arch-node an-local" data-arch="retriever" id="anode-retriever">
464
+ <span class="arch-node-icon">&#128269;</span>
465
+ <div class="arch-node-name">Retriever</div>
466
+ <div class="arch-node-sub">Local &middot; RRF</div>
467
+ </div>
468
+ <span class="arch-arr">&#8594;</span>
469
+ <div class="arch-node an-score" data-arch="grader" id="anode-grader">
470
+ <span class="arch-node-icon">&#9878;</span>
471
+ <div class="arch-node-name">Grader</div>
472
+ <div class="arch-node-sub">Score &middot; 0ms</div>
473
+ </div>
474
+ <span class="arch-arr">&#8594;</span>
475
+ <div class="arch-node an-llm" data-arch="generator" id="anode-generator">
476
+ <span class="arch-node-icon">&#9997;</span>
477
+ <div class="arch-node-name">Generator</div>
478
+ <div class="arch-node-sub">LLM &middot; 0.4</div>
479
+ </div>
480
+ <span class="arch-arr">&#8594;</span>
481
+ <div class="arch-node an-llm" data-arch="critic" id="anode-critic">
482
+ <span class="arch-node-icon">&#128300;</span>
483
+ <div class="arch-node-name">Critic</div>
484
+ <div class="arch-node-sub">LLM &middot; 0.1</div>
485
+ </div>
486
+ <span class="arch-arr">&#8594;</span>
487
+ <div class="arch-node an-out" id="anode-answer">
488
+ <span class="arch-node-icon">&#128203;</span>
489
+ <div class="arch-node-name">Answer</div>
490
+ <div class="arch-node-sub">Cited &middot; Verified</div>
491
+ </div>
492
+ </div>
493
+ </div>
494
+
495
+ <!-- RESEARCH QUESTION -->
496
  <div class="sec-head">
497
  <div class="sec-icon sec-icon-purple">&#128269;</div>
498
  <span class="sec-title">Research Query</span>
499
  </div>
 
500
  <div class="q-card">
501
  <div class="q-label">Your Question</div>
502
  <textarea id="q-inp" rows="3"
 
508
  </div>
509
  </div>
510
 
511
+ <!-- AGENT PIPELINE PROGRESS -->
512
  <div id="pipeline">
513
  <div class="pipe-row">
514
+ <div class="pipe-step" id="ps-planner"><span class="step-dot"></span>&#127919;&ensp;Planner</div>
515
  <div class="pipe-arrow">&#8594;</div>
516
  <div class="pipe-step" id="ps-retriever"><span class="step-dot"></span>&#128269;&ensp;Retriever</div>
517
  <div class="pipe-arrow">&#8594;</div>
 
528
  <div class="trace-hdr">
529
  <span class="trace-title">&#128240;&ensp;Agent Trace</span>
530
  </div>
531
+ <div id="trace-log"></div>
532
  </div>
533
 
534
  <!-- ANSWER -->
535
  <div id="answer-wrap">
536
  <div class="ans-header">
537
+ <span class="ans-label">&#129504;&ensp;Answer</span>
 
 
 
538
  <div class="ans-actions">
539
+ <button class="btn btn-ghost btn-sm" onclick="copyAns(this)">&#128203;&ensp;Copy</button>
 
540
  </div>
541
  </div>
542
+ <div id="answer-text"></div>
543
+ <div id="verdict"></div>
 
544
  </div>
545
+
546
  </div>
547
  </main>
548
 
549
  <script>
 
550
  const esc=s=>String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
551
+ let pollTimer=null, seen=0, selectedModel="qwen-7b";
552
 
553
+ // ── Tab switching ───────────────────────────────────────────────────────────
 
 
554
  function switchTab(btn,name){
555
  document.querySelectorAll(".tab-btn").forEach(b=>b.classList.remove("active"));
556
  btn.classList.add("active");
 
558
  document.getElementById("tab-url").style.display=name==="url"?"":"none";
559
  }
560
 
561
+ // ── Drag & drop ─────────────────────────────────────────────────────────────
562
  function dg(e,over){e.preventDefault();document.getElementById("dz").classList[over?"add":"remove"]("drag-over");}
563
  function dp(e){e.preventDefault();document.getElementById("dz").classList.remove("drag-over");const f=e.dataTransfer.files[0];if(f)up(f);}
564
  function fc(e){if(e.target.files[0])up(e.target.files[0]);}
565
 
566
+ // ── PDF upload ───────────────────────────────────────────────────────────────
567
  async function up(file){
568
+ if(!file.name.toLowerCase().endsWith(".pdf")){sm("pdf-msg","error","Only PDF files are supported.");return;}
569
+ sm("pdf-msg","info","Uploading "+file.name+"…");
570
  const fd=new FormData();fd.append("file",file);
571
  try{
572
  const r=await fetch("/api/upload",{method:"POST",body:fd});
573
  const d=await r.json();
574
+ if(d.error){sm("pdf-msg","error",d.error);return;}
575
  setSource(d.filename,d.chunks,"pdf");
576
+ sm("pdf-msg","ok","&#10003;&ensp;Indexed "+d.chunks+" chunks from \\""+d.filename+"\\"");
577
+ }catch(e){sm("pdf-msg","error","Upload failed: "+e.message);}
578
  }
579
 
580
+ // ── URL fetch ────────────────────────────────────────────────────────────────
581
  async function fetchURL(){
582
  const url=document.getElementById("url-inp").value.trim();
583
+ if(!url){sm("url-msg","error","Please enter a URL.");return;}
584
+ document.getElementById("url-btn").disabled=true;
585
+ sm("url-msg","info","Fetching page…");
 
586
  try{
587
  const r=await fetch("/api/ingest_url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url})});
588
  const d=await r.json();
589
+ if(d.error){sm("url-msg","error",d.error);return;}
590
  setSource(d.url,d.chunks,"url");
591
+ sm("url-msg","ok","&#10003;&ensp;Indexed "+d.chunks+" chunks");
592
+ }catch(e){sm("url-msg","error","Failed: "+e.message);}
593
+ finally{document.getElementById("url-btn").disabled=false;}
594
  }
595
 
596
+ // ── Source card ──────────────────────────────────────────────────────────────
597
  function setSource(name,chunks,type){
598
  document.getElementById("source-name").textContent=name;
599
  document.getElementById("source-chunks").textContent=chunks+" chunks indexed";
600
+ document.getElementById("sc-icon").textContent=type==="pdf"?"πŸ“„":"🌐";
601
  document.getElementById("source-card").style.display="block";
602
  const p=document.getElementById("hdr-src");
603
+ p.textContent=name.length>28?name.slice(0,28)+"…":name;
604
  p.classList.add("loaded");
605
  }
606
 
607
+ // ── Model selector ───────────────────────────────────────────────────────────
608
+ function selectModel(key,card){
609
+ document.querySelectorAll(".model-card").forEach(c=>c.classList.remove("selected"));
610
+ card.classList.add("selected");
611
+ selectedModel=key;
612
+ const label=card.dataset.label||key;
613
+ document.getElementById("hdr-model-badge").textContent="⚑ "+label;
614
+ // persist to server (best-effort, also passed per-request)
615
+ fetch("/api/set_model",{method:"POST",headers:{"Content-Type":"application/json"},
616
+ body:JSON.stringify({model:key})}).catch(()=>{});
617
  }
618
 
619
+ // ── Architecture node states ─────────────────────────────────────────────────
620
+ const ARCH_AGENTS=["planner","retriever","grader","generator","critic"];
621
+ function archReset(){
622
+ ARCH_AGENTS.forEach(a=>{
623
+ const n=document.getElementById("anode-"+a);
624
+ if(n){n.classList.remove("an-running","an-done");}
625
+ });
626
+ const out=document.getElementById("anode-answer");
627
+ if(out)out.classList.remove("an-running","an-done");
628
+ }
629
+ function archSetActive(agent){
630
+ ARCH_AGENTS.forEach(a=>{
631
+ const n=document.getElementById("anode-"+a);
632
+ if(!n)return;
633
+ if(a===agent){n.classList.add("an-running");n.classList.remove("an-done");}
634
+ else if(n.classList.contains("an-running")){n.classList.remove("an-running");n.classList.add("an-done");}
635
+ });
636
+ }
637
+ function archAllDone(){
638
+ ARCH_AGENTS.forEach(a=>{
639
+ const n=document.getElementById("anode-"+a);
640
+ if(n){n.classList.remove("an-running");n.classList.add("an-done");}
641
+ });
642
+ const out=document.getElementById("anode-answer");
643
+ if(out){out.classList.add("an-running");setTimeout(()=>{out.classList.remove("an-running");},1200);}
644
+ }
645
+
646
+ // ── Pipeline step states ─────────────────────────────────────────────────────
647
+ function pipeReset(){
648
+ ARCH_AGENTS.forEach(a=>{
649
+ const el=document.getElementById("ps-"+a);
650
+ if(el)el.className="pipe-step";
651
+ });
652
+ }
653
+ function pipeSetActive(agent){
654
+ ARCH_AGENTS.forEach(a=>{
655
+ const el=document.getElementById("ps-"+a);
656
+ if(!el)return;
657
+ if(a===agent)el.className="pipe-step ps-active";
658
+ else if(el.classList.contains("ps-active")){el.className="pipe-step ps-done";}
659
+ });
660
+ }
661
+ function pipeAllDone(){
662
+ ARCH_AGENTS.forEach(a=>{
663
+ const el=document.getElementById("ps-"+a);
664
+ if(el)el.className="pipe-step ps-done";
665
+ });
666
+ }
667
+
668
+ // ── Question submit ──────────────────────────────────────────────────────────
669
  function qk(e){if(e.key==="Enter"&&!e.shiftKey){e.preventDefault();ask();}}
670
 
671
  async function ask(){
 
675
 
676
  const btn=document.getElementById("ask-btn");
677
  btn.disabled=true;
678
+ btn.innerHTML=\'<span class="spinner"></span>&ensp;Thinking…\';
679
 
680
  document.getElementById("pipeline").style.display="block";
681
  document.getElementById("trace-wrap").style.display="block";
682
+ document.getElementById("trace-log").innerHTML=
683
+ \'<div class="t-step"><span class="t-msg" style="color:var(--muted)">Starting agents…</span></div>\';
684
  document.getElementById("answer-wrap").style.display="none";
685
+ pipeReset(); archReset(); seen=0; clearInterval(pollTimer);
 
686
 
687
  try{
688
+ const r=await fetch("/api/research",{
689
+ method:"POST",headers:{"Content-Type":"application/json"},
690
+ body:JSON.stringify({question:q,model:selectedModel})
691
+ });
692
  const d=await r.json();
693
+ if(d.error){traceErr(d.error);resetBtn();return;}
694
  pollTimer=setInterval(()=>poll(d.query_id),1500);
695
+ }catch(e){traceErr("Network error: "+e.message);resetBtn();}
696
  }
697
 
698
+ function resetBtn(){
699
  const btn=document.getElementById("ask-btn");
700
  btn.disabled=false;
701
+ btn.innerHTML=\'&#9889;&ensp;Ask\';
702
  }
703
 
704
+ // ── Polling ──────────────────────────────────────────────────────────────────
705
  async function poll(qid){
706
  try{
707
  const r=await fetch("/api/trace/"+qid);
708
+ if(!r.ok){traceErr("Server error "+r.status);clearInterval(pollTimer);resetBtn();return;}
709
  const d=await r.json();
710
  renderTrace(d.trace||[]);
711
  if(["complete","error"].includes(d.status)){
712
+ clearInterval(pollTimer);resetBtn();
713
+ if(d.status==="complete"&&d.result){renderAnswer(d.result);archAllDone();pipeAllDone();}
714
+ else if(d.status==="error"&&d.result){traceErr(d.result.error||"An error occurred.");}
 
715
  }
716
+ }catch(e){traceErr("Poll error: "+e.message);clearInterval(pollTimer);resetBtn();}
717
+ }
718
+
719
+ // ── Trace rendering ──────────────────────────────────────────────────────────
720
+ function traceErr(msg){
721
+ const log=document.getElementById("trace-log");
722
+ log.innerHTML+=\'<div class="t-step"><span class="t-badge b-error">error</span><span class="t-msg" style="color:var(--red)">\'+esc(msg)+\'</span></div>\';
723
+ log.scrollTop=log.scrollHeight;
724
  }
725
 
 
726
  function renderTrace(steps){
727
  if(!steps.length)return;
728
  const log=document.getElementById("trace-log");
729
  if(seen===0)log.innerHTML="";
730
  for(let i=seen;i<steps.length;i++){
731
  const s=steps[i];
732
+ archSetActive(s.agent);
733
+ pipeSetActive(s.agent);
734
+ const lat=s.latency_ms>0?\'<span class="t-lat">\'+s.latency_ms+\'ms</span>\':"";
735
+ log.innerHTML+=\'<div class="t-step"><span class="t-badge b-\'+s.agent+\'">\'+s.agent+\'</span><span class="t-msg">\'+esc(s.message)+\'</span>\'+lat+\'</div>\';
 
 
 
 
 
 
736
  }
737
+ seen=steps.length;log.scrollTop=log.scrollHeight;
 
 
 
 
 
 
 
738
  }
739
 
740
+ // ── Answer rendering ─────────────────────────────────────────────────────────
741
  function renderAnswer(result){
742
  document.getElementById("answer-wrap").style.display="block";
743
  document.getElementById("answer-text").textContent=result.generation||"No answer generated.";
744
+ const v=document.getElementById("verdict");
745
+ if(result.verdict==="APPROVED"){v.className="v-ok";v.textContent="βœ“ High confidence";}
746
+ else if(result.verdict){v.className="v-warn";v.textContent="⚠ Low confidence β€” verify with source";}
747
+ else{v.textContent="";}
748
+ document.getElementById("answer-wrap").scrollIntoView({behavior:"smooth",block:"nearest"});
749
  }
750
 
751
+ // ── Copy answer ──────────────────────────────────────────────────────────────
752
+ function copyAns(btn){
753
  const text=document.getElementById("answer-text").textContent;
754
+ navigator.clipboard.writeText(text).then(()=>{
755
+ btn.textContent="βœ“ Copied!";
756
+ setTimeout(()=>{btn.innerHTML="&#128203;&ensp;Copy";},1800);
757
+ });
 
 
758
  }
759
 
760
+ // ── Show message ─────────────────────────────────────────────────────────────
761
+ function sm(id,type,msg){
762
  const el=document.getElementById(id);
763
+ if(type==="ok")el.innerHTML=\'<div class="msg msg-ok">\'+msg+\'</div>\';
764
+ else if(type==="error")el.innerHTML=\'<div class="msg msg-err">\'+esc(msg)+\'</div>\';
765
+ else el.innerHTML=\'<div class="msg-info">\'+esc(msg)+\'</div>\';
766
  }
767
  </script>
768
  </body>
769
  </html>
770
  '''
771
 
772
+ out = pathlib.Path(__file__).parent / "templates" / "index.html"
773
+ out.write_text(HTML, encoding="utf-8")
774
+ print(f"Done, size: {len(HTML)}")