Spaces:
Sleeping
Sleeping
Commit Β·
f9b41cf
1
Parent(s): 4654a4b
Update 2026-03-22 21:26:46
Browse files- agents/critic.py +3 -16
- agents/generator.py +3 -15
- agents/llm_factory.py +51 -7
- agents/planner.py +3 -16
- app.py +34 -3
- templates/index.html +566 -321
- write_html.py +570 -324
agents/critic.py
CHANGED
|
@@ -1,15 +1,11 @@
|
|
| 1 |
"""
|
| 2 |
Critic agent β LangChain LCEL chain.
|
| 3 |
|
| 4 |
-
|
| 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 =
|
| 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 |
-
|
| 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
|
|
|
|
| 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 |
-
|
| 6 |
-
|
|
|
|
| 7 |
"""
|
| 8 |
import os
|
| 9 |
from langchain_openai import ChatOpenAI
|
| 10 |
|
| 11 |
_BASE_URL = "https://router.huggingface.co/v1"
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
|
| 15 |
def get_llm(temperature: float = 0.7, max_tokens: int = 512) -> ChatOpenAI:
|
| 16 |
-
"""Return a LangChain ChatOpenAI
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
-
.with_retry()
|
| 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=
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 23 |
-
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.
|
|
|
|
|
|
|
| 24 |
@keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}
|
| 25 |
|
| 26 |
-
/* ββ HEADER ββ
|
| 27 |
-
header{
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 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 |
-
.
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
.
|
| 44 |
-
.
|
| 45 |
-
.
|
| 46 |
-
|
| 47 |
-
.
|
| 48 |
-
#hdr-src{margin-left:auto
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
| 54 |
.panel-right{background:var(--bg)}
|
| 55 |
|
| 56 |
-
/* ββ SECTION HEADERS ββ
|
| 57 |
-
.sec-head{display:flex;align-items:center;gap:8px;margin-bottom:
|
| 58 |
-
.sec-icon{width:
|
| 59 |
-
justify-content:center;font-size:
|
| 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:.
|
| 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 |
-
/* ββ
|
| 75 |
-
.
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
.
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
.dropzone
|
| 85 |
-
|
| 86 |
-
.dropzone
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
.dz-label strong{color:var(--accent)}
|
| 90 |
-
.dz-hint{font-size:.72rem;color:var(--muted)
|
| 91 |
-
|
| 92 |
-
/* ββ URL
|
| 93 |
-
.url-row{display:flex;gap:8px;margin-bottom:
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
/* ββ SOURCE
|
| 100 |
-
#source-card{
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 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{
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
.
|
| 119 |
-
|
| 120 |
-
.
|
| 121 |
-
.
|
| 122 |
-
.
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
.
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
.
|
| 129 |
-
|
| 130 |
-
.
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
.
|
| 134 |
-
.
|
| 135 |
-
|
| 136 |
-
.
|
| 137 |
-
.
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
.
|
| 142 |
-
|
| 143 |
-
.
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
.
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
.
|
| 163 |
-
.
|
| 164 |
-
.
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
.
|
| 170 |
-
.
|
| 171 |
-
|
| 172 |
-
.
|
| 173 |
-
|
| 174 |
-
.
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
.t-step:last-child{border-bottom:none}
|
| 177 |
-
.t-
|
| 178 |
-
|
| 179 |
-
.
|
| 180 |
-
|
| 181 |
-
.
|
| 182 |
-
.
|
| 183 |
-
.
|
| 184 |
-
.
|
| 185 |
-
.
|
| 186 |
-
.
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
.ans-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
.
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 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">🧠</div>
|
| 223 |
<span class="logo-text">Doc<span>Mind</span></span>
|
| 224 |
</a>
|
| 225 |
-
<
|
| 226 |
-
|
| 227 |
-
<span class="badge
|
|
|
|
|
|
|
| 228 |
</div>
|
| 229 |
-
<span class="badge
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
</header>
|
| 231 |
|
| 232 |
<main>
|
| 233 |
-
<!--
|
| 234 |
<div class="panel panel-left">
|
| 235 |
-
<div class="sec-head">
|
| 236 |
-
<div class="sec-icon sec-icon-blue">📚</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')">📄 Upload PDF</button>
|
| 242 |
-
<button class="tab-btn" onclick="switchTab(this,'url')">🌐 Paste URL</button>
|
| 243 |
-
</div>
|
| 244 |
|
| 245 |
-
<!--
|
| 246 |
-
<div
|
| 247 |
-
<div class="
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<!--
|
| 259 |
-
<div
|
| 260 |
-
<div class="
|
| 261 |
-
<
|
| 262 |
-
|
| 263 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<!--
|
| 270 |
-
<div
|
| 271 |
-
<div class="
|
| 272 |
-
<div class="
|
| 273 |
-
<
|
| 274 |
-
|
| 275 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
</div>
|
| 277 |
</div>
|
| 278 |
-
<div class="sc-ready">✓ Ready for questions</div>
|
| 279 |
</div>
|
|
|
|
| 280 |
</div>
|
| 281 |
|
| 282 |
-
<!--
|
| 283 |
<div class="panel panel-right">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
<div class="sec-head">
|
| 285 |
<div class="sec-icon sec-icon-purple">🔍</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>&#
|
| 304 |
<div class="pipe-arrow">→</div>
|
| 305 |
<div class="pipe-step" id="ps-retriever"><span class="step-dot"></span>🔍 Retriever</div>
|
| 306 |
<div class="pipe-arrow">→</div>
|
|
@@ -317,35 +525,29 @@ textarea::placeholder{color:var(--muted)}
|
|
| 317 |
<div class="trace-hdr">
|
| 318 |
<span class="trace-title">📰 Agent Trace</span>
|
| 319 |
</div>
|
| 320 |
-
<div
|
| 321 |
</div>
|
| 322 |
|
| 323 |
<!-- ANSWER -->
|
| 324 |
<div id="answer-wrap">
|
| 325 |
<div class="ans-header">
|
| 326 |
-
<
|
| 327 |
-
<div class="ans-title-icon">💡</div>
|
| 328 |
-
Answer
|
| 329 |
-
</div>
|
| 330 |
<div class="ans-actions">
|
| 331 |
-
<
|
| 332 |
-
<button class="btn btn-ghost" id="copy-btn" onclick="copyAns()">📋 Copy</button>
|
| 333 |
</div>
|
| 334 |
</div>
|
| 335 |
-
<div
|
| 336 |
-
|
| 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,"&").replace(/</g,"<").replace(/>/g,">");
|
|
|
|
| 345 |
|
| 346 |
-
|
| 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 |
-
/
|
| 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 |
-
/
|
| 362 |
async function up(file){
|
| 363 |
-
if(!file.name.toLowerCase().endsWith(".pdf")){
|
| 364 |
-
|
| 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){
|
| 370 |
setSource(d.filename,d.chunks,"pdf");
|
| 371 |
-
|
| 372 |
-
}catch(e){
|
| 373 |
}
|
| 374 |
|
| 375 |
-
/
|
| 376 |
async function fetchURL(){
|
| 377 |
const url=document.getElementById("url-inp").value.trim();
|
| 378 |
-
if(!url){
|
| 379 |
-
|
| 380 |
-
|
| 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){
|
| 386 |
setSource(d.url,d.chunks,"url");
|
| 387 |
-
|
| 388 |
-
}catch(e){
|
| 389 |
-
finally{btn.disabled=false;
|
| 390 |
}
|
| 391 |
|
| 392 |
-
/
|
| 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>
|
| 400 |
p.classList.add("loaded");
|
| 401 |
}
|
| 402 |
|
| 403 |
-
/
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
|
|
|
|
|
|
| 411 |
}
|
| 412 |
|
| 413 |
-
/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 424 |
|
| 425 |
document.getElementById("pipeline").style.display="block";
|
| 426 |
document.getElementById("trace-wrap").style.display="block";
|
| 427 |
-
document.getElementById("trace-log").innerHTML=
|
|
|
|
| 428 |
document.getElementById("answer-wrap").style.display="none";
|
| 429 |
-
|
| 430 |
-
seen=0;clearInterval(pollTimer);
|
| 431 |
|
| 432 |
try{
|
| 433 |
-
const r=await fetch("/api/research",{
|
|
|
|
|
|
|
|
|
|
| 434 |
const d=await r.json();
|
| 435 |
-
if(d.error){traceErr(d.error);
|
| 436 |
pollTimer=setInterval(()=>poll(d.query_id),1500);
|
| 437 |
-
}catch(e){traceErr("Network error: "+e.message);
|
| 438 |
}
|
| 439 |
|
| 440 |
-
function
|
| 441 |
const btn=document.getElementById("ask-btn");
|
| 442 |
btn.disabled=false;
|
| 443 |
-
btn.innerHTML=
|
| 444 |
}
|
| 445 |
|
| 446 |
-
/
|
| 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);
|
| 451 |
const d=await r.json();
|
| 452 |
renderTrace(d.trace||[]);
|
| 453 |
if(["complete","error"].includes(d.status)){
|
| 454 |
-
clearInterval(pollTimer);
|
| 455 |
-
|
| 456 |
-
if(d.status==="
|
| 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);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 470 |
-
|
| 471 |
-
const
|
| 472 |
-
log.innerHTML+=
|
| 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'>❌</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 |
-
/
|
| 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
|
| 495 |
-
if(result.verdict==="APPROVED"){
|
| 496 |
-
else if(result.verdict){
|
| 497 |
-
else{
|
| 498 |
-
document.getElementById("answer-wrap").scrollIntoView({behavior:"smooth",block:"
|
| 499 |
}
|
| 500 |
|
| 501 |
-
/
|
| 502 |
-
|
| 503 |
const text=document.getElementById("answer-text").textContent;
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
setTimeout(()=>{btn.innerHTML="📋 Copy";},2000);
|
| 509 |
-
}catch(e){}
|
| 510 |
}
|
| 511 |
|
| 512 |
-
/
|
| 513 |
-
function
|
| 514 |
const el=document.getElementById(id);
|
| 515 |
-
if(type==="ok")el.innerHTML=
|
| 516 |
-
else if(type==="error")el.innerHTML=
|
| 517 |
-
else el.innerHTML=
|
| 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">🧠</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">🔗 LangGraph</span>
|
| 273 |
+
<span class="hs-badge hs-lc">⛩ LangChain LCEL</span>
|
| 274 |
+
<span class="hs-badge hs-fb">🗃 FAISS+BM25</span>
|
| 275 |
+
<span class="hs-badge hs-qw" id="hdr-model-badge">⚡ Qwen 2.5·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">📚</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')">📄 Upload PDF</button>
|
| 296 |
+
<button class="tab-btn" onclick="switchTab(this,'url')">🌐 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">📄</span>
|
| 303 |
+
<div class="dz-label"><strong>Click to browse</strong> or drag & drop</div>
|
| 304 |
+
<div class="dz-hint">PDF only · 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 & 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">📄</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">✓ 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">🤖</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·7B">
|
| 339 |
+
<div class="mc-color" style="background:#5b8ff9"></div>
|
| 340 |
+
<div class="mc-body">
|
| 341 |
+
<div class="mc-name">Qwen 2.5 · 7B</div>
|
| 342 |
+
<div class="mc-desc">Default · fast & free</div>
|
| 343 |
+
</div>
|
| 344 |
+
<div class="mc-right">
|
| 345 |
+
<span class="mc-params">7B</span>
|
| 346 |
+
<span class="mc-speed" style="color:#f5a623">⚡⚡⚡</span>
|
| 347 |
+
</div>
|
| 348 |
+
<span class="mc-check">✓</span>
|
| 349 |
+
</div>
|
| 350 |
+
<div class="model-card" onclick="selectModel('mistral-nemo',this)"
|
| 351 |
+
data-model="mistral-nemo" data-label="Mistral Nemo·12B">
|
| 352 |
+
<div class="mc-color" style="background:#a78bfa"></div>
|
| 353 |
+
<div class="mc-body">
|
| 354 |
+
<div class="mc-name">Mistral Nemo · 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">⚡⚡</span>
|
| 360 |
+
</div>
|
| 361 |
+
<span class="mc-check">✓</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·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 · 3.8B</div>
|
| 368 |
+
<div class="mc-desc">Ultra-fast & 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">⚡⚡⚡</span>
|
| 373 |
+
</div>
|
| 374 |
+
<span class="mc-check">✓</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">🛠</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">🔗</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">⛩</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">🗃</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">🪘</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">🆕</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">🐺</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">📊</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">📄</span>
|
| 434 |
+
<div class="arch-node-name">Source</div>
|
| 435 |
+
<div class="arch-node-sub">PDF · URL</div>
|
| 436 |
+
</div>
|
| 437 |
+
<span class="arch-arr">→</span>
|
| 438 |
+
<div class="arch-node an-chunker">
|
| 439 |
+
<span class="arch-node-icon">✂</span>
|
| 440 |
+
<div class="arch-node-name">Chunker</div>
|
| 441 |
+
<div class="arch-node-sub">RCTextSplitter</div>
|
| 442 |
+
</div>
|
| 443 |
+
<span class="arch-arr">→</span>
|
| 444 |
+
<div class="arch-node an-index">
|
| 445 |
+
<span class="arch-node-icon">🗃</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">↓ 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">🎯</span>
|
| 456 |
+
<div class="arch-node-name">Planner</div>
|
| 457 |
+
<div class="arch-node-sub">LLM · 0.3</div>
|
| 458 |
+
</div>
|
| 459 |
+
<span class="arch-arr">→</span>
|
| 460 |
+
<div class="arch-node an-local" data-arch="retriever" id="anode-retriever">
|
| 461 |
+
<span class="arch-node-icon">🔍</span>
|
| 462 |
+
<div class="arch-node-name">Retriever</div>
|
| 463 |
+
<div class="arch-node-sub">Local · RRF</div>
|
| 464 |
+
</div>
|
| 465 |
+
<span class="arch-arr">→</span>
|
| 466 |
+
<div class="arch-node an-score" data-arch="grader" id="anode-grader">
|
| 467 |
+
<span class="arch-node-icon">⚖</span>
|
| 468 |
+
<div class="arch-node-name">Grader</div>
|
| 469 |
+
<div class="arch-node-sub">Score · 0ms</div>
|
| 470 |
+
</div>
|
| 471 |
+
<span class="arch-arr">→</span>
|
| 472 |
+
<div class="arch-node an-llm" data-arch="generator" id="anode-generator">
|
| 473 |
+
<span class="arch-node-icon">✍</span>
|
| 474 |
+
<div class="arch-node-name">Generator</div>
|
| 475 |
+
<div class="arch-node-sub">LLM · 0.4</div>
|
| 476 |
+
</div>
|
| 477 |
+
<span class="arch-arr">→</span>
|
| 478 |
+
<div class="arch-node an-llm" data-arch="critic" id="anode-critic">
|
| 479 |
+
<span class="arch-node-icon">🔬</span>
|
| 480 |
+
<div class="arch-node-name">Critic</div>
|
| 481 |
+
<div class="arch-node-sub">LLM · 0.1</div>
|
| 482 |
+
</div>
|
| 483 |
+
<span class="arch-arr">→</span>
|
| 484 |
+
<div class="arch-node an-out" id="anode-answer">
|
| 485 |
+
<span class="arch-node-icon">📋</span>
|
| 486 |
+
<div class="arch-node-name">Answer</div>
|
| 487 |
+
<div class="arch-node-sub">Cited · 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">🔍</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>🎯 Planner</div>
|
| 512 |
<div class="pipe-arrow">→</div>
|
| 513 |
<div class="pipe-step" id="ps-retriever"><span class="step-dot"></span>🔍 Retriever</div>
|
| 514 |
<div class="pipe-arrow">→</div>
|
|
|
|
| 525 |
<div class="trace-hdr">
|
| 526 |
<span class="trace-title">📰 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">🧠 Answer</span>
|
|
|
|
|
|
|
|
|
|
| 535 |
<div class="ans-actions">
|
| 536 |
+
<button class="btn btn-ghost btn-sm" onclick="copyAns(this)">📋 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,"&").replace(/</g,"<").replace(/>/g,">");
|
| 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","✓ 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","✓ 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> 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='⚡ 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="📋 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(
|
| 26 |
-
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.
|
|
|
|
|
|
|
| 27 |
@keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}
|
| 28 |
|
| 29 |
-
/* ββ HEADER ββ
|
| 30 |
-
header{
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 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 |
-
.
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
.
|
| 47 |
-
.
|
| 48 |
-
.
|
| 49 |
-
|
| 50 |
-
.
|
| 51 |
-
#hdr-src{margin-left:auto
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
| 57 |
.panel-right{background:var(--bg)}
|
| 58 |
|
| 59 |
-
/* ββ SECTION HEADERS ββ
|
| 60 |
-
.sec-head{display:flex;align-items:center;gap:8px;margin-bottom:
|
| 61 |
-
.sec-icon{width:
|
| 62 |
-
justify-content:center;font-size:
|
| 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:.
|
| 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 |
-
/* ββ
|
| 78 |
-
.
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
.
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
.dropzone
|
| 88 |
-
|
| 89 |
-
.dropzone
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
.dz-label strong{color:var(--accent)}
|
| 93 |
-
.dz-hint{font-size:.72rem;color:var(--muted)
|
| 94 |
-
|
| 95 |
-
/* ββ URL
|
| 96 |
-
.url-row{display:flex;gap:8px;margin-bottom:
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
/* ββ SOURCE
|
| 103 |
-
#source-card{
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 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{
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
.
|
| 122 |
-
|
| 123 |
-
.
|
| 124 |
-
.
|
| 125 |
-
.
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
.
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
.
|
| 132 |
-
|
| 133 |
-
.
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
.
|
| 137 |
-
.
|
| 138 |
-
|
| 139 |
-
.
|
| 140 |
-
.
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
.
|
| 145 |
-
|
| 146 |
-
.
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
.
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
.
|
| 166 |
-
.
|
| 167 |
-
.
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
.
|
| 173 |
-
.
|
| 174 |
-
|
| 175 |
-
.
|
| 176 |
-
|
| 177 |
-
.
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
.t-step:last-child{border-bottom:none}
|
| 180 |
-
.t-
|
| 181 |
-
|
| 182 |
-
.
|
| 183 |
-
|
| 184 |
-
.
|
| 185 |
-
.
|
| 186 |
-
.
|
| 187 |
-
.
|
| 188 |
-
.
|
| 189 |
-
.
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
.ans-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
.
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 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">🧠</div>
|
| 226 |
<span class="logo-text">Doc<span>Mind</span></span>
|
| 227 |
</a>
|
| 228 |
-
<
|
| 229 |
-
|
| 230 |
-
<span class="badge
|
|
|
|
|
|
|
| 231 |
</div>
|
| 232 |
-
<span class="badge
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
</header>
|
| 234 |
|
| 235 |
<main>
|
| 236 |
-
<!--
|
| 237 |
<div class="panel panel-left">
|
| 238 |
-
<div class="sec-head">
|
| 239 |
-
<div class="sec-icon sec-icon-blue">📚</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')">📄 Upload PDF</button>
|
| 245 |
-
<button class="tab-btn" onclick="switchTab(this,'url')">🌐 Paste URL</button>
|
| 246 |
-
</div>
|
| 247 |
|
| 248 |
-
<!--
|
| 249 |
-
<div
|
| 250 |
-
<div class="
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<!--
|
| 262 |
-
<div
|
| 263 |
-
<div class="
|
| 264 |
-
<
|
| 265 |
-
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<!--
|
| 273 |
-
<div
|
| 274 |
-
<div class="
|
| 275 |
-
<div class="
|
| 276 |
-
<
|
| 277 |
-
|
| 278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
</div>
|
| 280 |
</div>
|
| 281 |
-
<div class="sc-ready">✓ Ready for questions</div>
|
| 282 |
</div>
|
|
|
|
| 283 |
</div>
|
| 284 |
|
| 285 |
-
<!--
|
| 286 |
<div class="panel panel-right">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
<div class="sec-head">
|
| 288 |
<div class="sec-icon sec-icon-purple">🔍</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>&#
|
| 307 |
<div class="pipe-arrow">→</div>
|
| 308 |
<div class="pipe-step" id="ps-retriever"><span class="step-dot"></span>🔍 Retriever</div>
|
| 309 |
<div class="pipe-arrow">→</div>
|
|
@@ -320,35 +528,29 @@ textarea::placeholder{color:var(--muted)}
|
|
| 320 |
<div class="trace-hdr">
|
| 321 |
<span class="trace-title">📰 Agent Trace</span>
|
| 322 |
</div>
|
| 323 |
-
<div
|
| 324 |
</div>
|
| 325 |
|
| 326 |
<!-- ANSWER -->
|
| 327 |
<div id="answer-wrap">
|
| 328 |
<div class="ans-header">
|
| 329 |
-
<
|
| 330 |
-
<div class="ans-title-icon">💡</div>
|
| 331 |
-
Answer
|
| 332 |
-
</div>
|
| 333 |
<div class="ans-actions">
|
| 334 |
-
<
|
| 335 |
-
<button class="btn btn-ghost" id="copy-btn" onclick="copyAns()">📋 Copy</button>
|
| 336 |
</div>
|
| 337 |
</div>
|
| 338 |
-
<div
|
| 339 |
-
|
| 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,"&").replace(/</g,"<").replace(/>/g,">");
|
|
|
|
| 348 |
|
| 349 |
-
|
| 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 |
-
/
|
| 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 |
-
/
|
| 365 |
async function up(file){
|
| 366 |
-
if(!file.name.toLowerCase().endsWith(".pdf")){
|
| 367 |
-
|
| 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){
|
| 373 |
setSource(d.filename,d.chunks,"pdf");
|
| 374 |
-
|
| 375 |
-
}catch(e){
|
| 376 |
}
|
| 377 |
|
| 378 |
-
/
|
| 379 |
async function fetchURL(){
|
| 380 |
const url=document.getElementById("url-inp").value.trim();
|
| 381 |
-
if(!url){
|
| 382 |
-
|
| 383 |
-
|
| 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){
|
| 389 |
setSource(d.url,d.chunks,"url");
|
| 390 |
-
|
| 391 |
-
}catch(e){
|
| 392 |
-
finally{btn.disabled=false;
|
| 393 |
}
|
| 394 |
|
| 395 |
-
/
|
| 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"?"
|
| 400 |
document.getElementById("source-card").style.display="block";
|
| 401 |
const p=document.getElementById("hdr-src");
|
| 402 |
-
p.textContent=name.length>
|
| 403 |
p.classList.add("loaded");
|
| 404 |
}
|
| 405 |
|
| 406 |
-
/
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
|
|
|
|
|
|
| 414 |
}
|
| 415 |
|
| 416 |
-
/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 427 |
|
| 428 |
document.getElementById("pipeline").style.display="block";
|
| 429 |
document.getElementById("trace-wrap").style.display="block";
|
| 430 |
-
document.getElementById("trace-log").innerHTML=
|
|
|
|
| 431 |
document.getElementById("answer-wrap").style.display="none";
|
| 432 |
-
|
| 433 |
-
seen=0;clearInterval(pollTimer);
|
| 434 |
|
| 435 |
try{
|
| 436 |
-
const r=await fetch("/api/research",{
|
|
|
|
|
|
|
|
|
|
| 437 |
const d=await r.json();
|
| 438 |
-
if(d.error){traceErr(d.error);
|
| 439 |
pollTimer=setInterval(()=>poll(d.query_id),1500);
|
| 440 |
-
}catch(e){traceErr("Network error: "+e.message);
|
| 441 |
}
|
| 442 |
|
| 443 |
-
function
|
| 444 |
const btn=document.getElementById("ask-btn");
|
| 445 |
btn.disabled=false;
|
| 446 |
-
btn.innerHTML=
|
| 447 |
}
|
| 448 |
|
| 449 |
-
/
|
| 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);
|
| 454 |
const d=await r.json();
|
| 455 |
renderTrace(d.trace||[]);
|
| 456 |
if(["complete","error"].includes(d.status)){
|
| 457 |
-
clearInterval(pollTimer);
|
| 458 |
-
|
| 459 |
-
if(d.status==="
|
| 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);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 473 |
-
|
| 474 |
-
const
|
| 475 |
-
log.innerHTML+=
|
| 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'>❌</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 |
-
/
|
| 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
|
| 498 |
-
if(result.verdict==="APPROVED"){
|
| 499 |
-
else if(result.verdict){
|
| 500 |
-
else{
|
| 501 |
-
document.getElementById("answer-wrap").scrollIntoView({behavior:"smooth",block:"
|
| 502 |
}
|
| 503 |
|
| 504 |
-
/
|
| 505 |
-
|
| 506 |
const text=document.getElementById("answer-text").textContent;
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
setTimeout(()=>{btn.innerHTML="📋 Copy";},2000);
|
| 512 |
-
}catch(e){}
|
| 513 |
}
|
| 514 |
|
| 515 |
-
/
|
| 516 |
-
function
|
| 517 |
const el=document.getElementById(id);
|
| 518 |
-
if(type==="ok")el.innerHTML=
|
| 519 |
-
else if(type==="error")el.innerHTML=
|
| 520 |
-
else el.innerHTML=
|
| 521 |
}
|
| 522 |
</script>
|
| 523 |
</body>
|
| 524 |
</html>
|
| 525 |
'''
|
| 526 |
|
| 527 |
-
pathlib.Path(
|
| 528 |
-
|
|
|
|
|
|
| 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">🧠</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">🔗 LangGraph</span>
|
| 276 |
+
<span class="hs-badge hs-lc">⛩ LangChain LCEL</span>
|
| 277 |
+
<span class="hs-badge hs-fb">🗃 FAISS+BM25</span>
|
| 278 |
+
<span class="hs-badge hs-qw" id="hdr-model-badge">⚡ Qwen 2.5·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">📚</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\')">📄 Upload PDF</button>
|
| 299 |
+
<button class="tab-btn" onclick="switchTab(this,\'url\')">🌐 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">📄</span>
|
| 306 |
+
<div class="dz-label"><strong>Click to browse</strong> or drag & drop</div>
|
| 307 |
+
<div class="dz-hint">PDF only · 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 & 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">📄</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">✓ 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">🤖</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·7B">
|
| 342 |
+
<div class="mc-color" style="background:#5b8ff9"></div>
|
| 343 |
+
<div class="mc-body">
|
| 344 |
+
<div class="mc-name">Qwen 2.5 · 7B</div>
|
| 345 |
+
<div class="mc-desc">Default · fast & free</div>
|
| 346 |
+
</div>
|
| 347 |
+
<div class="mc-right">
|
| 348 |
+
<span class="mc-params">7B</span>
|
| 349 |
+
<span class="mc-speed" style="color:#f5a623">⚡⚡⚡</span>
|
| 350 |
+
</div>
|
| 351 |
+
<span class="mc-check">✓</span>
|
| 352 |
+
</div>
|
| 353 |
+
<div class="model-card" onclick="selectModel(\'mistral-nemo\',this)"
|
| 354 |
+
data-model="mistral-nemo" data-label="Mistral Nemo·12B">
|
| 355 |
+
<div class="mc-color" style="background:#a78bfa"></div>
|
| 356 |
+
<div class="mc-body">
|
| 357 |
+
<div class="mc-name">Mistral Nemo · 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">⚡⚡</span>
|
| 363 |
+
</div>
|
| 364 |
+
<span class="mc-check">✓</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·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 · 3.8B</div>
|
| 371 |
+
<div class="mc-desc">Ultra-fast & 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">⚡⚡⚡</span>
|
| 376 |
+
</div>
|
| 377 |
+
<span class="mc-check">✓</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">🛠</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">🔗</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">⛩</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">🗃</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">🪘</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">🆕</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">🐺</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">📊</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">📄</span>
|
| 437 |
+
<div class="arch-node-name">Source</div>
|
| 438 |
+
<div class="arch-node-sub">PDF · URL</div>
|
| 439 |
+
</div>
|
| 440 |
+
<span class="arch-arr">→</span>
|
| 441 |
+
<div class="arch-node an-chunker">
|
| 442 |
+
<span class="arch-node-icon">✂</span>
|
| 443 |
+
<div class="arch-node-name">Chunker</div>
|
| 444 |
+
<div class="arch-node-sub">RCTextSplitter</div>
|
| 445 |
+
</div>
|
| 446 |
+
<span class="arch-arr">→</span>
|
| 447 |
+
<div class="arch-node an-index">
|
| 448 |
+
<span class="arch-node-icon">🗃</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">↓ 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">🎯</span>
|
| 459 |
+
<div class="arch-node-name">Planner</div>
|
| 460 |
+
<div class="arch-node-sub">LLM · 0.3</div>
|
| 461 |
+
</div>
|
| 462 |
+
<span class="arch-arr">→</span>
|
| 463 |
+
<div class="arch-node an-local" data-arch="retriever" id="anode-retriever">
|
| 464 |
+
<span class="arch-node-icon">🔍</span>
|
| 465 |
+
<div class="arch-node-name">Retriever</div>
|
| 466 |
+
<div class="arch-node-sub">Local · RRF</div>
|
| 467 |
+
</div>
|
| 468 |
+
<span class="arch-arr">→</span>
|
| 469 |
+
<div class="arch-node an-score" data-arch="grader" id="anode-grader">
|
| 470 |
+
<span class="arch-node-icon">⚖</span>
|
| 471 |
+
<div class="arch-node-name">Grader</div>
|
| 472 |
+
<div class="arch-node-sub">Score · 0ms</div>
|
| 473 |
+
</div>
|
| 474 |
+
<span class="arch-arr">→</span>
|
| 475 |
+
<div class="arch-node an-llm" data-arch="generator" id="anode-generator">
|
| 476 |
+
<span class="arch-node-icon">✍</span>
|
| 477 |
+
<div class="arch-node-name">Generator</div>
|
| 478 |
+
<div class="arch-node-sub">LLM · 0.4</div>
|
| 479 |
+
</div>
|
| 480 |
+
<span class="arch-arr">→</span>
|
| 481 |
+
<div class="arch-node an-llm" data-arch="critic" id="anode-critic">
|
| 482 |
+
<span class="arch-node-icon">🔬</span>
|
| 483 |
+
<div class="arch-node-name">Critic</div>
|
| 484 |
+
<div class="arch-node-sub">LLM · 0.1</div>
|
| 485 |
+
</div>
|
| 486 |
+
<span class="arch-arr">→</span>
|
| 487 |
+
<div class="arch-node an-out" id="anode-answer">
|
| 488 |
+
<span class="arch-node-icon">📋</span>
|
| 489 |
+
<div class="arch-node-name">Answer</div>
|
| 490 |
+
<div class="arch-node-sub">Cited · 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">🔍</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>🎯 Planner</div>
|
| 515 |
<div class="pipe-arrow">→</div>
|
| 516 |
<div class="pipe-step" id="ps-retriever"><span class="step-dot"></span>🔍 Retriever</div>
|
| 517 |
<div class="pipe-arrow">→</div>
|
|
|
|
| 528 |
<div class="trace-hdr">
|
| 529 |
<span class="trace-title">📰 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">🧠 Answer</span>
|
|
|
|
|
|
|
|
|
|
| 538 |
<div class="ans-actions">
|
| 539 |
+
<button class="btn btn-ghost btn-sm" onclick="copyAns(this)">📋 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,"&").replace(/</g,"<").replace(/>/g,">");
|
| 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","✓ 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","✓ 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> 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=\'⚡ 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="📋 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)}")
|