Spaces:
Sleeping
Sleeping
Commit ·
25d4f70
0
Parent(s):
local: phase 5 arena and text highlighter
Browse files- .geminirules +7 -0
- .gitignore +3 -0
- .python-version +1 -0
- README.md +0 -0
- backend/__init__.py +1 -0
- backend/constants.py +93 -0
- backend/engines/__init__.py +1 -0
- backend/engines/chunking.py +251 -0
- backend/engines/embedding.py +29 -0
- backend/engines/llm_client.py +17 -0
- backend/engines/reducer.py +43 -0
- backend/main.py +60 -0
- backend/models/__init__.py +1 -0
- backend/models/schemas.py +184 -0
- backend/routers/__init__.py +1 -0
- backend/routers/chunk_router.py +95 -0
- backend/routers/retrieval_router.py +191 -0
- backend/storage/vector_store.py +33 -0
- dev.bat +1 -0
- frontend/app.js +1652 -0
- frontend/index.html +391 -0
- frontend/styles.css +1899 -0
- pyproject.toml +19 -0
- uv.lock +0 -0
.geminirules
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Agent Behavioral Rules
|
| 2 |
+
|
| 3 |
+
1. NEVER write, edit, or modify any backend code files (Python, FastAPI, databases, engines, schemas, etc.) directly on your own.
|
| 4 |
+
2. The user is in charge of the backend implementation.
|
| 5 |
+
3. Instead of editing backend files, explain the concepts, algorithms, and provide code blocks in your responses so the user can review and implement them.
|
| 6 |
+
4. DO NOT provide full copy-pasteable files of backend code to the user. Instead, provide architectural guidance, conceptual breakdowns, schema designs, key functions, and partial snippets/pseudocode not actual python code so that the user writes and completes the actual code, reinforcing their hands-on learning.
|
| 7 |
+
5. Only edit frontend files (HTML, CSS, JS) if explicitly requested, but NEVER touch backend files.
|
.gitignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.venv
|
| 2 |
+
store/
|
| 3 |
+
__pycache__
|
.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.11
|
README.md
ADDED
|
File without changes
|
backend/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Backend package for RAG Visualizer
|
backend/constants.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
system_instructions = """
|
| 2 |
+
# ROLE
|
| 3 |
+
You are a strict, unbiased RAG evaluation judge. Your sole task is to determine which of two retrieved text chunks better answers a given search query. You have no preference for either chunk.
|
| 4 |
+
|
| 5 |
+
# BLINDING RULE (critical)
|
| 6 |
+
The chunks are labelled "Chunk A" and "Chunk B". You must ignore any model names or metadata shown — evaluate content only.
|
| 7 |
+
|
| 8 |
+
# SCORING CRITERIA (score each 1–10)
|
| 9 |
+
Score each dimension independently. Do not let one dimension influence another.
|
| 10 |
+
|
| 11 |
+
1. Query Relevance
|
| 12 |
+
Does the chunk directly address the specific question asked?
|
| 13 |
+
10 = precisely on-topic, 1 = entirely off-topic.
|
| 14 |
+
Penalise chunks that are topically adjacent but don't answer the actual query.
|
| 15 |
+
|
| 16 |
+
2. Answer Completeness
|
| 17 |
+
Is the answer self-contained within the chunk, or does it trail off / require external context?
|
| 18 |
+
10 = standalone complete answer, 1 = fragment with no usable answer.
|
| 19 |
+
Do NOT reward length. A concise, complete answer scores higher than a verbose partial one.
|
| 20 |
+
|
| 21 |
+
3. Factual Plausibility
|
| 22 |
+
Based on your knowledge, does the chunk contain accurate, internally consistent information?
|
| 23 |
+
10 = no detectable errors, 1 = clearly wrong or contradictory.
|
| 24 |
+
If you cannot verify a claim, score conservatively (5–6) rather than assuming correctness.
|
| 25 |
+
Do not penalise a chunk for information you simply don't recognise.
|
| 26 |
+
|
| 27 |
+
4. Clarity & Parsability
|
| 28 |
+
Is the chunk clean, readable, and free from noise (broken formatting, encoding artefacts, truncation mid-sentence)?
|
| 29 |
+
10 = polished and easy to parse, 1 = heavily noisy or unreadable.
|
| 30 |
+
|
| 31 |
+
# OVERALL SCORE
|
| 32 |
+
overall = (relevance + completeness + plausibility + clarity) / 4
|
| 33 |
+
Round to two decimal places.
|
| 34 |
+
|
| 35 |
+
# WINNER DECLARATION
|
| 36 |
+
- Declare "chunk_a" or "chunk_b" based on overall score.
|
| 37 |
+
- Declare "tie" ONLY if overall scores are within 0.5 of each other AND no single dimension differs by more than 2 points. Ties should be rare.
|
| 38 |
+
- If you declare a tie, the winner_reason must explicitly state why the gap is insufficient to prefer either chunk.
|
| 39 |
+
|
| 40 |
+
# CONFIDENCE
|
| 41 |
+
0.9–1.0: One chunk is clearly superior across most dimensions.
|
| 42 |
+
0.7–0.89: One chunk wins, but with a notable weakness.
|
| 43 |
+
0.5–0.69: Close call; winner has only a marginal edge.
|
| 44 |
+
Below 0.5: Reserve for genuine ties.
|
| 45 |
+
|
| 46 |
+
---
|
| 47 |
+
|
| 48 |
+
Search Query: {search_query}
|
| 49 |
+
|
| 50 |
+
--- Chunk A ---
|
| 51 |
+
{chunk_a}
|
| 52 |
+
|
| 53 |
+
--- Chunk B ---
|
| 54 |
+
{chunk_b}
|
| 55 |
+
|
| 56 |
+
---
|
| 57 |
+
|
| 58 |
+
# OUTPUT RULE
|
| 59 |
+
OUTPUT: respond with this exact JSON structure and nothing else:
|
| 60 |
+
{{
|
| 61 |
+
"winner": "chunk_a" | "chunk_b" | "tie",
|
| 62 |
+
"confidence": float,
|
| 63 |
+
"chunk_a_score": {{
|
| 64 |
+
"query_relevance": int,
|
| 65 |
+
"answer_completeness": int,
|
| 66 |
+
"factual_plausibility": int,
|
| 67 |
+
"clarity": int,
|
| 68 |
+
"overall": float
|
| 69 |
+
}},
|
| 70 |
+
"chunk_b_score": {{
|
| 71 |
+
"query_relevance": int,
|
| 72 |
+
"answer_completeness": int,
|
| 73 |
+
"factual_plausibility": int,
|
| 74 |
+
"clarity": int,
|
| 75 |
+
"overall": float
|
| 76 |
+
}},
|
| 77 |
+
"winner_reason": "2-3 sentences",
|
| 78 |
+
"deciding_dimension": "e.g. query_relevance",
|
| 79 |
+
"chunk_a_strengths": ["..."],
|
| 80 |
+
"chunk_a_weaknesses": ["..."],
|
| 81 |
+
"chunk_b_strengths": ["..."],
|
| 82 |
+
"chunk_b_weaknesses": ["..."]
|
| 83 |
+
}}
|
| 84 |
+
"""
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
hyde_prompt = """Generate a concise, factual passage that directly answers the query below.
|
| 88 |
+
Write as if excerpted from a authoritative document or textbook — no intro, no filler, no meta-commentary.
|
| 89 |
+
Match the tone and vocabulary a subject-matter expert would use when writing about this topic.
|
| 90 |
+
|
| 91 |
+
Query: {search_text}
|
| 92 |
+
|
| 93 |
+
Passage:"""
|
backend/engines/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Engines package — ChunkingEngine, EmbeddingEngine, etc.
|
backend/engines/chunking.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import tiktoken
|
| 3 |
+
from typing import List
|
| 4 |
+
from nltk.tokenize import sent_tokenize
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
from backend.engines.embedding import EmbeddingEngine
|
| 8 |
+
|
| 9 |
+
from langchain_text_splitters import (
|
| 10 |
+
RecursiveCharacterTextSplitter,
|
| 11 |
+
NLTKTextSplitter,
|
| 12 |
+
CharacterTextSplitter,
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
from backend.models.schemas import ChunkConfig, ChunkNode
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def count_token(text: str, tokenizer) -> int:
|
| 19 |
+
_encoder = tiktoken.get_encoding(tokenizer)
|
| 20 |
+
return len(_encoder.encode(text))
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def fixed_size_strategy(text, config: ChunkConfig) -> List[ChunkNode]:
|
| 24 |
+
splitter = CharacterTextSplitter.from_tiktoken_encoder(
|
| 25 |
+
encoding_name=config.tokenizer,
|
| 26 |
+
chunk_size=config.chunk_size,
|
| 27 |
+
chunk_overlap=config.chunk_overlap,
|
| 28 |
+
)
|
| 29 |
+
result = splitter.split_text(text)
|
| 30 |
+
result = construct_chunk_node(text, result, config.tokenizer)
|
| 31 |
+
return result
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def sentence_strategy(text, config: ChunkConfig) -> List[ChunkNode]:
|
| 35 |
+
text_splitter = NLTKTextSplitter.from_tiktoken_encoder(
|
| 36 |
+
encoding_name=config.tokenizer,
|
| 37 |
+
chunk_size=config.chunk_size,
|
| 38 |
+
chunk_overlap=config.chunk_overlap,
|
| 39 |
+
)
|
| 40 |
+
result = text_splitter.split_text(text)
|
| 41 |
+
result = construct_chunk_node(text, result, config.tokenizer)
|
| 42 |
+
return result
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def recursive_strategy(text, config: ChunkConfig) -> List[ChunkNode]:
|
| 46 |
+
splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
|
| 47 |
+
encoding_name=config.tokenizer,
|
| 48 |
+
chunk_size=config.chunk_size,
|
| 49 |
+
chunk_overlap=config.chunk_overlap,
|
| 50 |
+
separators=config.separators,
|
| 51 |
+
)
|
| 52 |
+
result = splitter.split_text(text)
|
| 53 |
+
result = construct_chunk_node(text, result, config.tokenizer)
|
| 54 |
+
return result
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def parent_child_strategy(text, config) -> List[ChunkNode]:
|
| 58 |
+
parent_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
|
| 59 |
+
encoding_name=config.tokenizer,
|
| 60 |
+
chunk_size=config.parent_chunk_size,
|
| 61 |
+
chunk_overlap=config.parent_chunk_overlap,
|
| 62 |
+
separators=config.separators,
|
| 63 |
+
)
|
| 64 |
+
parent_chunks = parent_splitter.split_text(text)
|
| 65 |
+
result = construct_parent_child_nodes(text, parent_chunks, config)
|
| 66 |
+
return result
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def construct_chunk_node(text, chunks, tokenizer):
|
| 70 |
+
nodes = []
|
| 71 |
+
current_position = 0
|
| 72 |
+
for i, chunk in enumerate(chunks):
|
| 73 |
+
# 1. Try exact find first
|
| 74 |
+
start = text.find(chunk, current_position)
|
| 75 |
+
|
| 76 |
+
# 2. If exact find fails, try a clean stripped version
|
| 77 |
+
if start == -1:
|
| 78 |
+
clean_anchor = chunk.strip()[:40]
|
| 79 |
+
if clean_anchor:
|
| 80 |
+
start = text.find(clean_anchor, current_position)
|
| 81 |
+
|
| 82 |
+
# 3. If it still fails, park it at current_position
|
| 83 |
+
if start == -1:
|
| 84 |
+
start = current_position
|
| 85 |
+
|
| 86 |
+
end = start + len(chunk)
|
| 87 |
+
node = ChunkNode(
|
| 88 |
+
id=f"chunk_{i}",
|
| 89 |
+
order=i,
|
| 90 |
+
text=chunk,
|
| 91 |
+
token_count=count_token(chunk, tokenizer),
|
| 92 |
+
start_char=start,
|
| 93 |
+
end_char=end,
|
| 94 |
+
)
|
| 95 |
+
nodes.append(node)
|
| 96 |
+
|
| 97 |
+
# Safely advance position but allow overlaps
|
| 98 |
+
current_position = max(current_position, start + 1)
|
| 99 |
+
|
| 100 |
+
return nodes
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def construct_parent_child_nodes(text, parent_chunks, config):
|
| 104 |
+
all_nodes = []
|
| 105 |
+
current_position = 0
|
| 106 |
+
for parent_index, parent_chunk in enumerate(parent_chunks):
|
| 107 |
+
parent_start = text.find(parent_chunk, current_position)
|
| 108 |
+
parent_end = parent_start + len(parent_chunk)
|
| 109 |
+
|
| 110 |
+
parent_id = f"parent_{parent_index}"
|
| 111 |
+
|
| 112 |
+
parent_node = ChunkNode(
|
| 113 |
+
id=parent_id,
|
| 114 |
+
order=parent_index,
|
| 115 |
+
text=parent_chunk,
|
| 116 |
+
token_count=count_token(parent_chunk, config.tokenizer),
|
| 117 |
+
start_char=parent_start,
|
| 118 |
+
end_char=parent_end,
|
| 119 |
+
level=0,
|
| 120 |
+
child_ids=[],
|
| 121 |
+
)
|
| 122 |
+
all_nodes.append(parent_node)
|
| 123 |
+
|
| 124 |
+
child_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
|
| 125 |
+
encoding_name=config.tokenizer,
|
| 126 |
+
chunk_size=config.child_chunk_size,
|
| 127 |
+
chunk_overlap=config.child_chunk_overlap,
|
| 128 |
+
separators=config.separators,
|
| 129 |
+
)
|
| 130 |
+
child_chunks = child_splitter.split_text(parent_chunk)
|
| 131 |
+
|
| 132 |
+
child_position = parent_start
|
| 133 |
+
|
| 134 |
+
for child_index, child_chunk in enumerate(child_chunks):
|
| 135 |
+
|
| 136 |
+
child_start = text.find(child_chunk, child_position)
|
| 137 |
+
child_end = child_start + len(child_chunk)
|
| 138 |
+
|
| 139 |
+
child_id = f"{parent_id}_child_{child_index}"
|
| 140 |
+
|
| 141 |
+
child_node = ChunkNode(
|
| 142 |
+
id=child_id,
|
| 143 |
+
order=child_index,
|
| 144 |
+
text=child_chunk,
|
| 145 |
+
token_count=count_token(child_chunk, config.tokenizer),
|
| 146 |
+
start_char=child_start,
|
| 147 |
+
end_char=child_end,
|
| 148 |
+
level=1,
|
| 149 |
+
parent_id=parent_id,
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
parent_node.child_ids.append(child_id)
|
| 153 |
+
|
| 154 |
+
all_nodes.append(child_node)
|
| 155 |
+
|
| 156 |
+
child_position = child_start + 1
|
| 157 |
+
|
| 158 |
+
current_position = parent_start + 1
|
| 159 |
+
|
| 160 |
+
return all_nodes
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def cosine_similarity(v1, v2):
|
| 164 |
+
dot_product = sum(x * y for x, y in zip(v1, v2))
|
| 165 |
+
norm_v1 = math.sqrt(sum(x * x for x in v1))
|
| 166 |
+
norm_v2 = math.sqrt(sum(x * x for x in v2))
|
| 167 |
+
if not norm_v1 or not norm_v2:
|
| 168 |
+
return 0.0
|
| 169 |
+
return dot_product / (norm_v1 * norm_v2)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
async def semantic_strategy(text, config: ChunkConfig, embedding_model):
|
| 173 |
+
sentences = sent_tokenize(text)
|
| 174 |
+
embedding_engine = EmbeddingEngine(embedding_model)
|
| 175 |
+
|
| 176 |
+
if not sentences:
|
| 177 |
+
return []
|
| 178 |
+
|
| 179 |
+
temp_nodes = [
|
| 180 |
+
ChunkNode(
|
| 181 |
+
id=f"temp_{i}", order=i, text=s, token_count=0, start_char=0, end_char=0
|
| 182 |
+
)
|
| 183 |
+
for i, s in enumerate(sentences)
|
| 184 |
+
]
|
| 185 |
+
|
| 186 |
+
sentence_embeddings = await embedding_engine.generate_embeddings(temp_nodes)
|
| 187 |
+
|
| 188 |
+
similarities = []
|
| 189 |
+
|
| 190 |
+
for i in range(len(sentence_embeddings) - 1):
|
| 191 |
+
sim = cosine_similarity(sentence_embeddings[i], sentence_embeddings[i + 1])
|
| 192 |
+
similarities.append(sim)
|
| 193 |
+
|
| 194 |
+
distances = [1 - s for s in similarities]
|
| 195 |
+
|
| 196 |
+
if not distances:
|
| 197 |
+
return construct_chunk_node(text, sentences, config.tokenizer)
|
| 198 |
+
|
| 199 |
+
mean_distance = np.mean(distances)
|
| 200 |
+
std_deviation = np.std(distances)
|
| 201 |
+
|
| 202 |
+
z_score_multiplier = 2.5 - (config.semantic_threshold * 3.0)
|
| 203 |
+
dynamic_threshold = mean_distance + (z_score_multiplier * std_deviation)
|
| 204 |
+
|
| 205 |
+
chunks = []
|
| 206 |
+
current_chunks = [sentences[0]]
|
| 207 |
+
|
| 208 |
+
for i in range(len(distances)):
|
| 209 |
+
current_dist = distances[i]
|
| 210 |
+
|
| 211 |
+
if current_dist > dynamic_threshold:
|
| 212 |
+
is_greater_than_prev = (i == 0) or (current_dist > distances[i - 1])
|
| 213 |
+
is_greater_than_or_equal_next = (i == len(distances) - 1) or (
|
| 214 |
+
current_dist >= distances[i + 1]
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
if is_greater_than_prev and is_greater_than_or_equal_next:
|
| 218 |
+
chunks.append(" ".join(current_chunks))
|
| 219 |
+
current_chunks = [sentences[i + 1]]
|
| 220 |
+
else:
|
| 221 |
+
current_chunks.append(sentences[i + 1])
|
| 222 |
+
else:
|
| 223 |
+
current_chunks.append(sentences[i + 1])
|
| 224 |
+
|
| 225 |
+
if current_chunks:
|
| 226 |
+
chunks.append(" ".join(current_chunks))
|
| 227 |
+
|
| 228 |
+
return construct_chunk_node(text, chunks, config.tokenizer)
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
class ChunkingEngine:
|
| 232 |
+
|
| 233 |
+
async def chunk(self, text, strategy, config, embedding_model) -> List[ChunkNode]:
|
| 234 |
+
strategy_func = self.available_strategy(strategy)
|
| 235 |
+
if not strategy_func:
|
| 236 |
+
raise ValueError(f"Unknown strategy: {strategy}")
|
| 237 |
+
|
| 238 |
+
if strategy == "semantic":
|
| 239 |
+
return await strategy_func(text, config, embedding_model)
|
| 240 |
+
|
| 241 |
+
return strategy_func(text, config)
|
| 242 |
+
|
| 243 |
+
def available_strategy(self, strategy):
|
| 244 |
+
STRATEGY = {
|
| 245 |
+
"fixed_size": fixed_size_strategy,
|
| 246 |
+
"sentence": sentence_strategy,
|
| 247 |
+
"recursive": recursive_strategy,
|
| 248 |
+
"parent_child": parent_child_strategy,
|
| 249 |
+
"semantic": semantic_strategy,
|
| 250 |
+
}
|
| 251 |
+
return STRATEGY.get(strategy)
|
backend/engines/embedding.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Sequence
|
| 2 |
+
import httpx
|
| 3 |
+
|
| 4 |
+
from backend.models.schemas import ChunkNode, EmbeddingModel
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class EmbeddingEngine:
|
| 8 |
+
def __init__(self, embedding_model):
|
| 9 |
+
self.embedding_model = embedding_model
|
| 10 |
+
self.client = httpx.AsyncClient(timeout=60)
|
| 11 |
+
|
| 12 |
+
async def generate_embeddings(self, chunks: Sequence[ChunkNode | str]):
|
| 13 |
+
texts = [c if isinstance(c, str) else c.text for c in chunks]
|
| 14 |
+
res = await self.client.post(
|
| 15 |
+
"http://localhost:11434/api/embed",
|
| 16 |
+
json={
|
| 17 |
+
"model": self.embedding_model,
|
| 18 |
+
"input": texts,
|
| 19 |
+
},
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
res_json = res.json()
|
| 23 |
+
|
| 24 |
+
if "embeddings" not in res_json or not res_json["embeddings"]:
|
| 25 |
+
raise ValueError(
|
| 26 |
+
f"Ollama failed to generate embeddings for {self.embedding_model}. Response: {res_json}"
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
return res_json["embeddings"]
|
backend/engines/llm_client.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
import ollama
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class OllamaClient:
|
| 6 |
+
def __init__(self):
|
| 7 |
+
self.model = "gemma4:e2b"
|
| 8 |
+
self.client = ollama.AsyncClient()
|
| 9 |
+
|
| 10 |
+
async def generate(self, prompt: str, response_format: Optional[str] = "json"):
|
| 11 |
+
args = {"model": self.model, "prompt": prompt}
|
| 12 |
+
|
| 13 |
+
if response_format:
|
| 14 |
+
args["format"] = response_format
|
| 15 |
+
|
| 16 |
+
response = await self.client.generate(**args) # type: ignore
|
| 17 |
+
return response["response"]
|
backend/engines/reducer.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from numpy.typing import NDArray
|
| 3 |
+
from typing import List
|
| 4 |
+
import umap
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class ReducerEngine:
|
| 8 |
+
_last_fitted_reducer = None
|
| 9 |
+
|
| 10 |
+
def __init__(self, n_neighbors, min_dist):
|
| 11 |
+
self.n_neighbors = n_neighbors
|
| 12 |
+
self.min_dist = min_dist
|
| 13 |
+
|
| 14 |
+
def reduce(self, embeddings: List[List[float]]) -> List[List[float]]:
|
| 15 |
+
n_samples = len(embeddings)
|
| 16 |
+
|
| 17 |
+
if n_samples == 0:
|
| 18 |
+
return []
|
| 19 |
+
|
| 20 |
+
if n_samples == 1:
|
| 21 |
+
return [[0.0, 0.1]]
|
| 22 |
+
|
| 23 |
+
if n_samples < 5:
|
| 24 |
+
return [[0.0, i * 0.1] for i in range(n_samples)]
|
| 25 |
+
|
| 26 |
+
data = np.array(embeddings)
|
| 27 |
+
|
| 28 |
+
safe_n_neighbors = min(self.n_neighbors, n_samples - 1)
|
| 29 |
+
safe_n_neighbors = max(2, safe_n_neighbors)
|
| 30 |
+
|
| 31 |
+
reducer = umap.UMAP(
|
| 32 |
+
n_neighbors=safe_n_neighbors,
|
| 33 |
+
min_dist=self.min_dist,
|
| 34 |
+
metric="cosine",
|
| 35 |
+
random_state=42,
|
| 36 |
+
n_components=2,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
coords_2d: NDArray[np.float32] = np.asarray(reducer.fit_transform(data))
|
| 40 |
+
|
| 41 |
+
ReducerEngine._last_fitted_reducer = reducer
|
| 42 |
+
|
| 43 |
+
return coords_2d.tolist()
|
backend/main.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import nltk
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from fastapi import FastAPI
|
| 5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
+
from fastapi.staticfiles import StaticFiles
|
| 7 |
+
from fastapi.responses import FileResponse
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def initialize_nltk():
|
| 11 |
+
nltk_data_dir = os.path.expanduser("~/nltk_data")
|
| 12 |
+
os.makedirs(nltk_data_dir, exist_ok=True)
|
| 13 |
+
|
| 14 |
+
if nltk_data_dir not in nltk.data.path:
|
| 15 |
+
nltk.data.path.append(nltk_data_dir)
|
| 16 |
+
|
| 17 |
+
resources = {
|
| 18 |
+
"tokenizers/punkt": "punkt",
|
| 19 |
+
"tokenizers/punkt_tab": "punkt_tab",
|
| 20 |
+
"corpora/stopwords": "stopwords",
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
for path, package in resources.items():
|
| 24 |
+
try:
|
| 25 |
+
nltk.data.find(path)
|
| 26 |
+
print(f"Found NLTK resource: {package}")
|
| 27 |
+
except LookupError:
|
| 28 |
+
print(f"Downloading missing NLTK resource: {package} to {nltk_data_dir}...")
|
| 29 |
+
nltk.download(package, download_dir=nltk_data_dir)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
initialize_nltk()
|
| 33 |
+
|
| 34 |
+
from backend.routers.retrieval_router import router as retrieval_router
|
| 35 |
+
from backend.routers.chunk_router import router as chunk_router
|
| 36 |
+
|
| 37 |
+
app = FastAPI(
|
| 38 |
+
title="RAG Visualizer",
|
| 39 |
+
description="An X-Ray machine for RAG pipelines",
|
| 40 |
+
version="0.1.0",
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
app.add_middleware(
|
| 44 |
+
CORSMiddleware,
|
| 45 |
+
allow_origins=["*"],
|
| 46 |
+
allow_credentials=True,
|
| 47 |
+
allow_methods=["*"],
|
| 48 |
+
allow_headers=["*"],
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
app.include_router(chunk_router)
|
| 52 |
+
app.include_router(retrieval_router)
|
| 53 |
+
|
| 54 |
+
FRONTEND_DIR = Path(__file__).resolve().parent.parent / "frontend"
|
| 55 |
+
app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR)), name="static")
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@app.get("/")
|
| 59 |
+
def serve_frontend():
|
| 60 |
+
return FileResponse(str(FRONTEND_DIR / "index.html"))
|
backend/models/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Models package — Pydantic schemas (API contracts)
|
backend/models/schemas.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from enum import Enum
|
| 2 |
+
from typing import Any, Dict, List, Literal, Optional
|
| 3 |
+
from pydantic import BaseModel, Field, model_validator
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
# --- Chunk-related schemas ---
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class Strategy(str, Enum):
|
| 10 |
+
FIXED_SIZE = "fixed_size"
|
| 11 |
+
SENTENCE = "sentence"
|
| 12 |
+
RECURSIVE = "recursive"
|
| 13 |
+
PARENT_CHILD = "parent_child"
|
| 14 |
+
SEMANTIC = "semantic"
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class EmbeddingModel(str, Enum):
|
| 18 |
+
NOMIC_EMBED_TEXT = "nomic-embed-text"
|
| 19 |
+
EMBEDDING_GEMMA = "EmbeddingGemma"
|
| 20 |
+
QWEN_EMBEDDING = "qwen3-embedding:0.6b"
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class RetrievalMode(str, Enum):
|
| 24 |
+
DENSE = "dense"
|
| 25 |
+
SPARSE = "sparse"
|
| 26 |
+
HYBRID = "hybrid"
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class ChunkConfig(BaseModel):
|
| 30 |
+
chunk_size: int = 500
|
| 31 |
+
chunk_overlap: int = 20
|
| 32 |
+
semantic_threshold: float = 0.5
|
| 33 |
+
separators: Optional[List[str]] = None
|
| 34 |
+
tokenizer: str = "cl100k_base"
|
| 35 |
+
parent_chunk_size: Optional[int] = None
|
| 36 |
+
parent_chunk_overlap: Optional[int] = None
|
| 37 |
+
child_chunk_size: Optional[int] = None
|
| 38 |
+
child_chunk_overlap: Optional[int] = None
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class ChunkNode(BaseModel):
|
| 42 |
+
id: str
|
| 43 |
+
order: int
|
| 44 |
+
text: str
|
| 45 |
+
token_count: int
|
| 46 |
+
start_char: int
|
| 47 |
+
end_char: int
|
| 48 |
+
level: int = 0
|
| 49 |
+
parent_id: Optional[str] = None
|
| 50 |
+
child_ids: List[str] = []
|
| 51 |
+
metadata: Dict[str, Any] = {}
|
| 52 |
+
embeddings: Optional[List[float]] = None
|
| 53 |
+
coords_2d: Optional[List[float]] = None
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class StrategyRun(BaseModel):
|
| 57 |
+
strategy: Strategy
|
| 58 |
+
config: ChunkConfig
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class StrategyResult(BaseModel):
|
| 62 |
+
strategy: Strategy
|
| 63 |
+
chunks: List[ChunkNode]
|
| 64 |
+
total_chunks: int
|
| 65 |
+
avg_token_count: int
|
| 66 |
+
total_tokens: int
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class ChunkRequest(BaseModel):
|
| 70 |
+
text: str
|
| 71 |
+
runs: List[StrategyRun]
|
| 72 |
+
embedding_model: EmbeddingModel = EmbeddingModel.NOMIC_EMBED_TEXT
|
| 73 |
+
n_neighbors: int = 15
|
| 74 |
+
min_dist: float = 0.1
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class ChunkResponse(BaseModel):
|
| 78 |
+
results: List[StrategyResult]
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class QueryRequest(BaseModel):
|
| 82 |
+
search_text: str
|
| 83 |
+
embedding_model: EmbeddingModel
|
| 84 |
+
strategy: Strategy
|
| 85 |
+
top_k: int = 3
|
| 86 |
+
retrieval_mode: RetrievalMode = RetrievalMode.DENSE
|
| 87 |
+
use_hyde: bool = False
|
| 88 |
+
use_reranking: bool = False
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class RetrievedChunk(BaseModel):
|
| 92 |
+
id: str
|
| 93 |
+
text: str
|
| 94 |
+
score: float
|
| 95 |
+
start_char: int
|
| 96 |
+
end_char: int
|
| 97 |
+
parent_id: Optional[str] = None
|
| 98 |
+
level: int = 0
|
| 99 |
+
text_highlighted: Optional[str] = None
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class QueryResponse(BaseModel):
|
| 103 |
+
query_text: str
|
| 104 |
+
query_coords: List[float] = [0.0, 0.0]
|
| 105 |
+
results: List[RetrievedChunk]
|
| 106 |
+
hypothetical_answer: Optional[str] = None
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class CompareRequest(BaseModel):
|
| 110 |
+
search_text: str
|
| 111 |
+
top_k: int = 3
|
| 112 |
+
model_a: EmbeddingModel
|
| 113 |
+
strategy_a: Strategy
|
| 114 |
+
model_b: EmbeddingModel
|
| 115 |
+
strategy_b: Strategy
|
| 116 |
+
|
| 117 |
+
retrieval_mode: RetrievalMode = RetrievalMode.DENSE
|
| 118 |
+
use_hyde: bool = False
|
| 119 |
+
use_reranking: bool = False
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
class CompareResponse(BaseModel):
|
| 123 |
+
search_text: str
|
| 124 |
+
results_a: List[RetrievedChunk]
|
| 125 |
+
results_b: List[RetrievedChunk]
|
| 126 |
+
hypothetical_answer: Optional[str] = None
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
class JudgeRequest(BaseModel):
|
| 130 |
+
search_query: str
|
| 131 |
+
chunk_a: str
|
| 132 |
+
chunk_b: str
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
class ChunkScore(BaseModel):
|
| 136 |
+
query_relevance: int = Field(ge=1, le=10)
|
| 137 |
+
answer_completeness: int = Field(ge=1, le=10)
|
| 138 |
+
factual_plausibility: int = Field(ge=1, le=10)
|
| 139 |
+
clarity: int = Field(ge=1, le=10)
|
| 140 |
+
overall: float
|
| 141 |
+
|
| 142 |
+
@model_validator(mode="after")
|
| 143 |
+
def check_overall(self) -> "ChunkScore":
|
| 144 |
+
expected = round(
|
| 145 |
+
(
|
| 146 |
+
self.query_relevance
|
| 147 |
+
+ self.answer_completeness
|
| 148 |
+
+ self.factual_plausibility
|
| 149 |
+
+ self.clarity
|
| 150 |
+
)
|
| 151 |
+
/ 4,
|
| 152 |
+
2,
|
| 153 |
+
)
|
| 154 |
+
self.overall = expected
|
| 155 |
+
return self
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
class JudgeResponse(BaseModel):
|
| 159 |
+
winner: Literal["chunk_a", "chunk_b", "tie"]
|
| 160 |
+
confidence: float = Field(ge=0, le=1)
|
| 161 |
+
|
| 162 |
+
chunk_a_score: ChunkScore
|
| 163 |
+
chunk_b_score: ChunkScore
|
| 164 |
+
|
| 165 |
+
winner_reason: str
|
| 166 |
+
chunk_a_strengths: List[str]
|
| 167 |
+
chunk_b_strengths: List[str]
|
| 168 |
+
chunk_a_weaknesses: List[str]
|
| 169 |
+
chunk_b_weaknesses: List[str]
|
| 170 |
+
deciding_dimension: str
|
| 171 |
+
|
| 172 |
+
@model_validator(mode="after")
|
| 173 |
+
def check_winner_consistency(self) -> "JudgeResponse":
|
| 174 |
+
a = self.chunk_a_score.overall
|
| 175 |
+
b = self.chunk_b_score.overall
|
| 176 |
+
gap = abs(a - b)
|
| 177 |
+
|
| 178 |
+
if gap <= 0.5:
|
| 179 |
+
self.winner = "tie"
|
| 180 |
+
elif a > b:
|
| 181 |
+
self.winner = "chunk_a"
|
| 182 |
+
else:
|
| 183 |
+
self.winner = "chunk_b"
|
| 184 |
+
return self
|
backend/routers/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Routers package — API endpoint definitions
|
backend/routers/chunk_router.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List
|
| 2 |
+
from fastapi import APIRouter, responses
|
| 3 |
+
from pydantic import config
|
| 4 |
+
|
| 5 |
+
from backend.engines.chunking import ChunkingEngine
|
| 6 |
+
from backend.engines.embedding import EmbeddingEngine
|
| 7 |
+
from backend.engines.reducer import ReducerEngine
|
| 8 |
+
from backend.models.schemas import (
|
| 9 |
+
ChunkNode,
|
| 10 |
+
ChunkRequest,
|
| 11 |
+
ChunkResponse,
|
| 12 |
+
Strategy,
|
| 13 |
+
StrategyResult,
|
| 14 |
+
)
|
| 15 |
+
from backend.storage.vector_store import VectorStore
|
| 16 |
+
|
| 17 |
+
# TODO: import your schemas and engine
|
| 18 |
+
|
| 19 |
+
router = APIRouter(prefix="/api", tags=["chunking"])
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# TODO: GET /strategies endpoint
|
| 23 |
+
@router.get("/strategies", response_model=List[Strategy])
|
| 24 |
+
def get_strategies():
|
| 25 |
+
return list(Strategy)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# TODO: POST /chunk endpoint
|
| 29 |
+
@router.post("/chunk", response_model=ChunkResponse)
|
| 30 |
+
async def create_chunk(request: ChunkRequest):
|
| 31 |
+
chunk_engine = ChunkingEngine()
|
| 32 |
+
embedding_engine = EmbeddingEngine(request.embedding_model.value)
|
| 33 |
+
reducer_engine = ReducerEngine(request.n_neighbors, request.min_dist)
|
| 34 |
+
vector_store = VectorStore()
|
| 35 |
+
response = []
|
| 36 |
+
all_chunks = []
|
| 37 |
+
strategy_results_map = {}
|
| 38 |
+
|
| 39 |
+
for run in request.runs:
|
| 40 |
+
strategy = run.strategy
|
| 41 |
+
config = run.config
|
| 42 |
+
chunks: List[ChunkNode] = await chunk_engine.chunk(
|
| 43 |
+
text=request.text,
|
| 44 |
+
strategy=strategy,
|
| 45 |
+
config=config,
|
| 46 |
+
embedding_model=request.embedding_model.value,
|
| 47 |
+
)
|
| 48 |
+
strategy_results_map[strategy] = chunks
|
| 49 |
+
all_chunks.extend(chunks)
|
| 50 |
+
|
| 51 |
+
embeddings = await embedding_engine.generate_embeddings(all_chunks)
|
| 52 |
+
|
| 53 |
+
ids = [c.id for c in all_chunks]
|
| 54 |
+
documents = [c.text for c in all_chunks]
|
| 55 |
+
|
| 56 |
+
chunk_metadatas = [
|
| 57 |
+
{
|
| 58 |
+
"start_char": c.start_char,
|
| 59 |
+
"end_char": c.end_char,
|
| 60 |
+
"token_count": c.token_count,
|
| 61 |
+
"level": c.level,
|
| 62 |
+
"parent_id": c.parent_id or "",
|
| 63 |
+
}
|
| 64 |
+
for c in all_chunks
|
| 65 |
+
]
|
| 66 |
+
|
| 67 |
+
await vector_store.upsert(
|
| 68 |
+
collection_name=f"{request.embedding_model.value}_{request.runs[0].strategy.value}".replace(":", "-"),
|
| 69 |
+
ids=ids,
|
| 70 |
+
documents=documents,
|
| 71 |
+
embeddings=embeddings,
|
| 72 |
+
metadatas=chunk_metadatas,
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
coords_2d = reducer_engine.reduce(embeddings)
|
| 76 |
+
|
| 77 |
+
for chunk, coords in zip(all_chunks, coords_2d):
|
| 78 |
+
chunk.coords_2d = coords
|
| 79 |
+
|
| 80 |
+
for strategy, chunks in strategy_results_map.items():
|
| 81 |
+
total_chunks = len(chunks)
|
| 82 |
+
total_tokens = sum(node.token_count for node in chunks)
|
| 83 |
+
avg_tokens = total_tokens / total_chunks if total_chunks > 0 else 0
|
| 84 |
+
|
| 85 |
+
response.append(
|
| 86 |
+
StrategyResult(
|
| 87 |
+
strategy=strategy,
|
| 88 |
+
chunks=chunks,
|
| 89 |
+
total_chunks=total_chunks,
|
| 90 |
+
total_tokens=total_tokens,
|
| 91 |
+
avg_token_count=int(avg_tokens),
|
| 92 |
+
)
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
return ChunkResponse(results=response)
|
backend/routers/retrieval_router.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from backend.constants import hyde_prompt
|
| 2 |
+
from backend.engines import llm_client
|
| 3 |
+
from backend.engines.llm_client import OllamaClient
|
| 4 |
+
from backend.constants import system_instructions
|
| 5 |
+
import asyncio
|
| 6 |
+
from typing import Any, Optional
|
| 7 |
+
from fastapi import APIRouter
|
| 8 |
+
import json
|
| 9 |
+
import re
|
| 10 |
+
from backend.engines.embedding import EmbeddingEngine
|
| 11 |
+
from backend.engines.reducer import ReducerEngine
|
| 12 |
+
from backend.models.schemas import (
|
| 13 |
+
CompareRequest,
|
| 14 |
+
CompareResponse,
|
| 15 |
+
JudgeResponse,
|
| 16 |
+
JudgeRequest,
|
| 17 |
+
QueryRequest,
|
| 18 |
+
QueryResponse,
|
| 19 |
+
RetrievedChunk,
|
| 20 |
+
)
|
| 21 |
+
from backend.storage.vector_store import VectorStore
|
| 22 |
+
|
| 23 |
+
from nltk.tokenize import RegexpTokenizer
|
| 24 |
+
from nltk.corpus import stopwords
|
| 25 |
+
|
| 26 |
+
router = APIRouter(prefix="/api", tags=["retrieval"])
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
tokenizer = RegexpTokenizer(r"\w+")
|
| 30 |
+
stop_words = set(stopwords.words("english"))
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@router.post("/retrieve", response_model=QueryResponse)
|
| 34 |
+
async def retrieve(request: QueryRequest):
|
| 35 |
+
|
| 36 |
+
search_text = (
|
| 37 |
+
await get_hyde_text(request.search_text)
|
| 38 |
+
if request.use_hyde
|
| 39 |
+
else request.search_text
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
if request.use_reranking == True:
|
| 43 |
+
"" ""
|
| 44 |
+
|
| 45 |
+
retrieved_chunks, embeddings = await process_retrieval(
|
| 46 |
+
request.embedding_model,
|
| 47 |
+
search_text,
|
| 48 |
+
request.strategy,
|
| 49 |
+
request.top_k,
|
| 50 |
+
request.search_text,
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
query_coords = [0.0, 0.0]
|
| 54 |
+
if ReducerEngine._last_fitted_reducer is not None:
|
| 55 |
+
projected: Any = ReducerEngine._last_fitted_reducer.transform(embeddings)
|
| 56 |
+
query_coords = projected[0].tolist()
|
| 57 |
+
|
| 58 |
+
response_kwargs = {
|
| 59 |
+
"query_text": request.search_text,
|
| 60 |
+
"query_coords": query_coords,
|
| 61 |
+
"results": retrieved_chunks,
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
if request.use_hyde:
|
| 65 |
+
response_kwargs["hypothetical_answer"] = search_text
|
| 66 |
+
|
| 67 |
+
return QueryResponse(**response_kwargs)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@router.post("/compare", response_model=CompareResponse)
|
| 71 |
+
async def compare(request: CompareRequest):
|
| 72 |
+
retrieval_text = (
|
| 73 |
+
await get_hyde_text(request.search_text)
|
| 74 |
+
if request.use_hyde
|
| 75 |
+
else request.search_text
|
| 76 |
+
)
|
| 77 |
+
(
|
| 78 |
+
(retrieved_chunks_a, _),
|
| 79 |
+
(retrieved_chunks_b, _),
|
| 80 |
+
) = await asyncio.gather(
|
| 81 |
+
process_retrieval(
|
| 82 |
+
request.model_a,
|
| 83 |
+
retrieval_text,
|
| 84 |
+
request.strategy_a,
|
| 85 |
+
request.top_k,
|
| 86 |
+
request.search_text,
|
| 87 |
+
),
|
| 88 |
+
process_retrieval(
|
| 89 |
+
request.model_b,
|
| 90 |
+
retrieval_text,
|
| 91 |
+
request.strategy_b,
|
| 92 |
+
request.top_k,
|
| 93 |
+
request.search_text,
|
| 94 |
+
),
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
response_kwargs = {
|
| 98 |
+
"search_text": request.search_text,
|
| 99 |
+
"results_a": retrieved_chunks_a,
|
| 100 |
+
"results_b": retrieved_chunks_b,
|
| 101 |
+
}
|
| 102 |
+
if request.use_hyde:
|
| 103 |
+
response_kwargs["hypothetical_answer"] = retrieval_text
|
| 104 |
+
|
| 105 |
+
return CompareResponse(**response_kwargs)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
@router.post("/judge", response_model=JudgeResponse)
|
| 109 |
+
async def judge(request: JudgeRequest):
|
| 110 |
+
prompt = system_instructions.format(
|
| 111 |
+
search_query=request.search_query,
|
| 112 |
+
chunk_a=request.chunk_a,
|
| 113 |
+
chunk_b=request.chunk_b,
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
llm_client = OllamaClient()
|
| 117 |
+
result = await llm_client.generate(prompt=prompt)
|
| 118 |
+
|
| 119 |
+
print(f"res:res: {result}")
|
| 120 |
+
|
| 121 |
+
result_dict = json.loads(result)
|
| 122 |
+
|
| 123 |
+
return JudgeResponse(**result_dict)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
async def process_retrieval(
|
| 127 |
+
model, search_text, strategy, top_k, original_query: Optional[str] = None
|
| 128 |
+
):
|
| 129 |
+
vector_store = VectorStore()
|
| 130 |
+
embedding_engine = EmbeddingEngine(model.value)
|
| 131 |
+
embeddings = await embedding_engine.generate_embeddings([search_text])
|
| 132 |
+
result: Any = await vector_store.retrieve(
|
| 133 |
+
collection_name=f"{model.value}_{strategy.value}".replace(":", "-"),
|
| 134 |
+
embeddings=embeddings,
|
| 135 |
+
n_results=top_k,
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
retrieved_chunks = []
|
| 139 |
+
|
| 140 |
+
highlight_text = original_query if original_query is not None else search_text
|
| 141 |
+
|
| 142 |
+
# Check if result contains valid data
|
| 143 |
+
if result and "ids" in result and result["ids"]:
|
| 144 |
+
ids = result["ids"][0]
|
| 145 |
+
documents = result["documents"][0]
|
| 146 |
+
distances = result["distances"][0]
|
| 147 |
+
metadatas = result["metadatas"][0]
|
| 148 |
+
|
| 149 |
+
for i in range(len(ids)):
|
| 150 |
+
text_highlighted = get_text_highlights(documents[i], highlight_text)
|
| 151 |
+
meta = metadatas[i] or {}
|
| 152 |
+
retrieved_chunks.append(
|
| 153 |
+
RetrievedChunk(
|
| 154 |
+
id=ids[i],
|
| 155 |
+
text=documents[i],
|
| 156 |
+
score=distances[i],
|
| 157 |
+
start_char=meta.get("start_char", 0),
|
| 158 |
+
end_char=meta.get("end_char", 0),
|
| 159 |
+
parent_id=meta.get("parent_id") or None,
|
| 160 |
+
level=meta.get("level", 0),
|
| 161 |
+
text_highlighted=text_highlighted,
|
| 162 |
+
)
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
return retrieved_chunks, embeddings
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def get_text_highlights(original_text, search_text):
|
| 169 |
+
query_keywords = get_keywords(search_text)
|
| 170 |
+
if not query_keywords:
|
| 171 |
+
return original_text
|
| 172 |
+
|
| 173 |
+
query_keywords.sort(key=len, reverse=True)
|
| 174 |
+
|
| 175 |
+
safe_keywords = [f"{re.escape(w)}(?:'s)?" for w in query_keywords]
|
| 176 |
+
|
| 177 |
+
pattern_string = r"\b(" + "|".join(safe_keywords) + r")\b"
|
| 178 |
+
pattern = re.compile(pattern_string, re.IGNORECASE)
|
| 179 |
+
|
| 180 |
+
return pattern.sub(r"<mark>\1</mark>", original_text)
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def get_keywords(text: str):
|
| 184 |
+
token = tokenizer.tokenize(text.lower())
|
| 185 |
+
return [word for word in token if word not in stop_words]
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
async def get_hyde_text(search_text):
|
| 189 |
+
llm_client = OllamaClient()
|
| 190 |
+
hyde_prompt.format(search_text=search_text)
|
| 191 |
+
return await llm_client.generate(hyde_prompt, response_format=None)
|
backend/storage/vector_store.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
from chromadb import PersistentClient
|
| 3 |
+
import asyncio
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class VectorStore:
|
| 7 |
+
def __init__(self):
|
| 8 |
+
base_dir = Path(__file__).resolve().parent.parent.parent
|
| 9 |
+
store_path = base_dir / "store"
|
| 10 |
+
self.client = PersistentClient(path=str(store_path))
|
| 11 |
+
|
| 12 |
+
def get_collection(self, name: str):
|
| 13 |
+
return self.client.get_or_create_collection(
|
| 14 |
+
name=name, metadata={"hnsw:space": "cosine"}
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
async def upsert(self, collection_name, ids, documents, embeddings, metadatas=None):
|
| 18 |
+
collection = self.get_collection(collection_name)
|
| 19 |
+
|
| 20 |
+
await asyncio.to_thread(
|
| 21 |
+
collection.upsert,
|
| 22 |
+
ids=ids,
|
| 23 |
+
documents=documents,
|
| 24 |
+
embeddings=embeddings,
|
| 25 |
+
metadatas=metadatas,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
async def retrieve(self, collection_name, embeddings, n_results=3):
|
| 29 |
+
collection = self.get_collection(collection_name)
|
| 30 |
+
|
| 31 |
+
return await asyncio.to_thread(
|
| 32 |
+
collection.query, query_embeddings=embeddings, n_results=n_results
|
| 33 |
+
)
|
dev.bat
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
uv run uvicorn backend.main:app --reload --port 8080
|
frontend/app.js
ADDED
|
@@ -0,0 +1,1652 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* RAG Visualizer — Chunking Lab
|
| 3 |
+
* Frontend logic: API calls, chunk highlighting, and interactive inspection.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
// ============================================================
|
| 7 |
+
// State
|
| 8 |
+
// ============================================================
|
| 9 |
+
const state = {
|
| 10 |
+
text: "",
|
| 11 |
+
strategy: "fixed_size",
|
| 12 |
+
config: {
|
| 13 |
+
chunk_size: 500,
|
| 14 |
+
chunk_overlap: 20,
|
| 15 |
+
tokenizer: "cl100k_base",
|
| 16 |
+
separators: null,
|
| 17 |
+
parent_chunk_size: 1000,
|
| 18 |
+
parent_chunk_overlap: 100,
|
| 19 |
+
child_chunk_size: 200,
|
| 20 |
+
child_chunk_overlap: 20,
|
| 21 |
+
embedding_model: "nomic-embed-text",
|
| 22 |
+
n_neighbors: 15,
|
| 23 |
+
min_dist: 0.1,
|
| 24 |
+
semantic_threshold: 0.5,
|
| 25 |
+
},
|
| 26 |
+
activeTab: "xray-tab",
|
| 27 |
+
results: null, // StrategyResult from API
|
| 28 |
+
activeChunkId: null, // Currently highlighted chunk
|
| 29 |
+
isLoading: false,
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
// ============================================================
|
| 33 |
+
// DOM References
|
| 34 |
+
// ============================================================
|
| 35 |
+
const $ = (sel) => document.querySelector(sel);
|
| 36 |
+
const $$ = (sel) => document.querySelectorAll(sel);
|
| 37 |
+
|
| 38 |
+
const dom = {
|
| 39 |
+
textInput: $("#text-input"),
|
| 40 |
+
charCount: $("#char-count"),
|
| 41 |
+
strategyGrid: $("#strategy-grid"),
|
| 42 |
+
btnRun: $("#btn-run"),
|
| 43 |
+
xrayEmpty: $("#xray-empty"),
|
| 44 |
+
xrayText: $("#xray-text"),
|
| 45 |
+
chunkList: $("#chunk-list"),
|
| 46 |
+
statTotal: $("#stat-total"),
|
| 47 |
+
statAvgTokens: $("#stat-avg-tokens"),
|
| 48 |
+
statTotalTokens: $("#stat-total-tokens"),
|
| 49 |
+
statStrategy: $("#stat-strategy"),
|
| 50 |
+
standardConfig: $("#standard-config"),
|
| 51 |
+
parentChildConfig: $("#parent-child-config"),
|
| 52 |
+
semanticConfig: $("#semantic-config"),
|
| 53 |
+
separatorsSection: $("#separators-section"),
|
| 54 |
+
separatorTags: $("#separator-tags"),
|
| 55 |
+
separatorInput: $("#separator-input"),
|
| 56 |
+
|
| 57 |
+
// Vector & UMAP config elements
|
| 58 |
+
embeddingModel: $("#embedding-model"),
|
| 59 |
+
nNeighbors: $("#n-neighbors"),
|
| 60 |
+
minDist: $("#min-dist"),
|
| 61 |
+
nNeighborsValue: $("#n-neighbors-value"),
|
| 62 |
+
minDistValue: $("#min-dist-value"),
|
| 63 |
+
|
| 64 |
+
// Tab elements
|
| 65 |
+
tabButtons: $$(".tab-btn"),
|
| 66 |
+
tabContents: $$(".tab-content"),
|
| 67 |
+
|
| 68 |
+
// Canvas elements
|
| 69 |
+
vectorCanvas: $("#vector-canvas"),
|
| 70 |
+
vectorTooltip: $("#vector-tooltip"),
|
| 71 |
+
|
| 72 |
+
// Query Simulator elements
|
| 73 |
+
queryInput: $("#query-input"),
|
| 74 |
+
btnQuery: $("#btn-query"),
|
| 75 |
+
queryResultsDrawer: $("#query-results-drawer"),
|
| 76 |
+
queryResultsList: $("#query-results-list"),
|
| 77 |
+
closeDrawer: $("#close-drawer"),
|
| 78 |
+
};
|
| 79 |
+
|
| 80 |
+
// ============================================================
|
| 81 |
+
// Slider & Selection Bindings
|
| 82 |
+
// ============================================================
|
| 83 |
+
const sliders = [
|
| 84 |
+
{ id: "chunk-size", stateKey: "chunk_size", valueId: "chunk-size-value" },
|
| 85 |
+
{ id: "overlap", stateKey: "chunk_overlap", valueId: "overlap-value" },
|
| 86 |
+
{
|
| 87 |
+
id: "parent-size",
|
| 88 |
+
stateKey: "parent_chunk_size",
|
| 89 |
+
valueId: "parent-size-value",
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
id: "parent-overlap",
|
| 93 |
+
stateKey: "parent_chunk_overlap",
|
| 94 |
+
valueId: "parent-overlap-value",
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
id: "child-size",
|
| 98 |
+
stateKey: "child_chunk_size",
|
| 99 |
+
valueId: "child-size-value",
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
id: "child-overlap",
|
| 103 |
+
stateKey: "child_chunk_overlap",
|
| 104 |
+
valueId: "child-overlap-value",
|
| 105 |
+
},
|
| 106 |
+
{ id: "n-neighbors", stateKey: "n_neighbors", valueId: "n-neighbors-value" },
|
| 107 |
+
{
|
| 108 |
+
id: "min-dist",
|
| 109 |
+
stateKey: "min_dist",
|
| 110 |
+
valueId: "min-dist-value",
|
| 111 |
+
isFloat: true,
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
id: "semantic-threshold",
|
| 115 |
+
stateKey: "semantic_threshold",
|
| 116 |
+
valueId: "semantic-threshold-value",
|
| 117 |
+
isFloat: true,
|
| 118 |
+
},
|
| 119 |
+
];
|
| 120 |
+
|
| 121 |
+
sliders.forEach(({ id, stateKey, valueId, isFloat }) => {
|
| 122 |
+
const slider = $(`#${id}`);
|
| 123 |
+
const badge = $(`#${valueId}`);
|
| 124 |
+
if (!slider || !badge) return;
|
| 125 |
+
|
| 126 |
+
slider.addEventListener("input", () => {
|
| 127 |
+
const val = isFloat ? parseFloat(slider.value) : parseInt(slider.value, 10);
|
| 128 |
+
state.config[stateKey] = val;
|
| 129 |
+
badge.textContent = isFloat ? val.toFixed(2) : val;
|
| 130 |
+
});
|
| 131 |
+
});
|
| 132 |
+
|
| 133 |
+
if (dom.embeddingModel) {
|
| 134 |
+
dom.embeddingModel.addEventListener("change", () => {
|
| 135 |
+
state.config.embedding_model = dom.embeddingModel.value;
|
| 136 |
+
});
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
// ============================================================
|
| 140 |
+
// Interactive Tab Switching
|
| 141 |
+
// ============================================================
|
| 142 |
+
dom.tabButtons.forEach((btn) => {
|
| 143 |
+
btn.addEventListener("click", () => {
|
| 144 |
+
const targetTab = btn.dataset.tab;
|
| 145 |
+
state.activeTab = targetTab;
|
| 146 |
+
|
| 147 |
+
// Toggle active classes on buttons
|
| 148 |
+
dom.tabButtons.forEach((b) => b.classList.toggle("active", b === btn));
|
| 149 |
+
|
| 150 |
+
// Toggle active content panels
|
| 151 |
+
dom.tabContents.forEach((content) => {
|
| 152 |
+
const isTarget = content.id === targetTab;
|
| 153 |
+
content.classList.toggle("active", isTarget);
|
| 154 |
+
content.style.display = isTarget ? "flex" : "none";
|
| 155 |
+
});
|
| 156 |
+
|
| 157 |
+
// If switching to vector tab, let the canvas redraw itself
|
| 158 |
+
if (
|
| 159 |
+
targetTab === "vector-tab" &&
|
| 160 |
+
typeof window.drawVectorSpace === "function"
|
| 161 |
+
) {
|
| 162 |
+
window.drawVectorSpace();
|
| 163 |
+
}
|
| 164 |
+
});
|
| 165 |
+
});
|
| 166 |
+
|
| 167 |
+
// ============================================================
|
| 168 |
+
// Text Input
|
| 169 |
+
// ============================================================
|
| 170 |
+
dom.textInput.addEventListener("input", () => {
|
| 171 |
+
state.text = dom.textInput.value;
|
| 172 |
+
dom.charCount.textContent = `${state.text.length} chars`;
|
| 173 |
+
});
|
| 174 |
+
|
| 175 |
+
// ============================================================
|
| 176 |
+
// Strategy Selector
|
| 177 |
+
// ============================================================
|
| 178 |
+
dom.strategyGrid.addEventListener("click", (e) => {
|
| 179 |
+
const card = e.target.closest(".strategy-card");
|
| 180 |
+
if (!card) return;
|
| 181 |
+
|
| 182 |
+
$$(".strategy-card").forEach((c) => c.classList.remove("active"));
|
| 183 |
+
card.classList.add("active");
|
| 184 |
+
card.querySelector('input[type="radio"]').checked = true;
|
| 185 |
+
|
| 186 |
+
state.strategy = card.dataset.strategy;
|
| 187 |
+
updateConfigVisibility();
|
| 188 |
+
});
|
| 189 |
+
|
| 190 |
+
function updateConfigVisibility() {
|
| 191 |
+
const isParentChild = state.strategy === "parent_child";
|
| 192 |
+
const isRecursive = state.strategy === "recursive";
|
| 193 |
+
const isSemantic = state.strategy === "semantic";
|
| 194 |
+
|
| 195 |
+
// Show/hide standard config (hide for parent_child and semantic)
|
| 196 |
+
dom.standardConfig.style.display =
|
| 197 |
+
isParentChild || isSemantic ? "none" : "block";
|
| 198 |
+
|
| 199 |
+
// Show/hide parent-child config
|
| 200 |
+
dom.parentChildConfig.classList.toggle("visible", isParentChild);
|
| 201 |
+
|
| 202 |
+
// Show/hide semantic config
|
| 203 |
+
if (dom.semanticConfig) {
|
| 204 |
+
dom.semanticConfig.style.display = isSemantic ? "block" : "none";
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
// Show/hide separators (for recursive and parent_child)
|
| 208 |
+
dom.separatorsSection.style.display =
|
| 209 |
+
isRecursive || isParentChild ? "block" : "none";
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
// ============================================================
|
| 213 |
+
// Separator Tags
|
| 214 |
+
// ============================================================
|
| 215 |
+
function getSeparators() {
|
| 216 |
+
const tags = dom.separatorTags.querySelectorAll(".separator-tag");
|
| 217 |
+
if (tags.length === 0) return null;
|
| 218 |
+
return Array.from(tags).map((tag) => {
|
| 219 |
+
const raw = tag.dataset.sep;
|
| 220 |
+
// Convert escaped sequences back to real characters
|
| 221 |
+
return raw.replace(/\\n/g, "\n").replace(/\\t/g, "\t");
|
| 222 |
+
});
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
// Remove separator tag
|
| 226 |
+
dom.separatorTags.addEventListener("click", (e) => {
|
| 227 |
+
if (e.target.classList.contains("remove")) {
|
| 228 |
+
e.target.closest(".separator-tag").remove();
|
| 229 |
+
}
|
| 230 |
+
});
|
| 231 |
+
|
| 232 |
+
// Add separator tag
|
| 233 |
+
dom.separatorInput.addEventListener("keydown", (e) => {
|
| 234 |
+
if (e.key === "Enter" && dom.separatorInput.value.trim()) {
|
| 235 |
+
e.preventDefault();
|
| 236 |
+
const val = dom.separatorInput.value;
|
| 237 |
+
const displayVal = val
|
| 238 |
+
.replace(/\n/g, "\\n")
|
| 239 |
+
.replace(/\t/g, "\\t")
|
| 240 |
+
.replace(/ /g, "⎵");
|
| 241 |
+
const tag = document.createElement("span");
|
| 242 |
+
tag.className = "separator-tag";
|
| 243 |
+
tag.dataset.sep = val;
|
| 244 |
+
tag.innerHTML = `<code>${escapeHtml(displayVal)}</code><span class="remove">×</span>`;
|
| 245 |
+
dom.separatorTags.insertBefore(tag, dom.separatorInput);
|
| 246 |
+
dom.separatorInput.value = "";
|
| 247 |
+
}
|
| 248 |
+
});
|
| 249 |
+
|
| 250 |
+
// ============================================================
|
| 251 |
+
// API Call
|
| 252 |
+
// ============================================================
|
| 253 |
+
async function runChunking() {
|
| 254 |
+
if (state.isLoading || !state.text.trim()) return;
|
| 255 |
+
|
| 256 |
+
// Reset visualizer and search highlights
|
| 257 |
+
state.results = [];
|
| 258 |
+
state.activeChunkId = null;
|
| 259 |
+
unhighlightChunk();
|
| 260 |
+
dom.xrayText.classList.remove("search-active");
|
| 261 |
+
|
| 262 |
+
state.isLoading = true;
|
| 263 |
+
dom.btnRun.classList.add("loading");
|
| 264 |
+
dom.btnRun.classList.remove("ready");
|
| 265 |
+
dom.btnRun.disabled = true;
|
| 266 |
+
|
| 267 |
+
// Build request payload
|
| 268 |
+
const runConfig = { tokenizer: state.config.tokenizer };
|
| 269 |
+
|
| 270 |
+
if (state.strategy === "parent_child") {
|
| 271 |
+
runConfig.parent_chunk_size = state.config.parent_chunk_size;
|
| 272 |
+
runConfig.parent_chunk_overlap = state.config.parent_chunk_overlap;
|
| 273 |
+
runConfig.child_chunk_size = state.config.child_chunk_size;
|
| 274 |
+
runConfig.child_chunk_overlap = state.config.child_chunk_overlap;
|
| 275 |
+
} else if (state.strategy === "semantic") {
|
| 276 |
+
runConfig.semantic_threshold = state.config.semantic_threshold;
|
| 277 |
+
} else {
|
| 278 |
+
runConfig.chunk_size = state.config.chunk_size;
|
| 279 |
+
runConfig.chunk_overlap = state.config.chunk_overlap;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
// Add separators if applicable
|
| 283 |
+
const separators = getSeparators();
|
| 284 |
+
if (
|
| 285 |
+
separators &&
|
| 286 |
+
(state.strategy === "recursive" || state.strategy === "parent_child")
|
| 287 |
+
) {
|
| 288 |
+
runConfig.separators = separators;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
const payload = {
|
| 292 |
+
text: state.text,
|
| 293 |
+
runs: [
|
| 294 |
+
{
|
| 295 |
+
strategy: state.strategy,
|
| 296 |
+
config: runConfig,
|
| 297 |
+
},
|
| 298 |
+
],
|
| 299 |
+
embedding_model: state.config.embedding_model,
|
| 300 |
+
n_neighbors: state.config.n_neighbors,
|
| 301 |
+
min_dist: state.config.min_dist,
|
| 302 |
+
};
|
| 303 |
+
|
| 304 |
+
try {
|
| 305 |
+
const res = await fetch("/api/chunk", {
|
| 306 |
+
method: "POST",
|
| 307 |
+
headers: { "Content-Type": "application/json" },
|
| 308 |
+
body: JSON.stringify(payload),
|
| 309 |
+
});
|
| 310 |
+
|
| 311 |
+
if (!res.ok) {
|
| 312 |
+
const err = await res.json();
|
| 313 |
+
console.error("API Error:", err);
|
| 314 |
+
alert(`API Error: ${JSON.stringify(err.detail || err)}`);
|
| 315 |
+
return;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
const data = await res.json();
|
| 319 |
+
state.results = data.results[0]; // Single strategy for now
|
| 320 |
+
state.activeChunkId = null;
|
| 321 |
+
|
| 322 |
+
renderResults();
|
| 323 |
+
} catch (err) {
|
| 324 |
+
console.error("Network error:", err);
|
| 325 |
+
alert("Failed to connect to the API. Is the server running?");
|
| 326 |
+
} finally {
|
| 327 |
+
state.isLoading = false;
|
| 328 |
+
dom.btnRun.classList.remove("loading");
|
| 329 |
+
dom.btnRun.disabled = false;
|
| 330 |
+
dom.btnRun.classList.add("ready");
|
| 331 |
+
}
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
dom.btnRun.addEventListener("click", runChunking);
|
| 335 |
+
|
| 336 |
+
// ============================================================
|
| 337 |
+
// Rendering
|
| 338 |
+
// ============================================================
|
| 339 |
+
function renderResults() {
|
| 340 |
+
if (!state.results) return;
|
| 341 |
+
|
| 342 |
+
const { chunks, total_chunks, avg_token_count, total_tokens, strategy } =
|
| 343 |
+
state.results;
|
| 344 |
+
|
| 345 |
+
// Compute stats if backend returned 0s
|
| 346 |
+
const computedTotal = total_chunks || chunks.length;
|
| 347 |
+
const computedTotalTokens =
|
| 348 |
+
total_tokens || chunks.reduce((sum, c) => sum + c.token_count, 0);
|
| 349 |
+
const computedAvg =
|
| 350 |
+
avg_token_count ||
|
| 351 |
+
(computedTotal > 0 ? Math.round(computedTotalTokens / computedTotal) : 0);
|
| 352 |
+
|
| 353 |
+
// Update stats
|
| 354 |
+
dom.statTotal.textContent = computedTotal;
|
| 355 |
+
dom.statAvgTokens.textContent = computedAvg;
|
| 356 |
+
dom.statTotalTokens.textContent = computedTotalTokens;
|
| 357 |
+
dom.statStrategy.textContent = strategy;
|
| 358 |
+
|
| 359 |
+
// Render X-Ray viewer
|
| 360 |
+
renderXrayText(chunks);
|
| 361 |
+
|
| 362 |
+
// Render chunk list
|
| 363 |
+
renderChunkList(chunks);
|
| 364 |
+
|
| 365 |
+
// Initialize and draw Vector Space (Phase 2)
|
| 366 |
+
if (typeof window.initVectorSpace === "function") {
|
| 367 |
+
window.initVectorSpace(chunks);
|
| 368 |
+
}
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
// ============================================================
|
| 372 |
+
// X-Ray Text Viewer — Highlight chunks in the original text
|
| 373 |
+
// ============================================================
|
| 374 |
+
function renderXrayText(chunks) {
|
| 375 |
+
dom.xrayEmpty.style.display = "none";
|
| 376 |
+
dom.xrayText.style.display = "block";
|
| 377 |
+
|
| 378 |
+
const text = state.text;
|
| 379 |
+
if (!text) return;
|
| 380 |
+
|
| 381 |
+
// 1. Create an array of "markers" for every character in the text
|
| 382 |
+
// Each marker will store which chunks "own" this character
|
| 383 |
+
const markers = Array.from({ length: text.length }, () => []);
|
| 384 |
+
|
| 385 |
+
chunks.forEach((chunk) => {
|
| 386 |
+
for (let i = chunk.start_char; i < chunk.end_char; i++) {
|
| 387 |
+
if (i >= 0 && i < markers.length) {
|
| 388 |
+
markers[i].push(chunk.id);
|
| 389 |
+
}
|
| 390 |
+
}
|
| 391 |
+
});
|
| 392 |
+
|
| 393 |
+
// 2. Build the HTML by grouping characters with the same "owners"
|
| 394 |
+
let html = "";
|
| 395 |
+
let currentOwnersId = null;
|
| 396 |
+
let currentBuffer = "";
|
| 397 |
+
|
| 398 |
+
for (let i = 0; i <= text.length; i++) {
|
| 399 |
+
const owners = markers[i] || [];
|
| 400 |
+
const ownersId = owners.join(",");
|
| 401 |
+
|
| 402 |
+
if (ownersId !== currentOwnersId) {
|
| 403 |
+
if (currentBuffer) {
|
| 404 |
+
if (!currentOwnersId) {
|
| 405 |
+
html += escapeHtml(currentBuffer);
|
| 406 |
+
} else {
|
| 407 |
+
const ownerList = currentOwnersId.split(",");
|
| 408 |
+
const isOverlap = ownerList.length > 1;
|
| 409 |
+
const primaryOwner = ownerList[0];
|
| 410 |
+
const colorIdx = (ownerList.length % 4) + 1;
|
| 411 |
+
|
| 412 |
+
html +=
|
| 413 |
+
`<span class="chunk-highlight${isOverlap ? " overlap-region" : ""}" ` +
|
| 414 |
+
`data-chunk-id="${primaryOwner}" ` +
|
| 415 |
+
`data-all-chunks="${currentOwnersId}" ` +
|
| 416 |
+
`data-color="${colorIdx}">` +
|
| 417 |
+
escapeHtml(currentBuffer) +
|
| 418 |
+
`</span>`;
|
| 419 |
+
}
|
| 420 |
+
}
|
| 421 |
+
currentOwnersId = ownersId;
|
| 422 |
+
currentBuffer = "";
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
if (i < text.length) {
|
| 426 |
+
currentBuffer += text[i];
|
| 427 |
+
}
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
dom.xrayText.innerHTML = html;
|
| 431 |
+
|
| 432 |
+
// Add hover/click listeners to highlights
|
| 433 |
+
dom.xrayText.querySelectorAll(".chunk-highlight").forEach((el) => {
|
| 434 |
+
el.addEventListener("mouseenter", () => highlightChunk(el.dataset.chunkId));
|
| 435 |
+
el.addEventListener("mouseleave", () => unhighlightChunk());
|
| 436 |
+
el.addEventListener("click", () => selectChunk(el.dataset.chunkId));
|
| 437 |
+
});
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
// ============================================================
|
| 441 |
+
// Chunk List (Right Panel)
|
| 442 |
+
// ============================================================
|
| 443 |
+
function renderChunkList(chunks) {
|
| 444 |
+
let html = "";
|
| 445 |
+
let currentParentId = null;
|
| 446 |
+
|
| 447 |
+
chunks.forEach((chunk, index) => {
|
| 448 |
+
// Add parent label for parent-child strategy
|
| 449 |
+
if (chunk.level === 0 && state.strategy === "parent_child") {
|
| 450 |
+
html += `<div class="parent-child-label">📦 Parent</div>`;
|
| 451 |
+
currentParentId = chunk.id;
|
| 452 |
+
} else if (chunk.level === 1 && chunk.parent_id !== currentParentId) {
|
| 453 |
+
currentParentId = chunk.parent_id;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
const preview =
|
| 457 |
+
chunk.text.length > 120 ? chunk.text.slice(0, 120) + "…" : chunk.text;
|
| 458 |
+
|
| 459 |
+
html += `
|
| 460 |
+
<div class="chunk-item" data-chunk-id="${chunk.id}" data-level="${chunk.level}" data-index="${index + 1}">
|
| 461 |
+
<div class="chunk-item-header">
|
| 462 |
+
<span class="chunk-item-id">${chunk.id}</span>
|
| 463 |
+
<div class="chunk-item-badges">
|
| 464 |
+
<span class="chunk-badge tokens">${chunk.token_count} tok</span>
|
| 465 |
+
<span class="chunk-badge">${chunk.end_char - chunk.start_char} chars</span>
|
| 466 |
+
</div>
|
| 467 |
+
</div>
|
| 468 |
+
<div class="chunk-item-text">${escapeHtml(preview)}</div>
|
| 469 |
+
<div class="chunk-item-meta">
|
| 470 |
+
<span>start: ${chunk.start_char}</span>
|
| 471 |
+
<span>end: ${chunk.end_char}</span>
|
| 472 |
+
${chunk.parent_id ? `<span>parent: ${chunk.parent_id}</span>` : ""}
|
| 473 |
+
</div>
|
| 474 |
+
</div>
|
| 475 |
+
`;
|
| 476 |
+
});
|
| 477 |
+
|
| 478 |
+
dom.chunkList.innerHTML = html;
|
| 479 |
+
|
| 480 |
+
// Add click/hover listeners
|
| 481 |
+
dom.chunkList.querySelectorAll(".chunk-item").forEach((el) => {
|
| 482 |
+
el.addEventListener("mouseenter", () => highlightChunk(el.dataset.chunkId));
|
| 483 |
+
el.addEventListener("mouseleave", () => unhighlightChunk());
|
| 484 |
+
el.addEventListener("click", () => selectChunk(el.dataset.chunkId));
|
| 485 |
+
});
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
// ============================================================
|
| 489 |
+
// Highlight / Selection Logic
|
| 490 |
+
// ============================================================
|
| 491 |
+
function highlightChunk(chunkId) {
|
| 492 |
+
// Highlight in X-ray viewer
|
| 493 |
+
dom.xrayText.querySelectorAll(".chunk-highlight").forEach((el) => {
|
| 494 |
+
el.classList.toggle("active", el.dataset.chunkId === chunkId);
|
| 495 |
+
});
|
| 496 |
+
|
| 497 |
+
// Highlight in chunk list
|
| 498 |
+
dom.chunkList.querySelectorAll(".chunk-item").forEach((el) => {
|
| 499 |
+
el.classList.toggle("active", el.dataset.chunkId === chunkId);
|
| 500 |
+
});
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
function unhighlightChunk() {
|
| 504 |
+
if (state.activeChunkId) {
|
| 505 |
+
// If a chunk is "selected" (clicked), keep it highlighted
|
| 506 |
+
highlightChunk(state.activeChunkId);
|
| 507 |
+
return;
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
dom.xrayText.querySelectorAll(".chunk-highlight.active").forEach((el) => {
|
| 511 |
+
el.classList.remove("active");
|
| 512 |
+
});
|
| 513 |
+
dom.chunkList.querySelectorAll(".chunk-item.active").forEach((el) => {
|
| 514 |
+
el.classList.remove("active");
|
| 515 |
+
});
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
window.selectChunk = function selectChunk(chunkId) {
|
| 519 |
+
// Toggle selection
|
| 520 |
+
state.activeChunkId = state.activeChunkId === chunkId ? null : chunkId;
|
| 521 |
+
if (state.activeChunkId) {
|
| 522 |
+
highlightChunk(state.activeChunkId);
|
| 523 |
+
|
| 524 |
+
// Scroll the chunk into view in the X-ray panel
|
| 525 |
+
const xrayEl = dom.xrayText.querySelector(`[data-chunk-id="${chunkId}"]`);
|
| 526 |
+
if (xrayEl) {
|
| 527 |
+
xrayEl.scrollIntoView({ behavior: "smooth", block: "center" });
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
// Scroll the chunk into view in the list
|
| 531 |
+
const listEl = dom.chunkList.querySelector(`[data-chunk-id="${chunkId}"]`);
|
| 532 |
+
if (listEl) {
|
| 533 |
+
listEl.scrollIntoView({ behavior: "smooth", block: "center" });
|
| 534 |
+
}
|
| 535 |
+
} else {
|
| 536 |
+
unhighlightChunk();
|
| 537 |
+
}
|
| 538 |
+
};
|
| 539 |
+
|
| 540 |
+
function escapeHtml(str) {
|
| 541 |
+
const div = document.createElement("div");
|
| 542 |
+
div.textContent = str;
|
| 543 |
+
return div.innerHTML;
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
document.addEventListener("keydown", (e) => {
|
| 547 |
+
// Ctrl/Cmd + Enter to run
|
| 548 |
+
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
| 549 |
+
e.preventDefault();
|
| 550 |
+
runChunking();
|
| 551 |
+
}
|
| 552 |
+
});
|
| 553 |
+
|
| 554 |
+
updateConfigVisibility();
|
| 555 |
+
|
| 556 |
+
// Local visual state for canvas physics and scaling
|
| 557 |
+
const vectorState = {
|
| 558 |
+
chunks: [], // Holds our active chunks
|
| 559 |
+
zoom: 1.0, // Scroll scale
|
| 560 |
+
pan: { x: 0, y: 0 }, // Drag offset
|
| 561 |
+
hoveredChunk: null, // Chunk under mouse cursor
|
| 562 |
+
selectedChunk: null, // Active clicked chunk
|
| 563 |
+
isDragging: false, // Drag state flag
|
| 564 |
+
dragStart: { x: 0, y: 0 },
|
| 565 |
+
activeQuery: null, // Active search query or sonar probe click
|
| 566 |
+
|
| 567 |
+
// High/low UMAP coordinates (for scaling math)
|
| 568 |
+
bounds: {
|
| 569 |
+
minX: 0,
|
| 570 |
+
maxX: 0,
|
| 571 |
+
minY: 0,
|
| 572 |
+
maxY: 0,
|
| 573 |
+
},
|
| 574 |
+
};
|
| 575 |
+
|
| 576 |
+
// Stubs for you to implement the magical vector space rendering:
|
| 577 |
+
window.initVectorSpace = function (chunks) {
|
| 578 |
+
console.log("Initializing Vector Space with chunks:", chunks);
|
| 579 |
+
vectorState.chunks = chunks;
|
| 580 |
+
|
| 581 |
+
if (chunks.length > 0) {
|
| 582 |
+
const xs = chunks.map((c) => (c.coords_2d ? c.coords_2d[0] : 0));
|
| 583 |
+
const ys = chunks.map((c) => (c.coords_2d ? c.coords_2d[1] : 0));
|
| 584 |
+
|
| 585 |
+
vectorState.bounds.minX = Math.min(...xs);
|
| 586 |
+
vectorState.bounds.maxX = Math.max(...xs);
|
| 587 |
+
vectorState.bounds.minY = Math.min(...ys);
|
| 588 |
+
vectorState.bounds.maxY = Math.max(...ys);
|
| 589 |
+
}
|
| 590 |
+
const canvas = dom.vectorCanvas;
|
| 591 |
+
if (!canvas) return;
|
| 592 |
+
|
| 593 |
+
// Set logical dimensions matching the bounding box
|
| 594 |
+
const rect = canvas.parentNode.getBoundingClientRect();
|
| 595 |
+
canvas.width = rect.width * window.devicePixelRatio;
|
| 596 |
+
canvas.height = rect.height * window.devicePixelRatio;
|
| 597 |
+
canvas.style.width = "100%";
|
| 598 |
+
canvas.style.height = "100%";
|
| 599 |
+
|
| 600 |
+
// Center alignment resetting
|
| 601 |
+
vectorState.pan = { x: 0, y: 0 };
|
| 602 |
+
vectorState.zoom = 1.0;
|
| 603 |
+
vectorState.hoveredChunk = null;
|
| 604 |
+
vectorState.selectedChunk = null;
|
| 605 |
+
vectorState.activeQuery = null;
|
| 606 |
+
|
| 607 |
+
// Bind custom interactive event handlers
|
| 608 |
+
if (!canvas.dataset.listenersAttached) {
|
| 609 |
+
setupCanvasListeners(canvas);
|
| 610 |
+
canvas.dataset.listenersAttached = "true";
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
// Active smooth render animation loop
|
| 614 |
+
if (!window.animationLoopActive) {
|
| 615 |
+
startAnimationLoop();
|
| 616 |
+
}
|
| 617 |
+
};
|
| 618 |
+
|
| 619 |
+
window.animationLoopActive = false;
|
| 620 |
+
function startAnimationLoop() {
|
| 621 |
+
window.animationLoopActive = true;
|
| 622 |
+
function tick() {
|
| 623 |
+
window.drawVectorSpace();
|
| 624 |
+
requestAnimationFrame(tick);
|
| 625 |
+
}
|
| 626 |
+
requestAnimationFrame(tick);
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
// Maps UMAP coordinates [x, y] to Canvas pixel coordinates [x, y]
|
| 630 |
+
function mapToCanvas(umapX, umapY, width, height) {
|
| 631 |
+
const padding = 50; // Keeps dots from clipping the canvas edges
|
| 632 |
+
const { minX, maxX, minY, maxY } = vectorState.bounds;
|
| 633 |
+
|
| 634 |
+
// Avoid division by zero if all coordinates are identical
|
| 635 |
+
const rangeX = maxX - minX || 1;
|
| 636 |
+
const rangeY = maxY - minY || 1;
|
| 637 |
+
|
| 638 |
+
// 1. Normalize UMAP value to a percentage (0.0 to 1.0)
|
| 639 |
+
const pctX = (umapX - minX) / rangeX;
|
| 640 |
+
const pctY = (umapY - minY) / rangeY;
|
| 641 |
+
|
| 642 |
+
// 2. Map percentage to canvas size (with padding)
|
| 643 |
+
const canvasX = padding + pctX * (width - padding * 2);
|
| 644 |
+
const canvasY = padding + pctY * (height - padding * 2);
|
| 645 |
+
|
| 646 |
+
return { x: canvasX, y: canvasY };
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
+
window.drawVectorSpace = function () {
|
| 650 |
+
const canvas = dom.vectorCanvas;
|
| 651 |
+
if (!canvas) return;
|
| 652 |
+
const ctx = canvas.getContext("2d");
|
| 653 |
+
|
| 654 |
+
// Self-healing resize guard (handles hidden container init and window resizing)
|
| 655 |
+
const rect = canvas.parentNode.getBoundingClientRect();
|
| 656 |
+
const targetWidth = Math.floor(rect.width * window.devicePixelRatio);
|
| 657 |
+
const targetHeight = Math.floor(rect.height * window.devicePixelRatio);
|
| 658 |
+
|
| 659 |
+
if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
|
| 660 |
+
if (rect.width > 0 && rect.height > 0) {
|
| 661 |
+
canvas.width = targetWidth;
|
| 662 |
+
canvas.height = targetHeight;
|
| 663 |
+
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
| 664 |
+
}
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
+
const width = canvas.width / window.devicePixelRatio;
|
| 668 |
+
const height = canvas.height / window.devicePixelRatio;
|
| 669 |
+
|
| 670 |
+
// Clear canvas
|
| 671 |
+
ctx.clearRect(0, 0, width, height);
|
| 672 |
+
|
| 673 |
+
// Apply Panning and Zooming transformations
|
| 674 |
+
ctx.save();
|
| 675 |
+
ctx.translate(vectorState.pan.x, vectorState.pan.y);
|
| 676 |
+
ctx.scale(vectorState.zoom, vectorState.zoom);
|
| 677 |
+
|
| 678 |
+
// A. DRAW GRID IN TRANSFORMED SPACE
|
| 679 |
+
ctx.strokeStyle = "rgba(15, 23, 42, 0.05)";
|
| 680 |
+
ctx.lineWidth = 1 / vectorState.zoom;
|
| 681 |
+
const gridSize = 40;
|
| 682 |
+
|
| 683 |
+
// Calculate bounding box in transformed space to draw grid infinite-scrolling
|
| 684 |
+
const startX = -vectorState.pan.x / vectorState.zoom;
|
| 685 |
+
const endX = (width - vectorState.pan.x) / vectorState.zoom;
|
| 686 |
+
const startY = -vectorState.pan.y / vectorState.zoom;
|
| 687 |
+
const endY = (height - vectorState.pan.y) / vectorState.zoom;
|
| 688 |
+
|
| 689 |
+
for (
|
| 690 |
+
let x = Math.floor(startX / gridSize) * gridSize;
|
| 691 |
+
x < endX;
|
| 692 |
+
x += gridSize
|
| 693 |
+
) {
|
| 694 |
+
ctx.beginPath();
|
| 695 |
+
ctx.moveTo(x, startY);
|
| 696 |
+
ctx.lineTo(x, endY);
|
| 697 |
+
ctx.stroke();
|
| 698 |
+
}
|
| 699 |
+
for (
|
| 700 |
+
let y = Math.floor(startY / gridSize) * gridSize;
|
| 701 |
+
y < endY;
|
| 702 |
+
y += gridSize
|
| 703 |
+
) {
|
| 704 |
+
ctx.beginPath();
|
| 705 |
+
ctx.moveTo(startX, y);
|
| 706 |
+
ctx.lineTo(endX, y);
|
| 707 |
+
ctx.stroke();
|
| 708 |
+
}
|
| 709 |
+
|
| 710 |
+
// B. DRAW PARENT-CHILD CONNECTION LINES
|
| 711 |
+
if (state.strategy === "parent_child") {
|
| 712 |
+
vectorState.chunks.forEach((chunk) => {
|
| 713 |
+
if (chunk.level === 1 && chunk.parent_id) {
|
| 714 |
+
const parent = vectorState.chunks.find((c) => c.id === chunk.parent_id);
|
| 715 |
+
if (parent && chunk.coords_2d && parent.coords_2d) {
|
| 716 |
+
const cpos = mapToCanvas(
|
| 717 |
+
chunk.coords_2d[0],
|
| 718 |
+
chunk.coords_2d[1],
|
| 719 |
+
width,
|
| 720 |
+
height,
|
| 721 |
+
);
|
| 722 |
+
const ppos = mapToCanvas(
|
| 723 |
+
parent.coords_2d[0],
|
| 724 |
+
parent.coords_2d[1],
|
| 725 |
+
width,
|
| 726 |
+
height,
|
| 727 |
+
);
|
| 728 |
+
|
| 729 |
+
ctx.beginPath();
|
| 730 |
+
ctx.strokeStyle = "rgba(245, 158, 11, 0.2)";
|
| 731 |
+
ctx.lineWidth = 1.5 / vectorState.zoom;
|
| 732 |
+
ctx.moveTo(ppos.x, ppos.y);
|
| 733 |
+
ctx.lineTo(cpos.x, cpos.y);
|
| 734 |
+
ctx.stroke();
|
| 735 |
+
}
|
| 736 |
+
}
|
| 737 |
+
});
|
| 738 |
+
} // C. DRAW ACTIVE RETRIEVAL LINES AND ANIMATIONS
|
| 739 |
+
if (vectorState.activeQuery) {
|
| 740 |
+
// Increment pulse radius for animation
|
| 741 |
+
vectorState.activeQuery.pulseRadius += 0.8;
|
| 742 |
+
if (vectorState.activeQuery.pulseRadius > 50) {
|
| 743 |
+
vectorState.activeQuery.pulseRadius = 0;
|
| 744 |
+
}
|
| 745 |
+
|
| 746 |
+
const qpos = mapToCanvas(
|
| 747 |
+
vectorState.activeQuery.x,
|
| 748 |
+
vectorState.activeQuery.y,
|
| 749 |
+
width,
|
| 750 |
+
height,
|
| 751 |
+
);
|
| 752 |
+
|
| 753 |
+
if (
|
| 754 |
+
vectorState.activeQuery.state === "fetching" ||
|
| 755 |
+
vectorState.activeQuery.state === "loaded"
|
| 756 |
+
) {
|
| 757 |
+
// Flowing lines or loaded lines to topK
|
| 758 |
+
const topK = vectorState.activeQuery.topK || [];
|
| 759 |
+
if (vectorState.activeQuery.state === "fetching") {
|
| 760 |
+
vectorState.activeQuery.flowOffset =
|
| 761 |
+
(vectorState.activeQuery.flowOffset || 0) - 2;
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
topK.forEach((item, index) => {
|
| 765 |
+
if (item.chunk.coords_2d) {
|
| 766 |
+
const cpos = mapToCanvas(
|
| 767 |
+
item.chunk.coords_2d[0],
|
| 768 |
+
item.chunk.coords_2d[1],
|
| 769 |
+
width,
|
| 770 |
+
height,
|
| 771 |
+
);
|
| 772 |
+
ctx.beginPath();
|
| 773 |
+
ctx.strokeStyle = "rgba(245, 158, 11, 0.6)"; // Stronger gold
|
| 774 |
+
ctx.lineWidth = 2 / vectorState.zoom;
|
| 775 |
+
ctx.setLineDash([5, 5]);
|
| 776 |
+
ctx.lineDashOffset =
|
| 777 |
+
vectorState.activeQuery.state === "fetching"
|
| 778 |
+
? vectorState.activeQuery.flowOffset
|
| 779 |
+
: 0;
|
| 780 |
+
ctx.moveTo(qpos.x, qpos.y);
|
| 781 |
+
ctx.lineTo(cpos.x, cpos.y);
|
| 782 |
+
ctx.stroke();
|
| 783 |
+
ctx.setLineDash([]);
|
| 784 |
+
}
|
| 785 |
+
});
|
| 786 |
+
}
|
| 787 |
+
}
|
| 788 |
+
// D. DRAW CHUNK PARTICLES
|
| 789 |
+
vectorState.chunks.forEach((chunk) => {
|
| 790 |
+
if (!chunk.coords_2d) return;
|
| 791 |
+
|
| 792 |
+
const cpos = mapToCanvas(
|
| 793 |
+
chunk.coords_2d[0],
|
| 794 |
+
chunk.coords_2d[1],
|
| 795 |
+
width,
|
| 796 |
+
height,
|
| 797 |
+
);
|
| 798 |
+
|
| 799 |
+
// Style according to strategy and highlights
|
| 800 |
+
let radius = 6;
|
| 801 |
+
let color = "#2563eb"; // Superman Blue default
|
| 802 |
+
let glowColor = "rgba(37, 99, 235, 0.15)";
|
| 803 |
+
|
| 804 |
+
const isHovered =
|
| 805 |
+
vectorState.hoveredChunk && vectorState.hoveredChunk.id === chunk.id;
|
| 806 |
+
const isSelected = state.activeChunkId === chunk.id;
|
| 807 |
+
|
| 808 |
+
if (state.strategy === "parent_child") {
|
| 809 |
+
if (chunk.level === 0) {
|
| 810 |
+
radius = 8;
|
| 811 |
+
color = "#f59e0b"; // Superman Gold Parent
|
| 812 |
+
glowColor = "rgba(245, 158, 11, 0.2)";
|
| 813 |
+
} else {
|
| 814 |
+
radius = 4.5;
|
| 815 |
+
color = "#2563eb"; // Superman Blue Child
|
| 816 |
+
glowColor = "rgba(37, 99, 235, 0.15)";
|
| 817 |
+
}
|
| 818 |
+
} else {
|
| 819 |
+
// Standard styles
|
| 820 |
+
radius = 5.5;
|
| 821 |
+
color = "#2563eb";
|
| 822 |
+
glowColor = "rgba(37, 99, 235, 0.15)";
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
// Dynamic scale-up on hover or selection (glowing Superman Red)
|
| 826 |
+
if (isHovered || isSelected) {
|
| 827 |
+
radius += 3;
|
| 828 |
+
color = "#ef4444"; // Superman Red
|
| 829 |
+
glowColor = isSelected
|
| 830 |
+
? "rgba(239, 68, 68, 0.45)"
|
| 831 |
+
: "rgba(239, 68, 68, 0.25)";
|
| 832 |
+
}
|
| 833 |
+
|
| 834 |
+
// 1. Draw glowing background shadow ring
|
| 835 |
+
ctx.beginPath();
|
| 836 |
+
ctx.arc(cpos.x, cpos.y, radius + 4, 0, Math.PI * 2);
|
| 837 |
+
ctx.fillStyle = glowColor;
|
| 838 |
+
ctx.fill();
|
| 839 |
+
|
| 840 |
+
// 2. Draw solid particle
|
| 841 |
+
ctx.beginPath();
|
| 842 |
+
ctx.arc(cpos.x, cpos.y, radius, 0, Math.PI * 2);
|
| 843 |
+
ctx.fillStyle = color;
|
| 844 |
+
ctx.strokeStyle = "#ffffff"; // Clean white border outline for Light Mode
|
| 845 |
+
ctx.lineWidth = 1.5;
|
| 846 |
+
ctx.fill();
|
| 847 |
+
ctx.stroke();
|
| 848 |
+
|
| 849 |
+
// 3. Draw highlighted white core on selection
|
| 850 |
+
if (isSelected) {
|
| 851 |
+
ctx.beginPath();
|
| 852 |
+
ctx.arc(cpos.x, cpos.y, 2, 0, Math.PI * 2);
|
| 853 |
+
ctx.fillStyle = "#ffffff";
|
| 854 |
+
ctx.fill();
|
| 855 |
+
}
|
| 856 |
+
|
| 857 |
+
// 4. Rank 1 Sun Glow if loaded
|
| 858 |
+
if (
|
| 859 |
+
vectorState.activeQuery &&
|
| 860 |
+
vectorState.activeQuery.state === "loaded" &&
|
| 861 |
+
vectorState.activeQuery.topK &&
|
| 862 |
+
vectorState.activeQuery.topK.length > 0
|
| 863 |
+
) {
|
| 864 |
+
if (chunk.id === vectorState.activeQuery.topK[0].chunk.id) {
|
| 865 |
+
// Pulsing sun glow radius
|
| 866 |
+
const sunRadius = 15 + Math.sin(Date.now() / 300) * 5;
|
| 867 |
+
ctx.beginPath();
|
| 868 |
+
ctx.arc(cpos.x, cpos.y, sunRadius, 0, Math.PI * 2);
|
| 869 |
+
ctx.fillStyle = "rgba(245, 158, 11, 0.3)";
|
| 870 |
+
ctx.fill();
|
| 871 |
+
ctx.beginPath();
|
| 872 |
+
ctx.arc(cpos.x, cpos.y, radius + 2, 0, Math.PI * 2);
|
| 873 |
+
ctx.fillStyle = "#f59e0b"; // Solid gold core
|
| 874 |
+
ctx.fill();
|
| 875 |
+
}
|
| 876 |
+
}
|
| 877 |
+
});
|
| 878 |
+
|
| 879 |
+
// E. DRAW ACTIVE QUERY & SONAR PING SHOCKWAVES
|
| 880 |
+
if (vectorState.activeQuery) {
|
| 881 |
+
const qpos = mapToCanvas(
|
| 882 |
+
vectorState.activeQuery.x,
|
| 883 |
+
vectorState.activeQuery.y,
|
| 884 |
+
width,
|
| 885 |
+
height,
|
| 886 |
+
);
|
| 887 |
+
|
| 888 |
+
// 1. Draw Sonar ripple pulse rings (Superman Red wave)
|
| 889 |
+
ctx.beginPath();
|
| 890 |
+
ctx.arc(
|
| 891 |
+
qpos.x,
|
| 892 |
+
qpos.y,
|
| 893 |
+
vectorState.activeQuery.pulseRadius,
|
| 894 |
+
0,
|
| 895 |
+
Math.PI * 2,
|
| 896 |
+
);
|
| 897 |
+
ctx.strokeStyle = `rgba(239, 68, 68, ${1 - vectorState.activeQuery.pulseRadius / 50})`;
|
| 898 |
+
ctx.lineWidth = 1.5 / vectorState.zoom;
|
| 899 |
+
ctx.stroke();
|
| 900 |
+
|
| 901 |
+
// 2. Draw outer glowing ring (Superman Red base)
|
| 902 |
+
ctx.beginPath();
|
| 903 |
+
ctx.arc(qpos.x, qpos.y, 8, 0, Math.PI * 2);
|
| 904 |
+
ctx.fillStyle = "rgba(239, 68, 68, 0.15)";
|
| 905 |
+
ctx.strokeStyle = "#ef4444";
|
| 906 |
+
ctx.lineWidth = 1.5 / vectorState.zoom;
|
| 907 |
+
ctx.fill();
|
| 908 |
+
ctx.stroke();
|
| 909 |
+
|
| 910 |
+
// 3. Draw crosshair target center
|
| 911 |
+
ctx.beginPath();
|
| 912 |
+
ctx.arc(qpos.x, qpos.y, 2, 0, Math.PI * 2);
|
| 913 |
+
ctx.fillStyle = "#ef4444";
|
| 914 |
+
ctx.fill();
|
| 915 |
+
|
| 916 |
+
// 4. Draw a small floating text label for the query
|
| 917 |
+
ctx.fillStyle = "#475569"; // Dark muted slate text for excellent light mode readability
|
| 918 |
+
ctx.font = `${Math.max(8, 10 / vectorState.zoom)}px var(--font-sans)`;
|
| 919 |
+
ctx.textAlign = "center";
|
| 920 |
+
ctx.fillText(
|
| 921 |
+
vectorState.activeQuery.label || "Sonar Query",
|
| 922 |
+
qpos.x,
|
| 923 |
+
qpos.y - 12,
|
| 924 |
+
);
|
| 925 |
+
}
|
| 926 |
+
|
| 927 |
+
ctx.restore();
|
| 928 |
+
};
|
| 929 |
+
|
| 930 |
+
function setupCanvasListeners(canvas) {
|
| 931 |
+
const getMousePos = (e) => {
|
| 932 |
+
const rect = canvas.getBoundingClientRect();
|
| 933 |
+
return {
|
| 934 |
+
x: e.clientX - rect.left,
|
| 935 |
+
y: e.clientY - rect.top,
|
| 936 |
+
};
|
| 937 |
+
};
|
| 938 |
+
|
| 939 |
+
canvas.addEventListener("mousedown", (e) => {
|
| 940 |
+
const pos = getMousePos(e);
|
| 941 |
+
vectorState.isDragging = true;
|
| 942 |
+
vectorState.dragStart = {
|
| 943 |
+
x: pos.x - vectorState.pan.x,
|
| 944 |
+
y: pos.y - vectorState.pan.y,
|
| 945 |
+
};
|
| 946 |
+
});
|
| 947 |
+
|
| 948 |
+
canvas.addEventListener("mousemove", (e) => {
|
| 949 |
+
const pos = getMousePos(e);
|
| 950 |
+
const width = canvas.width / window.devicePixelRatio;
|
| 951 |
+
const height = canvas.height / window.devicePixelRatio;
|
| 952 |
+
|
| 953 |
+
if (vectorState.isDragging) {
|
| 954 |
+
// Pan canvas
|
| 955 |
+
vectorState.pan.x = pos.x - vectorState.dragStart.x;
|
| 956 |
+
vectorState.pan.y = pos.y - vectorState.dragStart.y;
|
| 957 |
+
} else {
|
| 958 |
+
// Hover detection
|
| 959 |
+
const tx = (pos.x - vectorState.pan.x) / vectorState.zoom;
|
| 960 |
+
const ty = (pos.y - vectorState.pan.y) / vectorState.zoom;
|
| 961 |
+
|
| 962 |
+
let found = null;
|
| 963 |
+
let minDistance = Infinity;
|
| 964 |
+
|
| 965 |
+
vectorState.chunks.forEach((chunk) => {
|
| 966 |
+
if (!chunk.coords_2d) return;
|
| 967 |
+
const cpos = mapToCanvas(
|
| 968 |
+
chunk.coords_2d[0],
|
| 969 |
+
chunk.coords_2d[1],
|
| 970 |
+
width,
|
| 971 |
+
height,
|
| 972 |
+
);
|
| 973 |
+
const dist = Math.hypot(cpos.x - tx, cpos.y - ty);
|
| 974 |
+
|
| 975 |
+
if (dist < 12 && dist < minDistance) {
|
| 976 |
+
minDistance = dist;
|
| 977 |
+
found = chunk;
|
| 978 |
+
}
|
| 979 |
+
});
|
| 980 |
+
|
| 981 |
+
if (found !== vectorState.hoveredChunk) {
|
| 982 |
+
vectorState.hoveredChunk = found;
|
| 983 |
+
if (found) {
|
| 984 |
+
highlightChunk(found.id);
|
| 985 |
+
showTooltip(found, pos.x, pos.y);
|
| 986 |
+
} else {
|
| 987 |
+
unhighlightChunk();
|
| 988 |
+
hideTooltip();
|
| 989 |
+
}
|
| 990 |
+
}
|
| 991 |
+
}
|
| 992 |
+
});
|
| 993 |
+
|
| 994 |
+
canvas.addEventListener("mouseup", () => {
|
| 995 |
+
vectorState.isDragging = false;
|
| 996 |
+
});
|
| 997 |
+
|
| 998 |
+
canvas.addEventListener("mouseleave", () => {
|
| 999 |
+
vectorState.isDragging = false;
|
| 1000 |
+
vectorState.hoveredChunk = null;
|
| 1001 |
+
unhighlightChunk();
|
| 1002 |
+
hideTooltip();
|
| 1003 |
+
});
|
| 1004 |
+
|
| 1005 |
+
canvas.addEventListener(
|
| 1006 |
+
"wheel",
|
| 1007 |
+
(e) => {
|
| 1008 |
+
e.preventDefault();
|
| 1009 |
+
const pos = getMousePos(e);
|
| 1010 |
+
|
| 1011 |
+
// Decreased sensitivity with a smooth exponential multiplier
|
| 1012 |
+
const zoomFactor = 1 - e.deltaY * 0.0006;
|
| 1013 |
+
const prevZoom = vectorState.zoom;
|
| 1014 |
+
|
| 1015 |
+
// Smoothly clamp zoom scale between 0.15 and 8.0
|
| 1016 |
+
vectorState.zoom = Math.min(
|
| 1017 |
+
Math.max(vectorState.zoom * zoomFactor, 0.15),
|
| 1018 |
+
8.0,
|
| 1019 |
+
);
|
| 1020 |
+
|
| 1021 |
+
// Zoom centering physics
|
| 1022 |
+
vectorState.pan.x =
|
| 1023 |
+
pos.x - (pos.x - vectorState.pan.x) * (vectorState.zoom / prevZoom);
|
| 1024 |
+
vectorState.pan.y =
|
| 1025 |
+
pos.y - (pos.y - vectorState.pan.y) * (vectorState.zoom / prevZoom);
|
| 1026 |
+
},
|
| 1027 |
+
{ passive: false },
|
| 1028 |
+
);
|
| 1029 |
+
|
| 1030 |
+
canvas.addEventListener("click", (e) => {
|
| 1031 |
+
if (vectorState.isDragging) return;
|
| 1032 |
+
const pos = getMousePos(e);
|
| 1033 |
+
const width = canvas.width / window.devicePixelRatio;
|
| 1034 |
+
const height = canvas.height / window.devicePixelRatio;
|
| 1035 |
+
|
| 1036 |
+
const tx = (pos.x - vectorState.pan.x) / vectorState.zoom;
|
| 1037 |
+
const ty = (pos.y - vectorState.pan.y) / vectorState.zoom;
|
| 1038 |
+
|
| 1039 |
+
let clickedNode = null;
|
| 1040 |
+
let minDistance = Infinity;
|
| 1041 |
+
|
| 1042 |
+
vectorState.chunks.forEach((chunk) => {
|
| 1043 |
+
if (!chunk.coords_2d) return;
|
| 1044 |
+
const cpos = mapToCanvas(
|
| 1045 |
+
chunk.coords_2d[0],
|
| 1046 |
+
chunk.coords_2d[1],
|
| 1047 |
+
width,
|
| 1048 |
+
height,
|
| 1049 |
+
);
|
| 1050 |
+
const dist = Math.hypot(cpos.x - tx, cpos.y - ty);
|
| 1051 |
+
if (dist < 12 && dist < minDistance) {
|
| 1052 |
+
minDistance = dist;
|
| 1053 |
+
clickedNode = chunk;
|
| 1054 |
+
}
|
| 1055 |
+
});
|
| 1056 |
+
|
| 1057 |
+
if (clickedNode) {
|
| 1058 |
+
selectChunk(clickedNode.id);
|
| 1059 |
+
} else {
|
| 1060 |
+
// Probe click to trigger Sonar RAG retrieval!
|
| 1061 |
+
triggerSonarProbe(tx, ty);
|
| 1062 |
+
}
|
| 1063 |
+
});
|
| 1064 |
+
}
|
| 1065 |
+
|
| 1066 |
+
function showTooltip(chunk, x, y) {
|
| 1067 |
+
const tooltip = dom.vectorTooltip;
|
| 1068 |
+
if (!tooltip) return;
|
| 1069 |
+
|
| 1070 |
+
const snippet =
|
| 1071 |
+
chunk.text.length > 80 ? chunk.text.slice(0, 80) + "..." : chunk.text;
|
| 1072 |
+
const coordsStr = `[${chunk.coords_2d[0].toFixed(2)}, ${chunk.coords_2d[1].toFixed(2)}]`;
|
| 1073 |
+
|
| 1074 |
+
tooltip.style.display = "block";
|
| 1075 |
+
tooltip.style.left = `${x + 15}px`;
|
| 1076 |
+
tooltip.style.top = `${y + 15}px`;
|
| 1077 |
+
tooltip.innerHTML = `
|
| 1078 |
+
<div class="vector-tooltip-title">
|
| 1079 |
+
<span>${chunk.id}</span>
|
| 1080 |
+
<span class="vector-tooltip-coords">${coordsStr}</span>
|
| 1081 |
+
</div>
|
| 1082 |
+
<div style="font-size: 0.65rem; color: var(--text-tertiary); margin-bottom: var(--space-xs);">
|
| 1083 |
+
${chunk.token_count} tokens | ${chunk.text.length} chars
|
| 1084 |
+
</div>
|
| 1085 |
+
<div class="vector-tooltip-text">${escapeHtml(snippet)}</div>
|
| 1086 |
+
`;
|
| 1087 |
+
}
|
| 1088 |
+
|
| 1089 |
+
function hideTooltip() {
|
| 1090 |
+
const tooltip = dom.vectorTooltip;
|
| 1091 |
+
if (tooltip) tooltip.style.display = "none";
|
| 1092 |
+
}
|
| 1093 |
+
|
| 1094 |
+
function triggerSonarProbe(tx, ty) {
|
| 1095 |
+
const canvas = dom.vectorCanvas;
|
| 1096 |
+
const width = canvas.width / window.devicePixelRatio;
|
| 1097 |
+
const height = canvas.height / window.devicePixelRatio;
|
| 1098 |
+
|
| 1099 |
+
// Convert canvas position [tx, ty] back to UMAP [x, y] coordinates
|
| 1100 |
+
const padding = 50;
|
| 1101 |
+
const { minX, maxX, minY, maxY } = vectorState.bounds;
|
| 1102 |
+
const rangeX = maxX - minX || 1;
|
| 1103 |
+
const rangeY = maxY - minY || 1;
|
| 1104 |
+
|
| 1105 |
+
const pctX = (tx - padding) / (width - padding * 2);
|
| 1106 |
+
const pctY = (ty - padding) / (height - padding * 2);
|
| 1107 |
+
|
| 1108 |
+
const umapX = minX + pctX * rangeX;
|
| 1109 |
+
const umapY = minY + pctY * rangeY;
|
| 1110 |
+
|
| 1111 |
+
// Perform similarity distance in 2D space to prepare topK
|
| 1112 |
+
const scoredChunks = vectorState.chunks
|
| 1113 |
+
.map((chunk) => {
|
| 1114 |
+
if (!chunk.coords_2d) return { chunk, dist: Infinity };
|
| 1115 |
+
const dist = Math.hypot(
|
| 1116 |
+
chunk.coords_2d[0] - umapX,
|
| 1117 |
+
chunk.coords_2d[1] - umapY,
|
| 1118 |
+
);
|
| 1119 |
+
return { chunk, dist };
|
| 1120 |
+
})
|
| 1121 |
+
.sort((a, b) => a.dist - b.dist);
|
| 1122 |
+
|
| 1123 |
+
const topK = scoredChunks.slice(0, 3);
|
| 1124 |
+
|
| 1125 |
+
// Set active query crosshair
|
| 1126 |
+
vectorState.activeQuery = {
|
| 1127 |
+
x: umapX,
|
| 1128 |
+
y: umapY,
|
| 1129 |
+
tx: tx,
|
| 1130 |
+
ty: ty,
|
| 1131 |
+
pulseRadius: 0,
|
| 1132 |
+
label: "Sonar Probe",
|
| 1133 |
+
state: "loaded",
|
| 1134 |
+
topK: topK,
|
| 1135 |
+
};
|
| 1136 |
+
|
| 1137 |
+
// Render RAG results drawer
|
| 1138 |
+
dom.queryResultsDrawer.style.display = "block";
|
| 1139 |
+
dom.queryResultsList.innerHTML = topK
|
| 1140 |
+
.map((item, index) => {
|
| 1141 |
+
const similarity = Math.max(0, 1 - item.dist / 1.5).toFixed(2); // Normalised distance
|
| 1142 |
+
const snippet =
|
| 1143 |
+
item.chunk.text.length > 100
|
| 1144 |
+
? item.chunk.text.slice(0, 100) + "..."
|
| 1145 |
+
: item.chunk.text;
|
| 1146 |
+
const rankClass = index === 0 ? "rank-1" : "";
|
| 1147 |
+
return `
|
| 1148 |
+
<div class="retrieved-chunk-card ${rankClass}" onclick="selectChunk('${item.chunk.id}')">
|
| 1149 |
+
<div class="retrieved-chunk-meta">
|
| 1150 |
+
<span class="retrieved-chunk-rank">Rank ${index + 1} (${item.chunk.id})</span>
|
| 1151 |
+
<span class="retrieved-chunk-score">Match Score: ${similarity}</span>
|
| 1152 |
+
</div>
|
| 1153 |
+
<div class="retrieved-chunk-text">${escapeHtml(snippet)}</div>
|
| 1154 |
+
</div>
|
| 1155 |
+
`;
|
| 1156 |
+
})
|
| 1157 |
+
.join("");
|
| 1158 |
+
}
|
| 1159 |
+
|
| 1160 |
+
async function runQuerySimulator() {
|
| 1161 |
+
const queryText = dom.queryInput.value.trim();
|
| 1162 |
+
if (!queryText || !state.results || vectorState.chunks.length === 0) return;
|
| 1163 |
+
|
| 1164 |
+
dom.btnQuery.disabled = true;
|
| 1165 |
+
dom.btnQuery.textContent = "⚡ Search...";
|
| 1166 |
+
|
| 1167 |
+
try {
|
| 1168 |
+
console.log("Running Query against ChromaDB:", queryText);
|
| 1169 |
+
|
| 1170 |
+
// Reset camera view so the user can see the entire map
|
| 1171 |
+
vectorState.zoom = 1.0;
|
| 1172 |
+
vectorState.pan = { x: 0, y: 0 };
|
| 1173 |
+
|
| 1174 |
+
// Setup active query skeleton state (fetching)
|
| 1175 |
+
// We start at [0,0] and will update the actual coordinates once the server responds
|
| 1176 |
+
vectorState.activeQuery = {
|
| 1177 |
+
x: 0,
|
| 1178 |
+
y: 0,
|
| 1179 |
+
tx: 0,
|
| 1180 |
+
ty: 0,
|
| 1181 |
+
pulseRadius: 0,
|
| 1182 |
+
label: `Query: "${queryText.substring(0, 15)}"`,
|
| 1183 |
+
state: "fetching",
|
| 1184 |
+
flowOffset: 0,
|
| 1185 |
+
topK: [],
|
| 1186 |
+
};
|
| 1187 |
+
|
| 1188 |
+
dom.queryResultsDrawer.style.display = "block";
|
| 1189 |
+
dom.queryResultsList.innerHTML = `
|
| 1190 |
+
<div class="skeleton-loader"></div>
|
| 1191 |
+
<div class="skeleton-loader"></div>
|
| 1192 |
+
<div class="skeleton-loader"></div>
|
| 1193 |
+
`;
|
| 1194 |
+
|
| 1195 |
+
// Make the actual API call to the backend
|
| 1196 |
+
const res = await fetch("/api/retrieve", {
|
| 1197 |
+
method: "POST", // Needs to be POST to send a JSON body!
|
| 1198 |
+
headers: { "Content-Type": "application/json" },
|
| 1199 |
+
body: JSON.stringify({
|
| 1200 |
+
search_text: queryText,
|
| 1201 |
+
embedding_model: state.config.embedding_model,
|
| 1202 |
+
strategy: state.strategy,
|
| 1203 |
+
top_k: 3,
|
| 1204 |
+
}),
|
| 1205 |
+
});
|
| 1206 |
+
|
| 1207 |
+
if (!res.ok) {
|
| 1208 |
+
throw new Error(`HTTP error! status: ${res.status}`);
|
| 1209 |
+
}
|
| 1210 |
+
|
| 1211 |
+
const data = await res.json();
|
| 1212 |
+
|
| 1213 |
+
// Parse the query coordinates returned by UMAP
|
| 1214 |
+
const targetX = data.query_coords[0];
|
| 1215 |
+
const targetY = data.query_coords[1];
|
| 1216 |
+
|
| 1217 |
+
const canvas = dom.vectorCanvas;
|
| 1218 |
+
const width = canvas.width / window.devicePixelRatio;
|
| 1219 |
+
const height = canvas.height / window.devicePixelRatio;
|
| 1220 |
+
|
| 1221 |
+
// Dynamically expand bounds to fit the query dot if it lands far away
|
| 1222 |
+
if (vectorState.chunks.length > 0) {
|
| 1223 |
+
vectorState.bounds.minX = Math.min(vectorState.bounds.minX, targetX);
|
| 1224 |
+
vectorState.bounds.maxX = Math.max(vectorState.bounds.maxX, targetX);
|
| 1225 |
+
vectorState.bounds.minY = Math.min(vectorState.bounds.minY, targetY);
|
| 1226 |
+
vectorState.bounds.maxY = Math.max(vectorState.bounds.maxY, targetY);
|
| 1227 |
+
}
|
| 1228 |
+
|
| 1229 |
+
const screenPos = mapToCanvas(targetX, targetY, width, height);
|
| 1230 |
+
|
| 1231 |
+
// Link the retrieved results back to the frontend chunks for drawing lines
|
| 1232 |
+
const topK = data.results.map((result) => {
|
| 1233 |
+
// Find the corresponding chunk in the frontend's loaded chunks
|
| 1234 |
+
const chunk = vectorState.chunks.find((c) => c.id === result.id) || {
|
| 1235 |
+
id: result.id,
|
| 1236 |
+
text: result.text,
|
| 1237 |
+
coords_2d: null, // Fallback if chunk somehow isn't loaded
|
| 1238 |
+
};
|
| 1239 |
+
return {
|
| 1240 |
+
chunk: chunk,
|
| 1241 |
+
dist: result.score,
|
| 1242 |
+
text_highlighted: result.text_highlighted,
|
| 1243 |
+
};
|
| 1244 |
+
});
|
| 1245 |
+
|
| 1246 |
+
// Update the active query with the final data
|
| 1247 |
+
vectorState.activeQuery = {
|
| 1248 |
+
x: targetX,
|
| 1249 |
+
y: targetY,
|
| 1250 |
+
tx: screenPos.x,
|
| 1251 |
+
ty: screenPos.y,
|
| 1252 |
+
pulseRadius: 0,
|
| 1253 |
+
label: `Query: "${queryText.substring(0, 15)}"`,
|
| 1254 |
+
state: "loaded",
|
| 1255 |
+
flowOffset: 0,
|
| 1256 |
+
topK: topK,
|
| 1257 |
+
};
|
| 1258 |
+
|
| 1259 |
+
// Render results in the drawer
|
| 1260 |
+
dom.queryResultsList.innerHTML = topK
|
| 1261 |
+
.map((item, index) => {
|
| 1262 |
+
return `
|
| 1263 |
+
<div class="query-result-item" style="border-left-color: ${index === 0 ? "var(--warning-color)" : "var(--info-color)"}">
|
| 1264 |
+
<div class="query-result-header">
|
| 1265 |
+
<span class="rank-badge" style="color: ${index === 0 ? "var(--warning-color)" : "var(--info-color)"}">Rank ${index + 1} (${item.chunk.id})</span>
|
| 1266 |
+
<span class="dist-badge">Dist: ${item.dist.toFixed(3)}</span>
|
| 1267 |
+
</div>
|
| 1268 |
+
<div class="query-result-text">${item.text_highlighted || escapeHtml(item.chunk.text.substring(0, 100)) + "..."}</div>
|
| 1269 |
+
</div>
|
| 1270 |
+
`;
|
| 1271 |
+
})
|
| 1272 |
+
.join("");
|
| 1273 |
+
|
| 1274 |
+
// --- TASK 3.4: X-RAY DOCUMENT HIGHLIGHTING ---
|
| 1275 |
+
// 1. Activate the search mode on the X-Ray text to dim non-retrieved chunks
|
| 1276 |
+
dom.xrayText.classList.add("search-active");
|
| 1277 |
+
|
| 1278 |
+
// 2. Clear any previous search rankings
|
| 1279 |
+
dom.xrayText.querySelectorAll(".chunk-highlight").forEach((el) => {
|
| 1280 |
+
el.classList.remove(
|
| 1281 |
+
"retrieved-rank-1",
|
| 1282 |
+
"retrieved-rank-2",
|
| 1283 |
+
"retrieved-rank-3",
|
| 1284 |
+
);
|
| 1285 |
+
});
|
| 1286 |
+
|
| 1287 |
+
// 3. Highlight the new Top-K chunks in the Document Viewer
|
| 1288 |
+
topK.forEach((item, index) => {
|
| 1289 |
+
const rank = index + 1;
|
| 1290 |
+
const chunkId = item.chunk.id;
|
| 1291 |
+
const span = dom.xrayText.querySelector(`[data-chunk-id="${chunkId}"]`);
|
| 1292 |
+
|
| 1293 |
+
if (span) {
|
| 1294 |
+
span.classList.add(`retrieved-rank-${rank}`);
|
| 1295 |
+
// If it's the #1 hit, scroll the Document Viewer straight to it!
|
| 1296 |
+
if (rank === 1) {
|
| 1297 |
+
span.scrollIntoView({ behavior: "smooth", block: "center" });
|
| 1298 |
+
}
|
| 1299 |
+
}
|
| 1300 |
+
});
|
| 1301 |
+
// ----------------------------------------------
|
| 1302 |
+
} catch (err) {
|
| 1303 |
+
console.error("Query failed:", err);
|
| 1304 |
+
dom.queryResultsList.innerHTML = `<div style="color: #ef4444; padding: 1rem;">Failed to fetch results. Check console.</div>`;
|
| 1305 |
+
} finally {
|
| 1306 |
+
dom.btnQuery.disabled = false;
|
| 1307 |
+
dom.btnQuery.innerHTML = "🔍 Query";
|
| 1308 |
+
}
|
| 1309 |
+
}
|
| 1310 |
+
|
| 1311 |
+
// Bind Query events
|
| 1312 |
+
if (dom.btnQuery) {
|
| 1313 |
+
dom.btnQuery.addEventListener("click", runQuerySimulator);
|
| 1314 |
+
}
|
| 1315 |
+
if (dom.queryInput) {
|
| 1316 |
+
dom.queryInput.addEventListener("keydown", (e) => {
|
| 1317 |
+
if (e.key === "Enter") {
|
| 1318 |
+
runQuerySimulator();
|
| 1319 |
+
}
|
| 1320 |
+
});
|
| 1321 |
+
}
|
| 1322 |
+
if (dom.closeDrawer) {
|
| 1323 |
+
dom.closeDrawer.addEventListener("click", () => {
|
| 1324 |
+
dom.queryResultsDrawer.style.display = "none";
|
| 1325 |
+
});
|
| 1326 |
+
}
|
| 1327 |
+
|
| 1328 |
+
// ============================================================
|
| 1329 |
+
// TASK 3.5: ARENA COMPARISON LOGIC
|
| 1330 |
+
// ============================================================
|
| 1331 |
+
const domArena = {
|
| 1332 |
+
modal: document.getElementById("arena-modal"),
|
| 1333 |
+
btnOpen: document.getElementById("btn-open-arena"),
|
| 1334 |
+
btnClose: document.getElementById("btn-close-arena"),
|
| 1335 |
+
btnFight: document.getElementById("btn-arena-fight"),
|
| 1336 |
+
queryInput: document.getElementById("arena-query-input"),
|
| 1337 |
+
modelA: document.getElementById("arena-model-a"),
|
| 1338 |
+
strategyA: document.getElementById("arena-strategy-a"),
|
| 1339 |
+
resultsA: document.getElementById("arena-results-a"),
|
| 1340 |
+
modelB: document.getElementById("arena-model-b"),
|
| 1341 |
+
strategyB: document.getElementById("arena-strategy-b"),
|
| 1342 |
+
resultsB: document.getElementById("arena-results-b"),
|
| 1343 |
+
};
|
| 1344 |
+
|
| 1345 |
+
if (domArena.btnOpen) {
|
| 1346 |
+
domArena.btnOpen.addEventListener("click", () => {
|
| 1347 |
+
domArena.modal.style.display = "flex";
|
| 1348 |
+
if (dom.queryInput && dom.queryInput.value) {
|
| 1349 |
+
domArena.queryInput.value = dom.queryInput.value;
|
| 1350 |
+
}
|
| 1351 |
+
});
|
| 1352 |
+
}
|
| 1353 |
+
|
| 1354 |
+
if (domArena.btnClose) {
|
| 1355 |
+
domArena.btnClose.addEventListener("click", () => {
|
| 1356 |
+
domArena.modal.style.display = "none";
|
| 1357 |
+
});
|
| 1358 |
+
}
|
| 1359 |
+
|
| 1360 |
+
if (domArena.btnFight) {
|
| 1361 |
+
domArena.btnFight.addEventListener("click", async () => {
|
| 1362 |
+
const query = domArena.queryInput.value.trim();
|
| 1363 |
+
if (!query) return;
|
| 1364 |
+
|
| 1365 |
+
domArena.btnFight.disabled = true;
|
| 1366 |
+
domArena.btnFight.textContent = "FIGHTING...";
|
| 1367 |
+
|
| 1368 |
+
// Reset AI Referee if active
|
| 1369 |
+
if (typeof domReferee !== "undefined" && domReferee.panel) {
|
| 1370 |
+
domReferee.panel.style.display = "none";
|
| 1371 |
+
domReferee.verdictBox.style.display = "none";
|
| 1372 |
+
domReferee.loadingBox.style.display = "none";
|
| 1373 |
+
domReferee.triggerBox.style.display = "block";
|
| 1374 |
+
}
|
| 1375 |
+
|
| 1376 |
+
domArena.resultsA.innerHTML =
|
| 1377 |
+
'<div class="skeleton-loader"></div><div class="skeleton-loader"></div>';
|
| 1378 |
+
domArena.resultsB.innerHTML =
|
| 1379 |
+
'<div class="skeleton-loader"></div><div class="skeleton-loader"></div>';
|
| 1380 |
+
|
| 1381 |
+
try {
|
| 1382 |
+
const res = await fetch("/api/compare", {
|
| 1383 |
+
method: "POST",
|
| 1384 |
+
headers: { "Content-Type": "application/json" },
|
| 1385 |
+
body: JSON.stringify({
|
| 1386 |
+
search_text: query,
|
| 1387 |
+
top_k: 3,
|
| 1388 |
+
model_a: domArena.modelA.value,
|
| 1389 |
+
strategy_a: domArena.strategyA.value,
|
| 1390 |
+
model_b: domArena.modelB.value,
|
| 1391 |
+
strategy_b: domArena.strategyB.value,
|
| 1392 |
+
}),
|
| 1393 |
+
});
|
| 1394 |
+
|
| 1395 |
+
if (!res.ok)
|
| 1396 |
+
throw new Error(
|
| 1397 |
+
"Comparison failed. Check if server is running and /api/compare exists.",
|
| 1398 |
+
);
|
| 1399 |
+
|
| 1400 |
+
const data = await res.json();
|
| 1401 |
+
|
| 1402 |
+
// Render Column A
|
| 1403 |
+
domArena.resultsA.innerHTML = data.results_a
|
| 1404 |
+
.map(
|
| 1405 |
+
(chunk, i) => `
|
| 1406 |
+
<div class="query-result-item" style="border-left-color: var(--warning-color)">
|
| 1407 |
+
<div class="query-result-header">
|
| 1408 |
+
<span class="rank-badge" style="color: var(--warning-color)">Rank ${i + 1}</span>
|
| 1409 |
+
<span class="dist-badge">Dist: ${chunk.score.toFixed(3)}</span>
|
| 1410 |
+
</div>
|
| 1411 |
+
<div class="query-result-text">${chunk.text_highlighted || escapeHtml(chunk.text)}</div>
|
| 1412 |
+
</div>
|
| 1413 |
+
`,
|
| 1414 |
+
)
|
| 1415 |
+
.join("");
|
| 1416 |
+
|
| 1417 |
+
// Render Column B
|
| 1418 |
+
domArena.resultsB.innerHTML = data.results_b
|
| 1419 |
+
.map(
|
| 1420 |
+
(chunk, i) => `
|
| 1421 |
+
<div class="query-result-item" style="border-left-color: var(--info-color)">
|
| 1422 |
+
<div class="query-result-header">
|
| 1423 |
+
<span class="rank-badge" style="color: var(--info-color)">Rank ${i + 1}</span>
|
| 1424 |
+
<span class="dist-badge">Dist: ${chunk.score.toFixed(3)}</span>
|
| 1425 |
+
</div>
|
| 1426 |
+
<div class="query-result-text">${chunk.text_highlighted || escapeHtml(chunk.text)}</div>
|
| 1427 |
+
</div>
|
| 1428 |
+
`,
|
| 1429 |
+
)
|
| 1430 |
+
.join("");
|
| 1431 |
+
|
| 1432 |
+
// Activate AI Referee if both sides retrieved chunks
|
| 1433 |
+
if (
|
| 1434 |
+
typeof domReferee !== "undefined" &&
|
| 1435 |
+
domReferee.panel &&
|
| 1436 |
+
data.results_a.length > 0 &&
|
| 1437 |
+
data.results_b.length > 0
|
| 1438 |
+
) {
|
| 1439 |
+
domReferee.panel.style.display = "block";
|
| 1440 |
+
domReferee.btnCall.dataset.query = query;
|
| 1441 |
+
domReferee.btnCall.dataset.chunkA = data.results_a[0].text;
|
| 1442 |
+
domReferee.btnCall.dataset.chunkB = data.results_b[0].text;
|
| 1443 |
+
}
|
| 1444 |
+
} catch (err) {
|
| 1445 |
+
console.error(err);
|
| 1446 |
+
domArena.resultsA.innerHTML = `<div style="color: var(--superman-red); padding: 1rem;">Error: ${err.message}</div>`;
|
| 1447 |
+
domArena.resultsB.innerHTML = `<div style="color: var(--superman-red); padding: 1rem;">Error: ${err.message}</div>`;
|
| 1448 |
+
} finally {
|
| 1449 |
+
domArena.btnFight.disabled = false;
|
| 1450 |
+
domArena.btnFight.textContent = "🔥 FIGHT!";
|
| 1451 |
+
}
|
| 1452 |
+
});
|
| 1453 |
+
}
|
| 1454 |
+
|
| 1455 |
+
// ============================================================
|
| 1456 |
+
// TASK 5.1: AI REFEREE / LLM-AS-A-JUDGE LOGIC
|
| 1457 |
+
// ============================================================
|
| 1458 |
+
const domReferee = {
|
| 1459 |
+
panel: document.getElementById("arena-referee-panel"),
|
| 1460 |
+
triggerBox: document.getElementById("referee-trigger-box"),
|
| 1461 |
+
btnCall: document.getElementById("btn-call-referee"),
|
| 1462 |
+
loadingBox: document.getElementById("referee-loading-box"),
|
| 1463 |
+
statusText: document.getElementById("referee-status-text"),
|
| 1464 |
+
verdictBox: document.getElementById("referee-verdict-box"),
|
| 1465 |
+
};
|
| 1466 |
+
|
| 1467 |
+
if (domReferee.btnCall) {
|
| 1468 |
+
domReferee.btnCall.addEventListener("click", async () => {
|
| 1469 |
+
const query = domReferee.btnCall.dataset.query;
|
| 1470 |
+
const chunkA = domReferee.btnCall.dataset.chunkA;
|
| 1471 |
+
const chunkB = domReferee.btnCall.dataset.chunkB;
|
| 1472 |
+
if (!query || !chunkA || !chunkB) return;
|
| 1473 |
+
|
| 1474 |
+
// Capture user-selected model names for clean revealed scorecard display
|
| 1475 |
+
const labelA = domArena.modelA.options[domArena.modelA.selectedIndex].text;
|
| 1476 |
+
const labelB = domArena.modelB.options[domArena.modelB.selectedIndex].text;
|
| 1477 |
+
|
| 1478 |
+
domReferee.triggerBox.style.display = "none";
|
| 1479 |
+
domReferee.loadingBox.style.display = "block";
|
| 1480 |
+
domReferee.verdictBox.style.display = "none";
|
| 1481 |
+
|
| 1482 |
+
const statuses = [
|
| 1483 |
+
"⚖️ Summoning Gemma evaluation referee...",
|
| 1484 |
+
"🔍 Blinding model names and metadata...",
|
| 1485 |
+
"🧠 Auditing Corner A relevance and clarity...",
|
| 1486 |
+
"🧠 Auditing Corner B completeness and truthfulness...",
|
| 1487 |
+
"📊 Aggregating overall dimensional scores...",
|
| 1488 |
+
"✍️ Draft verdict and writing final card...",
|
| 1489 |
+
];
|
| 1490 |
+
let statusIdx = 0;
|
| 1491 |
+
domReferee.statusText.textContent = statuses[0];
|
| 1492 |
+
const statusInterval = setInterval(() => {
|
| 1493 |
+
statusIdx = (statusIdx + 1) % statuses.length;
|
| 1494 |
+
domReferee.statusText.textContent = statuses[statusIdx];
|
| 1495 |
+
}, 1500);
|
| 1496 |
+
|
| 1497 |
+
try {
|
| 1498 |
+
const res = await fetch("/api/judge", {
|
| 1499 |
+
method: "POST",
|
| 1500 |
+
headers: { "Content-Type": "application/json" },
|
| 1501 |
+
body: JSON.stringify({
|
| 1502 |
+
search_query: query,
|
| 1503 |
+
chunk_a: chunkA,
|
| 1504 |
+
chunk_b: chunkB,
|
| 1505 |
+
}),
|
| 1506 |
+
});
|
| 1507 |
+
|
| 1508 |
+
if (!res.ok)
|
| 1509 |
+
throw new Error(
|
| 1510 |
+
"Gemma evaluation failed. Check if local Ollama server is running.",
|
| 1511 |
+
);
|
| 1512 |
+
|
| 1513 |
+
const data = await res.json();
|
| 1514 |
+
clearInterval(statusInterval);
|
| 1515 |
+
|
| 1516 |
+
renderRefereeScorecard(data, labelA, labelB);
|
| 1517 |
+
} catch (err) {
|
| 1518 |
+
clearInterval(statusInterval);
|
| 1519 |
+
console.error(err);
|
| 1520 |
+
domReferee.verdictBox.innerHTML = `
|
| 1521 |
+
<div style="color: var(--superman-red); text-align: center; padding: 1.5rem; border: 1px solid var(--superman-red-glow); border-radius: 6px; background-color: var(--superman-red-muted);">
|
| 1522 |
+
<strong>Referee Error:</strong> ${err.message}
|
| 1523 |
+
</div>
|
| 1524 |
+
`;
|
| 1525 |
+
domReferee.verdictBox.style.display = "block";
|
| 1526 |
+
} finally {
|
| 1527 |
+
domReferee.loadingBox.style.display = "none";
|
| 1528 |
+
}
|
| 1529 |
+
});
|
| 1530 |
+
}
|
| 1531 |
+
|
| 1532 |
+
function renderRefereeScorecard(data, labelA, labelB) {
|
| 1533 |
+
const isA = data.winner === "chunk_a";
|
| 1534 |
+
const isB = data.winner === "chunk_b";
|
| 1535 |
+
const isTie = data.winner === "tie";
|
| 1536 |
+
|
| 1537 |
+
let winnerColor = "var(--superman-yellow)";
|
| 1538 |
+
let winnerBanner = "⚖️ IT'S A TIE";
|
| 1539 |
+
if (isA) {
|
| 1540 |
+
winnerColor = "var(--superman-yellow)";
|
| 1541 |
+
winnerBanner = `🏆 ${labelA.toUpperCase()} WINS`;
|
| 1542 |
+
} else if (isB) {
|
| 1543 |
+
winnerColor = "var(--superman-blue)";
|
| 1544 |
+
winnerBanner = `🏆 ${labelB.toUpperCase()} WINS`;
|
| 1545 |
+
}
|
| 1546 |
+
|
| 1547 |
+
// Dynamically replace blinded chunk labels with actual bolded model names
|
| 1548 |
+
const boldLabelA = `<strong>${labelA}</strong>`;
|
| 1549 |
+
const boldLabelB = `<strong>${labelB}</strong>`;
|
| 1550 |
+
|
| 1551 |
+
let cleanedReason = data.winner_reason.trim();
|
| 1552 |
+
// Strip leading/trailing quote characters
|
| 1553 |
+
cleanedReason = cleanedReason
|
| 1554 |
+
.replace(/^["']/, "")
|
| 1555 |
+
.replace(/["']$/, "")
|
| 1556 |
+
.trim();
|
| 1557 |
+
// Collapse any newlines or multiple whitespace characters to a single space for single-line display
|
| 1558 |
+
cleanedReason = cleanedReason.replace(/\r?\n|\r/g, " ").replace(/\s+/g, " ");
|
| 1559 |
+
|
| 1560 |
+
cleanedReason = cleanedReason
|
| 1561 |
+
.replace(/\b(Chunk A|chunk_a|chunk A)\b/g, boldLabelA)
|
| 1562 |
+
.replace(/\b(Chunk B|chunk_b|chunk B)\b/g, boldLabelB);
|
| 1563 |
+
|
| 1564 |
+
const scoreCardHtml = `
|
| 1565 |
+
<div class="verdict-banner" style="background-color: ${winnerColor}-muted; border-color: ${winnerColor};">
|
| 1566 |
+
<div class="verdict-title" style="color: ${winnerColor}">${winnerBanner}</div>
|
| 1567 |
+
<div class="verdict-confidence">Confidence: ${(data.confidence * 100).toFixed(0)}%</div>
|
| 1568 |
+
</div>
|
| 1569 |
+
|
| 1570 |
+
<div class="verdict-reason">
|
| 1571 |
+
<strong>Verdict Reason:</strong>
|
| 1572 |
+
<p>${cleanedReason}</p>
|
| 1573 |
+
<div class="deciding-factor">Deciding Factor: <code>${data.deciding_dimension}</code></div>
|
| 1574 |
+
</div>
|
| 1575 |
+
|
| 1576 |
+
<!-- Comparative Table of dimensions -->
|
| 1577 |
+
<div class="referee-scores-table">
|
| 1578 |
+
<div class="scores-table-header">
|
| 1579 |
+
<span class="score-col-label">Criterion</span>
|
| 1580 |
+
<span class="score-col-val" style="font-size: 0.65rem; color: var(--superman-yellow);">${labelA}</span>
|
| 1581 |
+
<span class="score-col-bar">Comparison</span>
|
| 1582 |
+
<span class="score-col-val" style="font-size: 0.65rem; color: var(--superman-blue);">${labelB}</span>
|
| 1583 |
+
</div>
|
| 1584 |
+
|
| 1585 |
+
${renderScoreRow("Query Relevance", data.chunk_a_score.query_relevance, data.chunk_b_score.query_relevance)}
|
| 1586 |
+
${renderScoreRow("Completeness", data.chunk_a_score.answer_completeness, data.chunk_b_score.answer_completeness)}
|
| 1587 |
+
${renderScoreRow("Plausibility", data.chunk_a_score.factual_plausibility, data.chunk_b_score.factual_plausibility)}
|
| 1588 |
+
${renderScoreRow("Clarity & Style", data.chunk_a_score.clarity, data.chunk_b_score.clarity)}
|
| 1589 |
+
|
| 1590 |
+
<div class="scores-table-row overall-row">
|
| 1591 |
+
<span class="score-col-label"><strong>Overall Average</strong></span>
|
| 1592 |
+
<span class="score-col-val" style="color: var(--superman-yellow)"><strong>${data.chunk_a_score.overall.toFixed(2)}</strong></span>
|
| 1593 |
+
<span class="score-col-bar">
|
| 1594 |
+
<div class="overall-progress-bar">
|
| 1595 |
+
<div class="overall-val-a" style="width: ${data.chunk_a_score.overall * 10}%"></div>
|
| 1596 |
+
<div class="overall-val-b" style="width: ${data.chunk_b_score.overall * 10}%"></div>
|
| 1597 |
+
</div>
|
| 1598 |
+
</span>
|
| 1599 |
+
<span class="score-col-val" style="color: var(--superman-blue)"><strong>${data.chunk_b_score.overall.toFixed(2)}</strong></span>
|
| 1600 |
+
</div>
|
| 1601 |
+
</div>
|
| 1602 |
+
|
| 1603 |
+
<!-- Strengths & Weaknesses Column -->
|
| 1604 |
+
<div class="verdict-details-grid">
|
| 1605 |
+
<div class="verdict-details-col">
|
| 1606 |
+
<h4>${labelA} Analysis</h4>
|
| 1607 |
+
<div class="verdict-list-title strength-list-title">🟢 Strengths</div>
|
| 1608 |
+
<ul>
|
| 1609 |
+
${data.chunk_a_strengths.map((s) => `<li>${escapeHtml(s)}</li>`).join("")}
|
| 1610 |
+
</ul>
|
| 1611 |
+
<div class="verdict-list-title weakness-list-title">🔴 Weaknesses</div>
|
| 1612 |
+
<ul>
|
| 1613 |
+
${data.chunk_a_weaknesses.map((w) => `<li>${escapeHtml(w)}</li>`).join("")}
|
| 1614 |
+
</ul>
|
| 1615 |
+
</div>
|
| 1616 |
+
|
| 1617 |
+
<div class="verdict-details-col">
|
| 1618 |
+
<h4>${labelB} Analysis</h4>
|
| 1619 |
+
<div class="verdict-list-title strength-list-title">🟢 Strengths</div>
|
| 1620 |
+
<ul>
|
| 1621 |
+
${data.chunk_b_strengths.map((s) => `<li>${escapeHtml(s)}</li>`).join("")}
|
| 1622 |
+
</ul>
|
| 1623 |
+
<div class="verdict-list-title weakness-list-title">🔴 Weaknesses</div>
|
| 1624 |
+
<ul>
|
| 1625 |
+
${data.chunk_b_weaknesses.map((w) => `<li>${escapeHtml(w)}</li>`).join("")}
|
| 1626 |
+
</ul>
|
| 1627 |
+
</div>
|
| 1628 |
+
</div>
|
| 1629 |
+
`;
|
| 1630 |
+
|
| 1631 |
+
domReferee.verdictBox.innerHTML = scoreCardHtml;
|
| 1632 |
+
domReferee.verdictBox.style.display = "block";
|
| 1633 |
+
}
|
| 1634 |
+
|
| 1635 |
+
function renderScoreRow(label, valA, valB) {
|
| 1636 |
+
const pctA = valA * 10;
|
| 1637 |
+
const pctB = valB * 10;
|
| 1638 |
+
|
| 1639 |
+
return `
|
| 1640 |
+
<div class="scores-table-row">
|
| 1641 |
+
<span class="score-col-label">${label}</span>
|
| 1642 |
+
<span class="score-col-val">${valA}/10</span>
|
| 1643 |
+
<span class="score-col-bar">
|
| 1644 |
+
<div class="score-progress-bar">
|
| 1645 |
+
<div class="score-val-a" style="width: ${pctA}%"></div>
|
| 1646 |
+
<div class="score-val-b" style="width: ${pctB}%"></div>
|
| 1647 |
+
</div>
|
| 1648 |
+
</span>
|
| 1649 |
+
<span class="score-col-val">${valB}/10</span>
|
| 1650 |
+
</div>
|
| 1651 |
+
`;
|
| 1652 |
+
}
|
frontend/index.html
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<meta name="description" content="RAG Visualizer — An X-Ray machine for RAG pipelines. Visualize and compare chunking strategies." />
|
| 7 |
+
<title>RAG Visualizer — Chunking Lab</title>
|
| 8 |
+
<link rel="stylesheet" href="/static/styles.css" />
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div class="app-wrapper">
|
| 12 |
+
|
| 13 |
+
<!-- ========== HEADER ========== -->
|
| 14 |
+
<header class="app-header">
|
| 15 |
+
<div class="app-logo">
|
| 16 |
+
<div class="app-logo-icon">🔬</div>
|
| 17 |
+
<div class="app-logo-text">RAG <span>Visualizer</span></div>
|
| 18 |
+
</div>
|
| 19 |
+
</header>
|
| 20 |
+
|
| 21 |
+
<!-- ========== MAIN 3-COLUMN LAYOUT ========== -->
|
| 22 |
+
<main class="app-main">
|
| 23 |
+
|
| 24 |
+
<!-- ===== LEFT PANEL: Configuration ===== -->
|
| 25 |
+
<aside class="panel" id="config-panel">
|
| 26 |
+
<div class="panel-header">
|
| 27 |
+
<h2><span class="icon">⚙️</span> Configuration</h2>
|
| 28 |
+
</div>
|
| 29 |
+
<div class="panel-body">
|
| 30 |
+
|
| 31 |
+
<!-- Text Input -->
|
| 32 |
+
<div class="config-section">
|
| 33 |
+
<div class="config-section-title">Input Text</div>
|
| 34 |
+
<div class="text-input-wrapper">
|
| 35 |
+
<textarea
|
| 36 |
+
id="text-input"
|
| 37 |
+
class="text-input"
|
| 38 |
+
placeholder="Paste your text here to visualize chunking strategies..."
|
| 39 |
+
spellcheck="false"
|
| 40 |
+
></textarea>
|
| 41 |
+
<span class="char-count" id="char-count">0 chars</span>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
<!-- Strategy Selector -->
|
| 46 |
+
<div class="config-section">
|
| 47 |
+
<div class="config-section-title">Chunking Strategy</div>
|
| 48 |
+
<div class="strategy-grid" id="strategy-grid">
|
| 49 |
+
<label class="strategy-card active" data-strategy="fixed_size">
|
| 50 |
+
<input type="radio" name="strategy" value="fixed_size" checked />
|
| 51 |
+
<div class="strategy-card-icon">📏</div>
|
| 52 |
+
<div class="strategy-card-name">Fixed Size</div>
|
| 53 |
+
<div class="strategy-card-desc">Cut every N tokens</div>
|
| 54 |
+
</label>
|
| 55 |
+
<label class="strategy-card" data-strategy="sentence">
|
| 56 |
+
<input type="radio" name="strategy" value="sentence" />
|
| 57 |
+
<div class="strategy-card-icon">💬</div>
|
| 58 |
+
<div class="strategy-card-name">Sentence</div>
|
| 59 |
+
<div class="strategy-card-desc">Split on boundaries</div>
|
| 60 |
+
</label>
|
| 61 |
+
<label class="strategy-card" data-strategy="recursive">
|
| 62 |
+
<input type="radio" name="strategy" value="recursive" />
|
| 63 |
+
<div class="strategy-card-icon">🔄</div>
|
| 64 |
+
<div class="strategy-card-name">Recursive</div>
|
| 65 |
+
<div class="strategy-card-desc">Hierarchy of separators</div>
|
| 66 |
+
</label>
|
| 67 |
+
<label class="strategy-card" data-strategy="parent_child">
|
| 68 |
+
<input type="radio" name="strategy" value="parent_child" />
|
| 69 |
+
<div class="strategy-card-icon">🌳</div>
|
| 70 |
+
<div class="strategy-card-name">Parent-Child</div>
|
| 71 |
+
<div class="strategy-card-desc">Nested chunking</div>
|
| 72 |
+
</label>
|
| 73 |
+
<label class="strategy-card" data-strategy="semantic">
|
| 74 |
+
<input type="radio" name="strategy" value="semantic" />
|
| 75 |
+
<div class="strategy-card-icon">🧠</div>
|
| 76 |
+
<div class="strategy-card-name">Semantic</div>
|
| 77 |
+
<div class="strategy-card-desc">Split on topic shifts</div>
|
| 78 |
+
</label>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
|
| 82 |
+
<!-- Standard Config Sliders -->
|
| 83 |
+
<div class="config-section" id="standard-config">
|
| 84 |
+
<div class="config-section-title">Parameters</div>
|
| 85 |
+
<div class="config-field">
|
| 86 |
+
<label>
|
| 87 |
+
Chunk Size (tokens)
|
| 88 |
+
<span class="value-badge" id="chunk-size-value">500</span>
|
| 89 |
+
</label>
|
| 90 |
+
<input type="range" id="chunk-size" min="50" max="2000" step="50" value="500" />
|
| 91 |
+
</div>
|
| 92 |
+
<div class="config-field">
|
| 93 |
+
<label>
|
| 94 |
+
Overlap (tokens)
|
| 95 |
+
<span class="value-badge" id="overlap-value">20</span>
|
| 96 |
+
</label>
|
| 97 |
+
<input type="range" id="overlap" min="0" max="500" step="10" value="20" />
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
<!-- Parent-Child Config (Hidden by default) -->
|
| 102 |
+
<div class="parent-child-config" id="parent-child-config">
|
| 103 |
+
<div class="pc-section-label parent">🔵 Parent Config</div>
|
| 104 |
+
<div class="config-field">
|
| 105 |
+
<label>
|
| 106 |
+
Parent Chunk Size
|
| 107 |
+
<span class="value-badge" id="parent-size-value">1000</span>
|
| 108 |
+
</label>
|
| 109 |
+
<input type="range" id="parent-size" min="100" max="4000" step="100" value="1000" />
|
| 110 |
+
</div>
|
| 111 |
+
<div class="config-field">
|
| 112 |
+
<label>
|
| 113 |
+
Parent Overlap
|
| 114 |
+
<span class="value-badge" id="parent-overlap-value">100</span>
|
| 115 |
+
</label>
|
| 116 |
+
<input type="range" id="parent-overlap" min="0" max="500" step="10" value="100" />
|
| 117 |
+
</div>
|
| 118 |
+
<div class="pc-section-label child">🟢 Child Config</div>
|
| 119 |
+
<div class="config-field">
|
| 120 |
+
<label>
|
| 121 |
+
Child Chunk Size
|
| 122 |
+
<span class="value-badge" id="child-size-value">200</span>
|
| 123 |
+
</label>
|
| 124 |
+
<input type="range" id="child-size" min="50" max="1000" step="50" value="200" />
|
| 125 |
+
</div>
|
| 126 |
+
<div class="config-field">
|
| 127 |
+
<label>
|
| 128 |
+
Child Overlap
|
| 129 |
+
<span class="value-badge" id="child-overlap-value">20</span>
|
| 130 |
+
</label>
|
| 131 |
+
<input type="range" id="child-overlap" min="0" max="200" step="10" value="20" />
|
| 132 |
+
</div>
|
| 133 |
+
</div>
|
| 134 |
+
|
| 135 |
+
<!-- Semantic Config (Hidden by default) -->
|
| 136 |
+
<div class="config-section" id="semantic-config" style="display: none;">
|
| 137 |
+
<div class="config-section-title">Semantic Settings</div>
|
| 138 |
+
<div class="config-field">
|
| 139 |
+
<label>
|
| 140 |
+
Similarity Threshold
|
| 141 |
+
<span class="value-badge" id="semantic-threshold-value">0.50</span>
|
| 142 |
+
</label>
|
| 143 |
+
<input type="range" id="semantic-threshold" min="0.1" max="1.0" step="0.05" value="0.5" />
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
|
| 147 |
+
<!-- Separators (for recursive) -->
|
| 148 |
+
<div class="config-section" id="separators-section" style="display: none;">
|
| 149 |
+
<div class="config-section-title">Separators</div>
|
| 150 |
+
<div class="separator-tags" id="separator-tags">
|
| 151 |
+
<span class="separator-tag" data-sep="\n\n"><code>\\n\\n</code><span class="remove">×</span></span>
|
| 152 |
+
<span class="separator-tag" data-sep="\n"><code>\\n</code><span class="remove">×</span></span>
|
| 153 |
+
<span class="separator-tag" data-sep=". "><code>. </code><span class="remove">×</span></span>
|
| 154 |
+
<span class="separator-tag" data-sep=" "><code>⎵</code><span class="remove">×</span></span>
|
| 155 |
+
<input type="text" class="separator-input" id="separator-input" placeholder="Add..." />
|
| 156 |
+
</div>
|
| 157 |
+
</div>
|
| 158 |
+
|
| 159 |
+
<!-- Embedding & Vector Space Config -->
|
| 160 |
+
<div class="config-section">
|
| 161 |
+
<div class="config-section-title">Vector Space Settings</div>
|
| 162 |
+
|
| 163 |
+
<div class="config-field">
|
| 164 |
+
<label for="embedding-model">Embedding Model</label>
|
| 165 |
+
<select id="embedding-model" class="select-input">
|
| 166 |
+
<option value="nomic-embed-text" selected>Nomic Embed Text</option>
|
| 167 |
+
<option value="EmbeddingGemma">Embedding Gemma</option>
|
| 168 |
+
<option value="qwen3-embedding:0.6b">Qwen3 Embedding</option>
|
| 169 |
+
</select>
|
| 170 |
+
</div>
|
| 171 |
+
|
| 172 |
+
<div class="config-field">
|
| 173 |
+
<label>
|
| 174 |
+
UMAP n_neighbors
|
| 175 |
+
<span class="value-badge" id="n-neighbors-value">15</span>
|
| 176 |
+
</label>
|
| 177 |
+
<input type="range" id="n-neighbors" min="2" max="100" step="1" value="15" />
|
| 178 |
+
</div>
|
| 179 |
+
|
| 180 |
+
<div class="config-field">
|
| 181 |
+
<label>
|
| 182 |
+
UMAP min_dist
|
| 183 |
+
<span class="value-badge" id="min-dist-value">0.10</span>
|
| 184 |
+
</label>
|
| 185 |
+
<input type="range" id="min-dist" min="0.01" max="1.0" step="0.05" value="0.1" />
|
| 186 |
+
</div>
|
| 187 |
+
</div>
|
| 188 |
+
|
| 189 |
+
<!-- Run Button -->
|
| 190 |
+
<button class="btn-run ready" id="btn-run">
|
| 191 |
+
<span class="spinner"></span>
|
| 192 |
+
<span class="btn-text">⚡ Run Chunking</span>
|
| 193 |
+
</button>
|
| 194 |
+
|
| 195 |
+
</div>
|
| 196 |
+
</aside>
|
| 197 |
+
|
| 198 |
+
<!-- ===== CENTER PANEL: X-Ray Text Viewer & Vector Space ===== -->
|
| 199 |
+
<section class="panel xray-viewer" id="xray-panel">
|
| 200 |
+
<div class="panel-header tabs-header">
|
| 201 |
+
<div class="tab-buttons">
|
| 202 |
+
<button class="tab-btn active" data-tab="xray-tab">
|
| 203 |
+
<span class="icon">🔍</span> Document Viewer
|
| 204 |
+
</button>
|
| 205 |
+
<button class="tab-btn" data-tab="vector-tab">
|
| 206 |
+
<span class="icon">🌌</span> Vector Space 2D
|
| 207 |
+
</button>
|
| 208 |
+
</div>
|
| 209 |
+
</div>
|
| 210 |
+
|
| 211 |
+
<!-- Tab Content 1: X-Ray Text Viewer -->
|
| 212 |
+
<div class="panel-body tab-content active" id="xray-tab">
|
| 213 |
+
<div class="empty-state" id="xray-empty">
|
| 214 |
+
<div class="empty-state-icon">🔬</div>
|
| 215 |
+
<div class="empty-state-title">Ready to Analyze</div>
|
| 216 |
+
<div class="empty-state-desc">
|
| 217 |
+
Paste your text, choose a chunking strategy, and hit <strong>Run</strong> to see how your text gets sliced.
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
<div class="xray-text-container" id="xray-text" style="display: none;"></div>
|
| 221 |
+
</div>
|
| 222 |
+
|
| 223 |
+
<!-- Tab Content 2: Vector Space 2D -->
|
| 224 |
+
<div class="panel-body tab-content" id="vector-tab" style="display: none;">
|
| 225 |
+
<div class="vector-space-container">
|
| 226 |
+
<div class="canvas-wrapper">
|
| 227 |
+
<canvas id="vector-canvas"></canvas>
|
| 228 |
+
<!-- Floating Tooltip inside canvas -->
|
| 229 |
+
<div id="vector-tooltip" class="vector-tooltip" style="display: none;"></div>
|
| 230 |
+
|
| 231 |
+
<!-- Help overlay for canvas controls -->
|
| 232 |
+
<div class="canvas-help-overlay">
|
| 233 |
+
<span>🖱️ Drag to Pan</span>
|
| 234 |
+
<span>☸️ Scroll to Zoom</span>
|
| 235 |
+
<span>✨ Click node to select</span>
|
| 236 |
+
</div>
|
| 237 |
+
</div>
|
| 238 |
+
|
| 239 |
+
<!-- Sonar Query Simulator Section -->
|
| 240 |
+
<div class="query-simulator-section">
|
| 241 |
+
<div class="query-section-title" style="display: flex; justify-content: space-between; align-items: center;">
|
| 242 |
+
<span>🔮 Sonar Query Simulator (RAG Retrieval)</span>
|
| 243 |
+
<button id="btn-open-arena" class="btn-sm" style="background: var(--superman-red); color: white; border: none; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-weight: bold; font-family: var(--font-sans);">⚔️ Arena Comparison</button>
|
| 244 |
+
</div>
|
| 245 |
+
<div class="query-input-wrapper">
|
| 246 |
+
<input type="text" id="query-input" placeholder="Type a concept query to fetch semantically similar chunks (e.g. 'linear regression')..." />
|
| 247 |
+
<button id="btn-query" class="btn-query">🔍 Query</button>
|
| 248 |
+
</div>
|
| 249 |
+
<div class="query-results-drawer" id="query-results-drawer" style="display: none;">
|
| 250 |
+
<div class="drawer-header">
|
| 251 |
+
<h3>Retrieved Context</h3>
|
| 252 |
+
<span class="close-drawer" id="close-drawer">×</span>
|
| 253 |
+
</div>
|
| 254 |
+
<div class="query-results-list" id="query-results-list">
|
| 255 |
+
<!-- Retrieved chunks will go here -->
|
| 256 |
+
</div>
|
| 257 |
+
</div>
|
| 258 |
+
</div>
|
| 259 |
+
</div>
|
| 260 |
+
</div>
|
| 261 |
+
</section>
|
| 262 |
+
|
| 263 |
+
<!-- ===== RIGHT PANEL: Chunk Inspector ===== -->
|
| 264 |
+
<aside class="panel" id="inspector-panel">
|
| 265 |
+
<div class="panel-header">
|
| 266 |
+
<h2><span class="icon">📊</span> Chunk Inspector</h2>
|
| 267 |
+
</div>
|
| 268 |
+
<div class="panel-body">
|
| 269 |
+
<!-- Stats -->
|
| 270 |
+
<div class="stats-grid" id="stats-grid">
|
| 271 |
+
<div class="stat-card">
|
| 272 |
+
<div class="stat-value" id="stat-total">—</div>
|
| 273 |
+
<div class="stat-label">Total Chunks</div>
|
| 274 |
+
</div>
|
| 275 |
+
<div class="stat-card">
|
| 276 |
+
<div class="stat-value" id="stat-avg-tokens">—</div>
|
| 277 |
+
<div class="stat-label">Avg Tokens</div>
|
| 278 |
+
</div>
|
| 279 |
+
<div class="stat-card">
|
| 280 |
+
<div class="stat-value" id="stat-total-tokens">—</div>
|
| 281 |
+
<div class="stat-label">Total Tokens</div>
|
| 282 |
+
</div>
|
| 283 |
+
<div class="stat-card">
|
| 284 |
+
<div class="stat-value" id="stat-strategy">—</div>
|
| 285 |
+
<div class="stat-label">Strategy</div>
|
| 286 |
+
</div>
|
| 287 |
+
</div>
|
| 288 |
+
|
| 289 |
+
<!-- Chunk List -->
|
| 290 |
+
<div class="config-section-title">Chunks</div>
|
| 291 |
+
<div class="chunk-list" id="chunk-list">
|
| 292 |
+
<div class="empty-state" style="min-height: 200px;">
|
| 293 |
+
<div class="empty-state-desc">Run chunking to see results here</div>
|
| 294 |
+
</div>
|
| 295 |
+
</div>
|
| 296 |
+
</div>
|
| 297 |
+
</aside>
|
| 298 |
+
|
| 299 |
+
<!-- ===== THE ARENA MODAL ===== -->
|
| 300 |
+
<div id="arena-modal" class="arena-modal-overlay" style="display: none;">
|
| 301 |
+
<div class="arena-modal-content">
|
| 302 |
+
<div class="arena-header">
|
| 303 |
+
<h2>⚔️ The Grand Comparison Arena</h2>
|
| 304 |
+
<button id="btn-close-arena" class="btn-close-arena">×</button>
|
| 305 |
+
</div>
|
| 306 |
+
|
| 307 |
+
<div class="arena-query-bar">
|
| 308 |
+
<input type="text" id="arena-query-input" placeholder="Type a query to compare configurations (e.g., 'What is Clark Kent's weakness?')..." />
|
| 309 |
+
<button class="btn-run" id="btn-arena-fight" style="width: 150px; flex-shrink: 0;">🔥 FIGHT!</button>
|
| 310 |
+
</div>
|
| 311 |
+
|
| 312 |
+
<div class="arena-body">
|
| 313 |
+
<div class="arena-columns">
|
| 314 |
+
<!-- COLUMN A -->
|
| 315 |
+
<div class="arena-col">
|
| 316 |
+
<div class="arena-col-header">
|
| 317 |
+
<h3>Corner A</h3>
|
| 318 |
+
<div class="arena-config-row">
|
| 319 |
+
<select id="arena-model-a" class="select-input">
|
| 320 |
+
<option value="nomic-embed-text">Nomic Embed</option>
|
| 321 |
+
<option value="EmbeddingGemma">Gemma Embed</option>
|
| 322 |
+
<option value="qwen3-embedding:0.6b">Qwen3 Embed</option>
|
| 323 |
+
</select>
|
| 324 |
+
<select id="arena-strategy-a" class="select-input">
|
| 325 |
+
<option value="fixed_size">Fixed Size</option>
|
| 326 |
+
<option value="sentence">Sentence</option>
|
| 327 |
+
<option value="recursive">Recursive</option>
|
| 328 |
+
<option value="semantic">Semantic</option>
|
| 329 |
+
</select>
|
| 330 |
+
</div>
|
| 331 |
+
</div>
|
| 332 |
+
<div class="arena-results-list" id="arena-results-a">
|
| 333 |
+
<div style="text-align: center; color: var(--text-tertiary); padding: 2rem;">Awaiting challengers...</div>
|
| 334 |
+
</div>
|
| 335 |
+
</div>
|
| 336 |
+
|
| 337 |
+
<!-- COLUMN B -->
|
| 338 |
+
<div class="arena-col">
|
| 339 |
+
<div class="arena-col-header" style="border-bottom-color: var(--info-color);">
|
| 340 |
+
<h3>Corner B</h3>
|
| 341 |
+
<div class="arena-config-row">
|
| 342 |
+
<select id="arena-model-b" class="select-input">
|
| 343 |
+
<option value="nomic-embed-text">Nomic Embed</option>
|
| 344 |
+
<option value="EmbeddingGemma">Gemma Embed</option>
|
| 345 |
+
<option value="qwen3-embedding:0.6b">Qwen3 Embed</option>
|
| 346 |
+
</select>
|
| 347 |
+
<select id="arena-strategy-b" class="select-input">
|
| 348 |
+
<option value="fixed_size">Fixed Size</option>
|
| 349 |
+
<option value="sentence">Sentence</option>
|
| 350 |
+
<option value="recursive">Recursive</option>
|
| 351 |
+
<option value="semantic">Semantic</option>
|
| 352 |
+
</select>
|
| 353 |
+
</div>
|
| 354 |
+
</div>
|
| 355 |
+
<div class="arena-results-list" id="arena-results-b">
|
| 356 |
+
<div style="text-align: center; color: var(--text-tertiary); padding: 2rem;">Awaiting challengers...</div>
|
| 357 |
+
</div>
|
| 358 |
+
</div>
|
| 359 |
+
</div>
|
| 360 |
+
|
| 361 |
+
<!-- THE AI REFEREE PANEL (TASK 5.1) -->
|
| 362 |
+
<div class="arena-referee-panel" id="arena-referee-panel" style="display: none;">
|
| 363 |
+
<div class="referee-divider">
|
| 364 |
+
<span class="referee-badge">⚖️ THE AI REFEREE</span>
|
| 365 |
+
</div>
|
| 366 |
+
|
| 367 |
+
<div class="referee-trigger-box" id="referee-trigger-box">
|
| 368 |
+
<p class="referee-prompt-text">Call upon the local Gemma model to analyze, score, and judge both contexts!</p>
|
| 369 |
+
<button class="btn-run" id="btn-call-referee" style="margin: 0 auto; display: block; max-width: 250px;">⚖️ CALL THE REFEREE</button>
|
| 370 |
+
</div>
|
| 371 |
+
|
| 372 |
+
<!-- Loading scanner status -->
|
| 373 |
+
<div class="referee-loading-box" id="referee-loading-box" style="display: none;">
|
| 374 |
+
<div class="skeleton-loader" style="max-width: 400px; margin: 0 auto 1rem; border-radius: 6px;"></div>
|
| 375 |
+
<div class="referee-status-text" id="referee-status-text">Summoning Gemma to review the case...</div>
|
| 376 |
+
</div>
|
| 377 |
+
|
| 378 |
+
<!-- Scorecard Verdict Display -->
|
| 379 |
+
<div class="referee-verdict-box" id="referee-verdict-box" style="display: none;"></div>
|
| 380 |
+
</div>
|
| 381 |
+
</div>
|
| 382 |
+
|
| 383 |
+
</div>
|
| 384 |
+
</div>
|
| 385 |
+
|
| 386 |
+
</main>
|
| 387 |
+
</div>
|
| 388 |
+
|
| 389 |
+
<script src="/static/app.js"></script>
|
| 390 |
+
</body>
|
| 391 |
+
</html>
|
frontend/styles.css
ADDED
|
@@ -0,0 +1,1899 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ============================================================
|
| 2 |
+
RAG Visualizer — Chunking Lab Design System
|
| 3 |
+
Theme: Superman High-Contrast Color-Blocked Light Mode
|
| 4 |
+
Palette: Blue #2563eb | Red #ef4444 | Gold #f59e0b
|
| 5 |
+
============================================================ */
|
| 6 |
+
|
| 7 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Roboto+Condensed:wght@400;700&display=swap');
|
| 8 |
+
|
| 9 |
+
/* --- Design Tokens --- */
|
| 10 |
+
:root {
|
| 11 |
+
/* Surface Colors */
|
| 12 |
+
--bg-root: #f8fafc;
|
| 13 |
+
--bg-surface: #ffffff;
|
| 14 |
+
--bg-surface-raised: #f1f5f9;
|
| 15 |
+
--bg-surface-overlay: #e2e8f0;
|
| 16 |
+
--bg-input: #f8fafc;
|
| 17 |
+
|
| 18 |
+
/* Borders */
|
| 19 |
+
--border-subtle: rgba(15, 23, 42, 0.05);
|
| 20 |
+
--border-default: rgba(15, 23, 42, 0.09);
|
| 21 |
+
--border-active: rgba(37, 99, 235, 0.35);
|
| 22 |
+
|
| 23 |
+
/* Text Colors */
|
| 24 |
+
--text-primary: #0f172a;
|
| 25 |
+
--text-secondary: #475569;
|
| 26 |
+
--text-tertiary: #64748b;
|
| 27 |
+
--text-inverse: #ffffff;
|
| 28 |
+
|
| 29 |
+
/* Superman Blue — Structural (borders, headers, outlines, secondary data) */
|
| 30 |
+
--superman-blue: #2563eb;
|
| 31 |
+
--superman-blue-hover: #1d4ed8;
|
| 32 |
+
--superman-blue-muted: rgba(37, 99, 235, 0.08);
|
| 33 |
+
--superman-blue-glow: rgba(37, 99, 235, 0.2);
|
| 34 |
+
|
| 35 |
+
/* Superman Red — Action/Attention (CTA buttons, active states, query nodes) */
|
| 36 |
+
--superman-red: #ef4444;
|
| 37 |
+
--superman-red-hover: #dc2626;
|
| 38 |
+
--superman-red-muted: rgba(239, 68, 68, 0.08);
|
| 39 |
+
--superman-red-glow: rgba(239, 68, 68, 0.2);
|
| 40 |
+
|
| 41 |
+
/* Superman Gold — Interactive/Highlight (slider thumbs, Rank 1, connecting lines) */
|
| 42 |
+
--superman-yellow: #f59e0b;
|
| 43 |
+
--superman-yellow-hover: #d97706;
|
| 44 |
+
--superman-yellow-muted: rgba(245, 158, 11, 0.08);
|
| 45 |
+
--superman-yellow-glow: rgba(245, 158, 11, 0.2);
|
| 46 |
+
|
| 47 |
+
/* Functional Accent — Default to Blue for structural context */
|
| 48 |
+
--accent: var(--superman-blue);
|
| 49 |
+
--accent-hover: var(--superman-blue-hover);
|
| 50 |
+
--accent-muted: var(--superman-blue-muted);
|
| 51 |
+
--accent-glow: var(--superman-blue-glow);
|
| 52 |
+
|
| 53 |
+
/* Chunk Highlight Colors */
|
| 54 |
+
--chunk-color-1: var(--superman-blue);
|
| 55 |
+
--chunk-color-2: var(--superman-red);
|
| 56 |
+
--chunk-color-3: var(--superman-yellow);
|
| 57 |
+
--chunk-color-4: #818cf8;
|
| 58 |
+
|
| 59 |
+
/* Semantic */
|
| 60 |
+
--success: #16a34a;
|
| 61 |
+
--warning: #d97706;
|
| 62 |
+
--error: #dc2626;
|
| 63 |
+
--info: #2563eb;
|
| 64 |
+
|
| 65 |
+
/* Spacing */
|
| 66 |
+
--space-xs: 4px;
|
| 67 |
+
--space-sm: 8px;
|
| 68 |
+
--space-md: 16px;
|
| 69 |
+
--space-lg: 24px;
|
| 70 |
+
--space-xl: 32px;
|
| 71 |
+
--space-2xl: 48px;
|
| 72 |
+
|
| 73 |
+
/* Radius */
|
| 74 |
+
--radius-sm: 6px;
|
| 75 |
+
--radius-md: 10px;
|
| 76 |
+
--radius-lg: 16px;
|
| 77 |
+
--radius-xl: 20px;
|
| 78 |
+
|
| 79 |
+
/* Shadows */
|
| 80 |
+
--shadow-sm: 0 1px 3px rgba(15, 23, 42, 0.05);
|
| 81 |
+
--shadow-md: 0 4px 16px rgba(15, 23, 42, 0.06);
|
| 82 |
+
--shadow-lg: 0 10px 36px rgba(15, 23, 42, 0.08);
|
| 83 |
+
--shadow-glow-red: 0 0 20px var(--superman-red-glow);
|
| 84 |
+
--shadow-glow-blue: 0 0 20px var(--superman-blue-glow);
|
| 85 |
+
--shadow-glow-gold: 0 0 20px var(--superman-yellow-glow);
|
| 86 |
+
|
| 87 |
+
/* Transitions */
|
| 88 |
+
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
| 89 |
+
--transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
| 90 |
+
--transition-slow: 400ms cubic-bezier(0.4, 0, 0.2, 1);
|
| 91 |
+
|
| 92 |
+
/* Typography */
|
| 93 |
+
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 94 |
+
--font-heading: 'Roboto Condensed', 'Inter', sans-serif;
|
| 95 |
+
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
/* --- Kryptonite Protocol (Error States) --- */
|
| 99 |
+
.text-kryptonite {
|
| 100 |
+
color: #22c55e !important;
|
| 101 |
+
}
|
| 102 |
+
.bg-kryptonite-wash {
|
| 103 |
+
background-color: rgba(34, 197, 94, 0.1) !important;
|
| 104 |
+
border: 1px solid #22c55e !important;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/* --- Reset & Base --- */
|
| 108 |
+
*,
|
| 109 |
+
*::before,
|
| 110 |
+
*::after {
|
| 111 |
+
margin: 0;
|
| 112 |
+
padding: 0;
|
| 113 |
+
box-sizing: border-box;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
html {
|
| 117 |
+
font-size: 14px;
|
| 118 |
+
-webkit-font-smoothing: antialiased;
|
| 119 |
+
-moz-osx-font-smoothing: grayscale;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
body {
|
| 123 |
+
font-family: var(--font-sans);
|
| 124 |
+
background: var(--bg-root);
|
| 125 |
+
color: var(--text-primary);
|
| 126 |
+
line-height: 1.6;
|
| 127 |
+
min-height: 100vh;
|
| 128 |
+
overflow-x: hidden;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
/* Subtle dot grid texture */
|
| 132 |
+
body::before {
|
| 133 |
+
content: '';
|
| 134 |
+
position: fixed;
|
| 135 |
+
inset: 0;
|
| 136 |
+
background-image:
|
| 137 |
+
radial-gradient(circle at 1px 1px, rgba(148, 163, 184, 0.03) 1px, transparent 0);
|
| 138 |
+
background-size: 32px 32px;
|
| 139 |
+
pointer-events: none;
|
| 140 |
+
z-index: 0;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
/* --- Scrollbar --- */
|
| 144 |
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
| 145 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 146 |
+
::-webkit-scrollbar-thumb { background: var(--text-tertiary); border-radius: 99px; }
|
| 147 |
+
::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); }
|
| 148 |
+
|
| 149 |
+
/* --- Layout --- */
|
| 150 |
+
.app-wrapper {
|
| 151 |
+
position: relative;
|
| 152 |
+
z-index: 1;
|
| 153 |
+
display: flex;
|
| 154 |
+
flex-direction: column;
|
| 155 |
+
min-height: 100vh;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
/* ============================================================
|
| 159 |
+
1. HEADER — Blue-to-Red gradient logo, frosted glass bar
|
| 160 |
+
============================================================ */
|
| 161 |
+
.app-header {
|
| 162 |
+
display: flex;
|
| 163 |
+
align-items: center;
|
| 164 |
+
justify-content: space-between;
|
| 165 |
+
padding: var(--space-md) var(--space-xl);
|
| 166 |
+
border-bottom: 1px solid var(--border-default);
|
| 167 |
+
background: rgba(255, 255, 255, 0.85);
|
| 168 |
+
backdrop-filter: blur(16px);
|
| 169 |
+
-webkit-backdrop-filter: blur(16px);
|
| 170 |
+
position: sticky;
|
| 171 |
+
top: 0;
|
| 172 |
+
z-index: 100;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.app-logo {
|
| 176 |
+
display: flex;
|
| 177 |
+
align-items: center;
|
| 178 |
+
gap: var(--space-sm);
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
/* Logo icon: Blue-to-Red gradient */
|
| 182 |
+
.app-logo-icon {
|
| 183 |
+
width: 32px;
|
| 184 |
+
height: 32px;
|
| 185 |
+
background: linear-gradient(90deg, #2563eb 0%, #ef4444 100%);
|
| 186 |
+
border-radius: var(--radius-sm);
|
| 187 |
+
display: grid;
|
| 188 |
+
place-items: center;
|
| 189 |
+
font-size: 16px;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.app-logo-text {
|
| 193 |
+
font-weight: 700;
|
| 194 |
+
font-size: 1.15rem;
|
| 195 |
+
letter-spacing: -0.02em;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
/* "Visualizer" span in Blue */
|
| 199 |
+
.app-logo-text span {
|
| 200 |
+
color: var(--superman-blue);
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.app-header-badge {
|
| 204 |
+
font-size: 0.75rem;
|
| 205 |
+
font-weight: 500;
|
| 206 |
+
color: var(--superman-blue);
|
| 207 |
+
background: var(--superman-blue-muted);
|
| 208 |
+
padding: 2px 10px;
|
| 209 |
+
border-radius: 99px;
|
| 210 |
+
border: 1px solid rgba(37, 99, 235, 0.2);
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
/* ============================================================
|
| 214 |
+
2. MAIN CONTENT — 3 Column Grid
|
| 215 |
+
============================================================ */
|
| 216 |
+
.app-main {
|
| 217 |
+
display: grid;
|
| 218 |
+
grid-template-columns: 300px 1fr 340px;
|
| 219 |
+
gap: 0;
|
| 220 |
+
flex: 1;
|
| 221 |
+
overflow: hidden;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
/* --- Panels --- */
|
| 225 |
+
.panel {
|
| 226 |
+
display: flex;
|
| 227 |
+
flex-direction: column;
|
| 228 |
+
border-right: 1px solid var(--border-default);
|
| 229 |
+
overflow-y: auto;
|
| 230 |
+
height: calc(100vh - 57px);
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
.panel:last-child {
|
| 234 |
+
border-right: none;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.panel-header {
|
| 238 |
+
padding: var(--space-md) var(--space-lg);
|
| 239 |
+
border-bottom: 1px solid var(--border-subtle);
|
| 240 |
+
background: var(--bg-surface);
|
| 241 |
+
position: sticky;
|
| 242 |
+
top: 0;
|
| 243 |
+
z-index: 10;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.panel-header h2 {
|
| 247 |
+
font-family: var(--font-heading);
|
| 248 |
+
font-size: 1rem;
|
| 249 |
+
font-weight: 700;
|
| 250 |
+
text-transform: uppercase;
|
| 251 |
+
letter-spacing: 0.08em;
|
| 252 |
+
color: var(--text-secondary);
|
| 253 |
+
display: flex;
|
| 254 |
+
align-items: center;
|
| 255 |
+
gap: var(--space-sm);
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.panel-header h2 .icon {
|
| 259 |
+
font-size: 1rem;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
/* Sidebar Panel Header Overrides */
|
| 263 |
+
#config-panel .panel-header,
|
| 264 |
+
#inspector-panel .panel-header {
|
| 265 |
+
border-bottom: none;
|
| 266 |
+
background: transparent;
|
| 267 |
+
padding-bottom: var(--space-xs);
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
#config-panel .panel-header h2,
|
| 271 |
+
#inspector-panel .panel-header h2 {
|
| 272 |
+
font-family: var(--font-heading);
|
| 273 |
+
font-size: 1.15rem;
|
| 274 |
+
font-weight: 700;
|
| 275 |
+
text-transform: none;
|
| 276 |
+
letter-spacing: normal;
|
| 277 |
+
color: var(--text-primary);
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
.panel-body {
|
| 281 |
+
padding: var(--space-lg);
|
| 282 |
+
flex: 1;
|
| 283 |
+
overflow-y: auto;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
/* ============================================================
|
| 287 |
+
3. CONFIG PANEL (Left Sidebar)
|
| 288 |
+
============================================================ */
|
| 289 |
+
.config-section {
|
| 290 |
+
margin-bottom: var(--space-xl);
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
.config-section-title {
|
| 294 |
+
font-family: var(--font-heading);
|
| 295 |
+
font-size: 0.8rem;
|
| 296 |
+
font-weight: 700;
|
| 297 |
+
text-transform: uppercase;
|
| 298 |
+
letter-spacing: 0.1em;
|
| 299 |
+
color: var(--text-tertiary);
|
| 300 |
+
margin-bottom: var(--space-md);
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
/* --- Strategy Selector Cards --- */
|
| 304 |
+
.strategy-grid {
|
| 305 |
+
display: grid;
|
| 306 |
+
grid-template-columns: 1fr 1fr;
|
| 307 |
+
gap: var(--space-sm);
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.strategy-card {
|
| 311 |
+
position: relative;
|
| 312 |
+
padding: var(--space-md);
|
| 313 |
+
background-color: #ffffff;
|
| 314 |
+
border: 1px solid #e5e7eb;
|
| 315 |
+
border-top: 4px solid var(--superman-blue);
|
| 316 |
+
border-radius: 10px;
|
| 317 |
+
cursor: pointer;
|
| 318 |
+
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease, background 0.2s ease;
|
| 319 |
+
text-align: center;
|
| 320 |
+
overflow: hidden;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
.strategy-card:hover {
|
| 324 |
+
transform: translateY(-2px);
|
| 325 |
+
box-shadow: 0 10px 15px -3px rgba(37, 99, 235, 0.15);
|
| 326 |
+
border-color: var(--superman-blue);
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
/* Active strategy card: White background, thicker blue border, corner badge */
|
| 330 |
+
.strategy-card.active {
|
| 331 |
+
border: 2px solid var(--superman-blue);
|
| 332 |
+
border-top: 4px solid var(--superman-blue);
|
| 333 |
+
background-color: #ffffff;
|
| 334 |
+
box-shadow: 0 10px 15px -3px rgba(37, 99, 235, 0.25), var(--shadow-glow-blue);
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
/* Corner badge for active strategy */
|
| 338 |
+
.strategy-card.active::after {
|
| 339 |
+
content: '✓';
|
| 340 |
+
position: absolute;
|
| 341 |
+
top: 0;
|
| 342 |
+
right: 0;
|
| 343 |
+
width: 24px;
|
| 344 |
+
height: 24px;
|
| 345 |
+
background-color: var(--superman-red);
|
| 346 |
+
color: #ffffff;
|
| 347 |
+
font-size: 12px;
|
| 348 |
+
font-weight: bold;
|
| 349 |
+
display: flex;
|
| 350 |
+
align-items: center;
|
| 351 |
+
justify-content: center;
|
| 352 |
+
border-bottom-left-radius: 4px;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
.strategy-card input[type="radio"] {
|
| 356 |
+
display: none;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
.strategy-card-icon {
|
| 360 |
+
font-size: 1.4rem;
|
| 361 |
+
margin-bottom: var(--space-xs);
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
.strategy-card-name {
|
| 365 |
+
font-size: 0.78rem;
|
| 366 |
+
font-weight: 600;
|
| 367 |
+
color: var(--text-primary);
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
.strategy-card-desc {
|
| 371 |
+
font-size: 0.65rem;
|
| 372 |
+
color: var(--text-tertiary);
|
| 373 |
+
margin-top: 2px;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
/* --- Sliders (Blue track, Gold thumb) --- */
|
| 377 |
+
.config-field {
|
| 378 |
+
margin-bottom: var(--space-lg);
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.config-field label {
|
| 382 |
+
display: flex;
|
| 383 |
+
justify-content: space-between;
|
| 384 |
+
align-items: center;
|
| 385 |
+
font-family: var(--font-heading);
|
| 386 |
+
font-size: 0.85rem;
|
| 387 |
+
font-weight: 700;
|
| 388 |
+
color: var(--text-secondary);
|
| 389 |
+
margin-bottom: var(--space-sm);
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
/* Value badges in Blue */
|
| 393 |
+
.config-field .value-badge {
|
| 394 |
+
font-family: var(--font-mono);
|
| 395 |
+
font-size: 0.72rem;
|
| 396 |
+
font-weight: 500;
|
| 397 |
+
color: var(--superman-blue);
|
| 398 |
+
background: var(--superman-blue-muted);
|
| 399 |
+
padding: 1px 8px;
|
| 400 |
+
border-radius: 99px;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
/* Slider track: thin Blue */
|
| 404 |
+
input[type="range"] {
|
| 405 |
+
-webkit-appearance: none;
|
| 406 |
+
appearance: none;
|
| 407 |
+
width: 100%;
|
| 408 |
+
height: 4px;
|
| 409 |
+
background: linear-gradient(90deg, var(--superman-blue-muted) 0%, rgba(37, 99, 235, 0.15) 100%);
|
| 410 |
+
border-radius: 99px;
|
| 411 |
+
outline: none;
|
| 412 |
+
cursor: pointer;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
/* Slider thumb: Superman Gold */
|
| 416 |
+
input[type="range"]::-webkit-slider-thumb {
|
| 417 |
+
-webkit-appearance: none;
|
| 418 |
+
appearance: none;
|
| 419 |
+
width: 16px;
|
| 420 |
+
height: 16px;
|
| 421 |
+
border-radius: 50%;
|
| 422 |
+
background: var(--superman-yellow);
|
| 423 |
+
border: 2px solid var(--bg-surface);
|
| 424 |
+
box-shadow: 0 0 8px var(--superman-yellow-glow);
|
| 425 |
+
cursor: pointer;
|
| 426 |
+
transition: transform var(--transition-fast);
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
input[type="range"]::-webkit-slider-thumb:hover {
|
| 430 |
+
transform: scale(1.2);
|
| 431 |
+
box-shadow: 0 0 12px var(--superman-yellow-glow);
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
input[type="range"]::-moz-range-thumb {
|
| 435 |
+
width: 16px;
|
| 436 |
+
height: 16px;
|
| 437 |
+
border-radius: 50%;
|
| 438 |
+
background: var(--superman-yellow);
|
| 439 |
+
border: 2px solid var(--bg-surface);
|
| 440 |
+
box-shadow: 0 0 8px var(--superman-yellow-glow);
|
| 441 |
+
cursor: pointer;
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
/* --- Text Input --- */
|
| 445 |
+
.text-input-wrapper {
|
| 446 |
+
position: relative;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
.text-input {
|
| 450 |
+
width: 100%;
|
| 451 |
+
min-height: 120px;
|
| 452 |
+
padding: var(--space-md);
|
| 453 |
+
background: var(--bg-input);
|
| 454 |
+
border: 1px solid var(--border-default);
|
| 455 |
+
border-radius: var(--radius-md);
|
| 456 |
+
color: var(--text-primary);
|
| 457 |
+
font-family: var(--font-mono);
|
| 458 |
+
font-size: 0.78rem;
|
| 459 |
+
line-height: 1.7;
|
| 460 |
+
resize: vertical;
|
| 461 |
+
outline: none;
|
| 462 |
+
transition: border-color var(--transition-base), box-shadow var(--transition-base);
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
.text-input:focus {
|
| 466 |
+
border-color: var(--superman-blue);
|
| 467 |
+
box-shadow: 0 0 0 3px var(--superman-blue-muted);
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.text-input::placeholder {
|
| 471 |
+
color: var(--text-tertiary);
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
.char-count {
|
| 475 |
+
position: absolute;
|
| 476 |
+
bottom: var(--space-sm);
|
| 477 |
+
right: var(--space-md);
|
| 478 |
+
font-size: 0.65rem;
|
| 479 |
+
font-family: var(--font-mono);
|
| 480 |
+
color: var(--text-tertiary);
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
/* --- Separators Input --- */
|
| 484 |
+
.separator-tags {
|
| 485 |
+
display: flex;
|
| 486 |
+
flex-wrap: wrap;
|
| 487 |
+
gap: var(--space-xs);
|
| 488 |
+
padding: var(--space-sm);
|
| 489 |
+
background: var(--bg-input);
|
| 490 |
+
border: 1px solid var(--border-default);
|
| 491 |
+
border-radius: var(--radius-md);
|
| 492 |
+
min-height: 36px;
|
| 493 |
+
cursor: text;
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
.separator-tag {
|
| 497 |
+
display: inline-flex;
|
| 498 |
+
align-items: center;
|
| 499 |
+
gap: 4px;
|
| 500 |
+
padding: 2px 8px;
|
| 501 |
+
background: var(--bg-surface-overlay);
|
| 502 |
+
border: 1px solid var(--border-default);
|
| 503 |
+
border-radius: var(--radius-sm);
|
| 504 |
+
font-family: var(--font-mono);
|
| 505 |
+
font-size: 0.7rem;
|
| 506 |
+
color: var(--text-secondary);
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
.separator-tag .remove {
|
| 510 |
+
cursor: pointer;
|
| 511 |
+
color: var(--text-tertiary);
|
| 512 |
+
font-size: 0.85rem;
|
| 513 |
+
line-height: 1;
|
| 514 |
+
transition: color var(--transition-fast);
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
.separator-tag .remove:hover {
|
| 518 |
+
color: var(--superman-red);
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
.separator-input {
|
| 522 |
+
border: none;
|
| 523 |
+
outline: none;
|
| 524 |
+
background: transparent;
|
| 525 |
+
color: var(--text-primary);
|
| 526 |
+
font-family: var(--font-mono);
|
| 527 |
+
font-size: 0.72rem;
|
| 528 |
+
flex: 1;
|
| 529 |
+
min-width: 60px;
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
/* ============================================================
|
| 533 |
+
4. RUN BUTTON — Superman Red CTA (primary action)
|
| 534 |
+
============================================================ */
|
| 535 |
+
.btn-run {
|
| 536 |
+
width: 100%;
|
| 537 |
+
padding: var(--space-md) var(--space-lg);
|
| 538 |
+
background: linear-gradient(135deg, var(--superman-red), var(--superman-red-hover));
|
| 539 |
+
color: var(--text-inverse);
|
| 540 |
+
font-family: var(--font-sans);
|
| 541 |
+
font-size: 0.85rem;
|
| 542 |
+
font-weight: 700;
|
| 543 |
+
border: none;
|
| 544 |
+
border-radius: var(--radius-md);
|
| 545 |
+
cursor: pointer;
|
| 546 |
+
transition: all var(--transition-base);
|
| 547 |
+
display: flex;
|
| 548 |
+
align-items: center;
|
| 549 |
+
justify-content: center;
|
| 550 |
+
gap: var(--space-sm);
|
| 551 |
+
text-transform: uppercase;
|
| 552 |
+
letter-spacing: 0.05em;
|
| 553 |
+
position: relative;
|
| 554 |
+
overflow: hidden;
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
.btn-run::before {
|
| 558 |
+
content: '';
|
| 559 |
+
position: absolute;
|
| 560 |
+
inset: 0;
|
| 561 |
+
background: linear-gradient(135deg, transparent, rgba(255, 255, 255, 0.15));
|
| 562 |
+
opacity: 0;
|
| 563 |
+
transition: opacity var(--transition-base);
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
.btn-run:hover::before {
|
| 567 |
+
opacity: 1;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
.btn-run:hover {
|
| 571 |
+
transform: translateY(-1px);
|
| 572 |
+
box-shadow: var(--shadow-glow-red), var(--shadow-md);
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
.btn-run:active {
|
| 576 |
+
transform: translateY(0);
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
.btn-run:disabled {
|
| 580 |
+
opacity: 0.5;
|
| 581 |
+
cursor: not-allowed;
|
| 582 |
+
transform: none;
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
.btn-run:disabled:hover::before {
|
| 586 |
+
opacity: 0;
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.btn-run .spinner {
|
| 590 |
+
width: 16px;
|
| 591 |
+
height: 16px;
|
| 592 |
+
border: 2px solid rgba(255, 255, 255, 0.3);
|
| 593 |
+
border-top-color: var(--text-inverse);
|
| 594 |
+
border-radius: 50%;
|
| 595 |
+
animation: spin 0.6s linear infinite;
|
| 596 |
+
display: none;
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
.btn-run.loading .spinner {
|
| 600 |
+
display: block;
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
.btn-run.loading .btn-text {
|
| 604 |
+
display: none;
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
@keyframes spin {
|
| 608 |
+
to { transform: rotate(360deg); }
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
/* Pulse animation for Run button — Red glow */
|
| 612 |
+
@keyframes pulse-glow-red {
|
| 613 |
+
0%, 100% { box-shadow: 0 0 8px var(--superman-red-glow); }
|
| 614 |
+
50% { box-shadow: 0 0 20px var(--superman-red-glow), 0 0 40px rgba(239, 68, 68, 0.1); }
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
.btn-run.ready {
|
| 618 |
+
animation: pulse-glow-red 2s ease-in-out infinite;
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
/* ============================================================
|
| 622 |
+
5. CENTER PANEL — Tabs (Document Viewer / Vector Space 2D)
|
| 623 |
+
============================================================ */
|
| 624 |
+
.xray-viewer {
|
| 625 |
+
position: relative;
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
/* --- Interactive Tabs Navigation --- */
|
| 629 |
+
.tabs-header {
|
| 630 |
+
padding: 0 !important;
|
| 631 |
+
}
|
| 632 |
+
.tab-buttons {
|
| 633 |
+
display: flex;
|
| 634 |
+
height: 56px;
|
| 635 |
+
width: 100%;
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
/* Tab button: Blue text, no background highlight */
|
| 639 |
+
.tab-btn {
|
| 640 |
+
flex: 1;
|
| 641 |
+
background: transparent;
|
| 642 |
+
border: none;
|
| 643 |
+
border-bottom: 3px solid transparent;
|
| 644 |
+
color: var(--text-secondary);
|
| 645 |
+
font-family: var(--font-sans);
|
| 646 |
+
font-size: 0.85rem;
|
| 647 |
+
font-weight: 600;
|
| 648 |
+
cursor: pointer;
|
| 649 |
+
display: flex;
|
| 650 |
+
align-items: center;
|
| 651 |
+
justify-content: center;
|
| 652 |
+
gap: var(--space-sm);
|
| 653 |
+
transition: all var(--transition-base);
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
.tab-btn:hover {
|
| 657 |
+
color: var(--text-primary);
|
| 658 |
+
background: transparent;
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
/* Active tab: 3px solid Red bottom border, Red text, NO background tint */
|
| 662 |
+
.tab-btn.active {
|
| 663 |
+
color: var(--superman-red);
|
| 664 |
+
border-bottom-color: var(--superman-red);
|
| 665 |
+
background: transparent;
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
/* --- Tab Content Visibility --- */
|
| 669 |
+
.tab-content {
|
| 670 |
+
display: none !important;
|
| 671 |
+
}
|
| 672 |
+
.tab-content.active {
|
| 673 |
+
display: flex !important;
|
| 674 |
+
flex-direction: column;
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
/* --- X-Ray Text Container --- */
|
| 678 |
+
.xray-text-container {
|
| 679 |
+
font-family: var(--font-mono);
|
| 680 |
+
font-size: 0.8rem;
|
| 681 |
+
line-height: 2;
|
| 682 |
+
white-space: pre-wrap;
|
| 683 |
+
word-break: break-word;
|
| 684 |
+
padding: var(--space-lg);
|
| 685 |
+
position: relative;
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
/* Chunk highlights in the text */
|
| 689 |
+
.chunk-highlight {
|
| 690 |
+
position: relative;
|
| 691 |
+
border-radius: 3px;
|
| 692 |
+
padding: 1px 0;
|
| 693 |
+
border-left: 3px solid var(--superman-red); /* "Cape" Accent */
|
| 694 |
+
transition: all var(--transition-fast);
|
| 695 |
+
cursor: pointer;
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
/* ============================================================
|
| 699 |
+
RETRIEVAL X-RAY HIGHLIGHTS (TASK 3.4)
|
| 700 |
+
============================================================ */
|
| 701 |
+
.xray-content.search-active .chunk-highlight {
|
| 702 |
+
opacity: 0.3;
|
| 703 |
+
transition: all 0.3s ease;
|
| 704 |
+
}
|
| 705 |
+
|
| 706 |
+
.xray-content.search-active .chunk-highlight.retrieved-rank-1 {
|
| 707 |
+
opacity: 1.0;
|
| 708 |
+
background-color: rgba(255, 165, 0, 0.4);
|
| 709 |
+
box-shadow: 0 0 12px rgba(255, 165, 0, 0.6);
|
| 710 |
+
border-top: 2px solid var(--warning-color);
|
| 711 |
+
border-bottom: 2px solid var(--warning-color);
|
| 712 |
+
z-index: 10;
|
| 713 |
+
position: relative;
|
| 714 |
+
}
|
| 715 |
+
|
| 716 |
+
.xray-content.search-active .chunk-highlight.retrieved-rank-2 {
|
| 717 |
+
opacity: 0.9;
|
| 718 |
+
background-color: rgba(255, 165, 0, 0.25);
|
| 719 |
+
border-top: 2px dashed rgba(255, 165, 0, 0.6);
|
| 720 |
+
border-bottom: 2px dashed rgba(255, 165, 0, 0.6);
|
| 721 |
+
z-index: 9;
|
| 722 |
+
position: relative;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.xray-content.search-active .chunk-highlight.retrieved-rank-3 {
|
| 726 |
+
opacity: 0.8;
|
| 727 |
+
background-color: rgba(255, 165, 0, 0.15);
|
| 728 |
+
border-top: 2px dotted rgba(255, 165, 0, 0.4);
|
| 729 |
+
border-bottom: 2px dotted rgba(255, 165, 0, 0.4);
|
| 730 |
+
z-index: 8;
|
| 731 |
+
position: relative;
|
| 732 |
+
}
|
| 733 |
+
|
| 734 |
+
/* Lexical Heatmap Highlights */
|
| 735 |
+
.query-result-text mark,
|
| 736 |
+
.retrieved-chunk-text mark {
|
| 737 |
+
background-color: rgba(245, 158, 11, 0.22); /* Superman Gold Muted */
|
| 738 |
+
color: #b45309; /* Highly readable deep amber */
|
| 739 |
+
border-bottom: 2px solid var(--superman-yellow);
|
| 740 |
+
font-weight: 600;
|
| 741 |
+
padding: 1px 3px;
|
| 742 |
+
border-radius: 3px;
|
| 743 |
+
transition: all 0.2s ease;
|
| 744 |
+
}
|
| 745 |
+
|
| 746 |
+
.query-result-text mark:hover,
|
| 747 |
+
.retrieved-chunk-text mark:hover {
|
| 748 |
+
background-color: rgba(245, 158, 11, 0.35);
|
| 749 |
+
color: #78350f;
|
| 750 |
+
cursor: help;
|
| 751 |
+
}
|
| 752 |
+
|
| 753 |
+
|
| 754 |
+
/* ============================================================
|
| 755 |
+
THE GRAND COMPARISON ARENA (TASK 3.5)
|
| 756 |
+
============================================================ */
|
| 757 |
+
.arena-modal-overlay {
|
| 758 |
+
position: fixed;
|
| 759 |
+
inset: 0;
|
| 760 |
+
background: rgba(15, 23, 42, 0.9);
|
| 761 |
+
backdrop-filter: blur(8px);
|
| 762 |
+
z-index: 9999;
|
| 763 |
+
display: flex;
|
| 764 |
+
align-items: center;
|
| 765 |
+
justify-content: center;
|
| 766 |
+
}
|
| 767 |
+
|
| 768 |
+
.arena-modal-content {
|
| 769 |
+
background: var(--bg-surface);
|
| 770 |
+
width: 90vw;
|
| 771 |
+
height: 90vh;
|
| 772 |
+
border-radius: var(--radius-lg);
|
| 773 |
+
box-shadow: var(--shadow-xl);
|
| 774 |
+
display: flex;
|
| 775 |
+
flex-direction: column;
|
| 776 |
+
overflow: hidden;
|
| 777 |
+
border: 1px solid var(--border-default);
|
| 778 |
+
}
|
| 779 |
+
|
| 780 |
+
.arena-header {
|
| 781 |
+
display: flex;
|
| 782 |
+
justify-content: space-between;
|
| 783 |
+
align-items: center;
|
| 784 |
+
padding: var(--space-md) var(--space-xl);
|
| 785 |
+
background: var(--bg-surface-elevated);
|
| 786 |
+
border-bottom: 1px solid var(--border-default);
|
| 787 |
+
}
|
| 788 |
+
|
| 789 |
+
.arena-header h2 {
|
| 790 |
+
margin: 0;
|
| 791 |
+
font-size: 1.25rem;
|
| 792 |
+
color: var(--superman-red);
|
| 793 |
+
}
|
| 794 |
+
|
| 795 |
+
.btn-close-arena {
|
| 796 |
+
background: none;
|
| 797 |
+
border: none;
|
| 798 |
+
color: var(--text-secondary);
|
| 799 |
+
font-size: 1.5rem;
|
| 800 |
+
cursor: pointer;
|
| 801 |
+
}
|
| 802 |
+
|
| 803 |
+
.btn-close-arena:hover {
|
| 804 |
+
color: var(--superman-red);
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
.arena-query-bar {
|
| 808 |
+
display: flex;
|
| 809 |
+
padding: var(--space-lg) var(--space-xl);
|
| 810 |
+
gap: var(--space-md);
|
| 811 |
+
background: var(--bg-surface-elevated);
|
| 812 |
+
border-bottom: 1px solid var(--border-default);
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
.arena-query-bar input {
|
| 816 |
+
flex: 1;
|
| 817 |
+
padding: var(--space-md) var(--space-lg);
|
| 818 |
+
border-radius: var(--radius-md);
|
| 819 |
+
border: 1px solid var(--border-default);
|
| 820 |
+
background: var(--bg-surface);
|
| 821 |
+
color: var(--text-primary);
|
| 822 |
+
font-size: 1rem;
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
.arena-columns {
|
| 826 |
+
display: flex;
|
| 827 |
+
height: 385px;
|
| 828 |
+
flex-shrink: 0;
|
| 829 |
+
border-bottom: 1px solid var(--border-default);
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
.arena-body {
|
| 833 |
+
flex: 1;
|
| 834 |
+
overflow-y: auto;
|
| 835 |
+
padding: 0 0 var(--space-xl) 0;
|
| 836 |
+
display: flex;
|
| 837 |
+
flex-direction: column;
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
.arena-col {
|
| 841 |
+
flex: 1;
|
| 842 |
+
display: flex;
|
| 843 |
+
flex-direction: column;
|
| 844 |
+
border-right: 1px solid var(--border-default);
|
| 845 |
+
}
|
| 846 |
+
.arena-col:last-child {
|
| 847 |
+
border-right: none;
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
.arena-col-header {
|
| 851 |
+
padding: var(--space-md) var(--space-xl);
|
| 852 |
+
border-bottom: 2px solid var(--warning-color);
|
| 853 |
+
background: rgba(255, 255, 255, 0.02);
|
| 854 |
+
}
|
| 855 |
+
|
| 856 |
+
.arena-col-header h3 {
|
| 857 |
+
margin: 0 0 var(--space-sm) 0;
|
| 858 |
+
font-size: 1.1rem;
|
| 859 |
+
font-weight: 700;
|
| 860 |
+
color: var(--text-primary);
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
.arena-config-row {
|
| 864 |
+
display: flex;
|
| 865 |
+
gap: var(--space-sm);
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
.arena-config-row select {
|
| 869 |
+
flex: 1;
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
.arena-results-list {
|
| 873 |
+
flex: 1;
|
| 874 |
+
overflow-y: auto;
|
| 875 |
+
padding: var(--space-xl);
|
| 876 |
+
display: flex;
|
| 877 |
+
flex-direction: column;
|
| 878 |
+
gap: var(--space-md);
|
| 879 |
+
}
|
| 880 |
+
|
| 881 |
+
/* Low opacity wash of primary colors, no underlines */
|
| 882 |
+
.chunk-highlight[data-color="1"] { background: rgba(37, 99, 235, 0.1); }
|
| 883 |
+
.chunk-highlight[data-color="2"] { background: rgba(239, 68, 68, 0.1); }
|
| 884 |
+
.chunk-highlight[data-color="3"] { background: rgba(245, 158, 11, 0.1); }
|
| 885 |
+
.chunk-highlight[data-color="4"] { background: rgba(129, 140, 248, 0.1); }
|
| 886 |
+
|
| 887 |
+
.chunk-highlight.active[data-color="1"] { background: rgba(37, 99, 235, 0.25); box-shadow: 0 0 12px rgba(37, 99, 235, 0.15); }
|
| 888 |
+
.chunk-highlight.active[data-color="2"] { background: rgba(239, 68, 68, 0.25); box-shadow: 0 0 12px rgba(239, 68, 68, 0.15); }
|
| 889 |
+
.chunk-highlight.active[data-color="3"] { background: rgba(245, 158, 11, 0.25); box-shadow: 0 0 12px rgba(245, 158, 11, 0.15); }
|
| 890 |
+
.chunk-highlight.active[data-color="4"] { background: rgba(129, 140, 248, 0.25); box-shadow: 0 0 12px rgba(129, 140, 248, 0.15); }
|
| 891 |
+
|
| 892 |
+
/* Chunk boundary marker */
|
| 893 |
+
.chunk-boundary {
|
| 894 |
+
display: inline-block;
|
| 895 |
+
width: 0;
|
| 896 |
+
height: 1em;
|
| 897 |
+
border-left: 2px dashed var(--text-tertiary);
|
| 898 |
+
margin: 0 2px;
|
| 899 |
+
vertical-align: middle;
|
| 900 |
+
opacity: 0.5;
|
| 901 |
+
}
|
| 902 |
+
|
| 903 |
+
/* Overlap region */
|
| 904 |
+
.overlap-region {
|
| 905 |
+
background: repeating-linear-gradient(
|
| 906 |
+
-45deg,
|
| 907 |
+
transparent,
|
| 908 |
+
transparent 2px,
|
| 909 |
+
rgba(251, 146, 60, 0.15) 2px,
|
| 910 |
+
rgba(251, 146, 60, 0.15) 4px
|
| 911 |
+
) !important;
|
| 912 |
+
border-bottom: 2px dashed var(--warning) !important;
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
/* Empty state */
|
| 916 |
+
.empty-state {
|
| 917 |
+
display: flex;
|
| 918 |
+
flex-direction: column;
|
| 919 |
+
align-items: center;
|
| 920 |
+
justify-content: center;
|
| 921 |
+
height: 100%;
|
| 922 |
+
min-height: 400px;
|
| 923 |
+
color: var(--text-tertiary);
|
| 924 |
+
text-align: center;
|
| 925 |
+
gap: var(--space-md);
|
| 926 |
+
}
|
| 927 |
+
|
| 928 |
+
.empty-state-icon {
|
| 929 |
+
font-size: 3rem;
|
| 930 |
+
opacity: 0.8;
|
| 931 |
+
color: #22c55e; /* Kryptonite Green */
|
| 932 |
+
}
|
| 933 |
+
|
| 934 |
+
.empty-state-title {
|
| 935 |
+
font-size: 1rem;
|
| 936 |
+
font-weight: 600;
|
| 937 |
+
color: var(--text-secondary);
|
| 938 |
+
}
|
| 939 |
+
|
| 940 |
+
.empty-state-desc {
|
| 941 |
+
font-size: 0.78rem;
|
| 942 |
+
max-width: 280px;
|
| 943 |
+
line-height: 1.6;
|
| 944 |
+
}
|
| 945 |
+
|
| 946 |
+
/* ============================================================
|
| 947 |
+
6. VECTOR SPACE TAB (Phase 2)
|
| 948 |
+
============================================================ */
|
| 949 |
+
.vector-space-container {
|
| 950 |
+
display: flex;
|
| 951 |
+
flex-direction: column;
|
| 952 |
+
height: 100%;
|
| 953 |
+
position: relative;
|
| 954 |
+
gap: var(--space-md);
|
| 955 |
+
padding: var(--space-lg);
|
| 956 |
+
}
|
| 957 |
+
|
| 958 |
+
/* Canvas wrapper: Fortress of Solitude Grid */
|
| 959 |
+
.canvas-wrapper {
|
| 960 |
+
flex: 1;
|
| 961 |
+
position: relative;
|
| 962 |
+
border: 1px solid var(--superman-blue);
|
| 963 |
+
border-radius: var(--radius-lg);
|
| 964 |
+
background-color: #fafafa;
|
| 965 |
+
background-image:
|
| 966 |
+
linear-gradient(rgba(37, 99, 235, 0.05) 1px, transparent 1px),
|
| 967 |
+
linear-gradient(90deg, rgba(37, 99, 235, 0.05) 1px, transparent 1px);
|
| 968 |
+
background-size: 24px 24px;
|
| 969 |
+
overflow: hidden;
|
| 970 |
+
box-shadow: inset 0 2px 8px rgba(15, 23, 42, 0.04);
|
| 971 |
+
min-height: 380px;
|
| 972 |
+
}
|
| 973 |
+
|
| 974 |
+
#vector-canvas {
|
| 975 |
+
display: block;
|
| 976 |
+
width: 100%;
|
| 977 |
+
height: 100%;
|
| 978 |
+
cursor: grab;
|
| 979 |
+
}
|
| 980 |
+
#vector-canvas:active {
|
| 981 |
+
cursor: grabbing;
|
| 982 |
+
}
|
| 983 |
+
|
| 984 |
+
/* Canvas Help Overlay */
|
| 985 |
+
.canvas-help-overlay {
|
| 986 |
+
position: absolute;
|
| 987 |
+
top: var(--space-md);
|
| 988 |
+
right: var(--space-md);
|
| 989 |
+
display: flex;
|
| 990 |
+
flex-direction: column;
|
| 991 |
+
gap: var(--space-xs);
|
| 992 |
+
background: rgba(255, 255, 255, 0.85);
|
| 993 |
+
backdrop-filter: blur(4px);
|
| 994 |
+
padding: 6px 12px;
|
| 995 |
+
border-radius: var(--radius-md);
|
| 996 |
+
border: 1px solid var(--border-default);
|
| 997 |
+
pointer-events: none;
|
| 998 |
+
}
|
| 999 |
+
.canvas-help-overlay span {
|
| 1000 |
+
font-size: 0.68rem;
|
| 1001 |
+
color: var(--text-tertiary);
|
| 1002 |
+
font-family: var(--font-mono);
|
| 1003 |
+
}
|
| 1004 |
+
|
| 1005 |
+
/* Floating Tooltip inside Canvas — Blue left accent bar */
|
| 1006 |
+
.vector-tooltip {
|
| 1007 |
+
position: absolute;
|
| 1008 |
+
pointer-events: none;
|
| 1009 |
+
background: rgba(255, 255, 255, 0.95);
|
| 1010 |
+
backdrop-filter: blur(12px);
|
| 1011 |
+
-webkit-backdrop-filter: blur(12px);
|
| 1012 |
+
border: 1px solid var(--border-default);
|
| 1013 |
+
border-left: 3px solid var(--superman-blue);
|
| 1014 |
+
border-radius: var(--radius-md);
|
| 1015 |
+
padding: var(--space-md);
|
| 1016 |
+
color: var(--text-primary);
|
| 1017 |
+
max-width: 280px;
|
| 1018 |
+
font-size: 0.75rem;
|
| 1019 |
+
box-shadow: var(--shadow-lg);
|
| 1020 |
+
z-index: 50;
|
| 1021 |
+
transition: opacity 100ms ease;
|
| 1022 |
+
}
|
| 1023 |
+
.vector-tooltip-title {
|
| 1024 |
+
font-family: var(--font-mono);
|
| 1025 |
+
font-weight: 700;
|
| 1026 |
+
color: var(--superman-blue);
|
| 1027 |
+
margin-bottom: var(--space-xs);
|
| 1028 |
+
font-size: 0.68rem;
|
| 1029 |
+
text-transform: uppercase;
|
| 1030 |
+
letter-spacing: 0.05em;
|
| 1031 |
+
display: flex;
|
| 1032 |
+
justify-content: space-between;
|
| 1033 |
+
}
|
| 1034 |
+
.vector-tooltip-coords {
|
| 1035 |
+
color: var(--text-tertiary);
|
| 1036 |
+
font-family: var(--font-mono);
|
| 1037 |
+
font-size: 0.65rem;
|
| 1038 |
+
}
|
| 1039 |
+
.vector-tooltip-text {
|
| 1040 |
+
font-family: var(--font-mono);
|
| 1041 |
+
color: var(--text-secondary);
|
| 1042 |
+
line-height: 1.5;
|
| 1043 |
+
margin-top: var(--space-xs);
|
| 1044 |
+
border-top: 1px solid var(--border-subtle);
|
| 1045 |
+
padding-top: var(--space-xs);
|
| 1046 |
+
}
|
| 1047 |
+
|
| 1048 |
+
/* ============================================================
|
| 1049 |
+
7. SONAR QUERY SIMULATOR
|
| 1050 |
+
============================================================ */
|
| 1051 |
+
.query-simulator-section {
|
| 1052 |
+
background: var(--bg-surface);
|
| 1053 |
+
border: 1px solid var(--border-default);
|
| 1054 |
+
border-radius: var(--radius-lg);
|
| 1055 |
+
padding: var(--space-md);
|
| 1056 |
+
display: flex;
|
| 1057 |
+
flex-direction: column;
|
| 1058 |
+
gap: var(--space-sm);
|
| 1059 |
+
box-shadow: var(--shadow-md);
|
| 1060 |
+
}
|
| 1061 |
+
|
| 1062 |
+
.query-section-title {
|
| 1063 |
+
font-family: var(--font-heading);
|
| 1064 |
+
font-size: 0.9rem;
|
| 1065 |
+
font-weight: 700;
|
| 1066 |
+
text-transform: uppercase;
|
| 1067 |
+
letter-spacing: 0.08em;
|
| 1068 |
+
color: var(--superman-blue);
|
| 1069 |
+
display: flex;
|
| 1070 |
+
align-items: center;
|
| 1071 |
+
gap: 6px;
|
| 1072 |
+
}
|
| 1073 |
+
|
| 1074 |
+
.query-section-title .radar-icon {
|
| 1075 |
+
color: var(--superman-yellow);
|
| 1076 |
+
}
|
| 1077 |
+
|
| 1078 |
+
.query-input-wrapper {
|
| 1079 |
+
display: flex;
|
| 1080 |
+
gap: var(--space-sm);
|
| 1081 |
+
}
|
| 1082 |
+
|
| 1083 |
+
/* Query input: Blue focus ring */
|
| 1084 |
+
#query-input {
|
| 1085 |
+
flex: 1;
|
| 1086 |
+
padding: 10px 14px;
|
| 1087 |
+
background: var(--bg-input);
|
| 1088 |
+
border: 1px solid var(--border-default);
|
| 1089 |
+
border-radius: var(--radius-md);
|
| 1090 |
+
color: var(--text-primary);
|
| 1091 |
+
font-size: 0.8rem;
|
| 1092 |
+
outline: none;
|
| 1093 |
+
transition: all var(--transition-base);
|
| 1094 |
+
}
|
| 1095 |
+
#query-input:focus {
|
| 1096 |
+
border-color: var(--superman-blue);
|
| 1097 |
+
box-shadow: 0 0 0 2px var(--superman-blue-muted);
|
| 1098 |
+
}
|
| 1099 |
+
|
| 1100 |
+
/* Query button: Red CTA style */
|
| 1101 |
+
.btn-query {
|
| 1102 |
+
padding: 10px var(--space-lg);
|
| 1103 |
+
background: var(--superman-red);
|
| 1104 |
+
border: 1px solid var(--superman-red);
|
| 1105 |
+
color: var(--text-inverse);
|
| 1106 |
+
font-family: var(--font-heading);
|
| 1107 |
+
font-weight: 700;
|
| 1108 |
+
font-size: 0.9rem;
|
| 1109 |
+
border-radius: var(--radius-md);
|
| 1110 |
+
cursor: pointer;
|
| 1111 |
+
transition: all var(--transition-base);
|
| 1112 |
+
}
|
| 1113 |
+
.btn-query:hover {
|
| 1114 |
+
background: var(--superman-red-hover);
|
| 1115 |
+
box-shadow: var(--shadow-glow-red);
|
| 1116 |
+
}
|
| 1117 |
+
|
| 1118 |
+
/* Retrieved Results Drawer */
|
| 1119 |
+
.query-results-drawer {
|
| 1120 |
+
border-top: 1px solid var(--border-subtle);
|
| 1121 |
+
padding-top: var(--space-sm);
|
| 1122 |
+
display: flex;
|
| 1123 |
+
flex-direction: column;
|
| 1124 |
+
gap: var(--space-sm);
|
| 1125 |
+
max-height: 200px;
|
| 1126 |
+
overflow-y: auto;
|
| 1127 |
+
}
|
| 1128 |
+
|
| 1129 |
+
.drawer-header {
|
| 1130 |
+
display: flex;
|
| 1131 |
+
justify-content: space-between;
|
| 1132 |
+
align-items: center;
|
| 1133 |
+
}
|
| 1134 |
+
.drawer-header h3 {
|
| 1135 |
+
font-size: 0.72rem;
|
| 1136 |
+
font-weight: 600;
|
| 1137 |
+
text-transform: uppercase;
|
| 1138 |
+
letter-spacing: 0.05em;
|
| 1139 |
+
color: var(--text-secondary);
|
| 1140 |
+
}
|
| 1141 |
+
.close-drawer {
|
| 1142 |
+
cursor: pointer;
|
| 1143 |
+
font-size: 1.2rem;
|
| 1144 |
+
color: var(--text-tertiary);
|
| 1145 |
+
line-height: 1;
|
| 1146 |
+
transition: color var(--transition-fast);
|
| 1147 |
+
}
|
| 1148 |
+
.close-drawer:hover {
|
| 1149 |
+
color: var(--superman-red);
|
| 1150 |
+
}
|
| 1151 |
+
|
| 1152 |
+
.query-results-list {
|
| 1153 |
+
display: flex;
|
| 1154 |
+
flex-direction: column;
|
| 1155 |
+
gap: var(--space-xs);
|
| 1156 |
+
}
|
| 1157 |
+
|
| 1158 |
+
.retrieved-chunk-card {
|
| 1159 |
+
display: flex;
|
| 1160 |
+
flex-direction: column;
|
| 1161 |
+
padding: var(--space-sm) var(--space-md);
|
| 1162 |
+
background-color: #ffffff;
|
| 1163 |
+
border: 1px solid var(--border-subtle);
|
| 1164 |
+
border-left: 4px solid var(--superman-blue);
|
| 1165 |
+
border-radius: var(--radius-md);
|
| 1166 |
+
gap: 2px;
|
| 1167 |
+
cursor: pointer;
|
| 1168 |
+
transition: all var(--transition-fast);
|
| 1169 |
+
}
|
| 1170 |
+
.retrieved-chunk-card:hover {
|
| 1171 |
+
border-color: var(--superman-blue);
|
| 1172 |
+
background: var(--superman-blue-muted);
|
| 1173 |
+
}
|
| 1174 |
+
|
| 1175 |
+
/* Rank 1 card: Gold left accent border */
|
| 1176 |
+
.retrieved-chunk-card.rank-1 {
|
| 1177 |
+
border-left: 4px solid var(--superman-yellow);
|
| 1178 |
+
background: #ffffff;
|
| 1179 |
+
}
|
| 1180 |
+
|
| 1181 |
+
.retrieved-chunk-card.rank-1:hover {
|
| 1182 |
+
background: var(--superman-yellow-muted);
|
| 1183 |
+
}
|
| 1184 |
+
|
| 1185 |
+
.retrieved-chunk-meta {
|
| 1186 |
+
display: flex;
|
| 1187 |
+
justify-content: space-between;
|
| 1188 |
+
font-size: 0.65rem;
|
| 1189 |
+
font-family: var(--font-mono);
|
| 1190 |
+
}
|
| 1191 |
+
|
| 1192 |
+
/* Rank label: Gold for Rank 1, Blue for others */
|
| 1193 |
+
.retrieved-chunk-rank {
|
| 1194 |
+
font-weight: 700;
|
| 1195 |
+
color: var(--superman-blue);
|
| 1196 |
+
}
|
| 1197 |
+
.retrieved-chunk-card.rank-1 .retrieved-chunk-rank {
|
| 1198 |
+
color: var(--superman-yellow);
|
| 1199 |
+
}
|
| 1200 |
+
|
| 1201 |
+
/* Score badge: Red pill */
|
| 1202 |
+
.retrieved-chunk-score {
|
| 1203 |
+
color: var(--text-inverse);
|
| 1204 |
+
background-color: var(--superman-red);
|
| 1205 |
+
font-weight: bold;
|
| 1206 |
+
padding: 1px 8px;
|
| 1207 |
+
border-radius: 99px;
|
| 1208 |
+
}
|
| 1209 |
+
|
| 1210 |
+
.retrieved-chunk-text {
|
| 1211 |
+
font-family: var(--font-mono);
|
| 1212 |
+
font-size: 0.7rem;
|
| 1213 |
+
color: var(--text-secondary);
|
| 1214 |
+
line-height: 1.4;
|
| 1215 |
+
white-space: nowrap;
|
| 1216 |
+
overflow: hidden;
|
| 1217 |
+
text-overflow: ellipsis;
|
| 1218 |
+
}
|
| 1219 |
+
|
| 1220 |
+
/* ============================================================
|
| 1221 |
+
8. CHUNK INSPECTOR (Right Panel)
|
| 1222 |
+
============================================================ */
|
| 1223 |
+
|
| 1224 |
+
/* --- Stat Cards: Frosted glass with Blue top accent border --- */
|
| 1225 |
+
.stats-grid {
|
| 1226 |
+
display: grid;
|
| 1227 |
+
grid-template-columns: 1fr 1fr;
|
| 1228 |
+
gap: var(--space-sm);
|
| 1229 |
+
margin-bottom: var(--space-lg);
|
| 1230 |
+
}
|
| 1231 |
+
|
| 1232 |
+
.stat-card {
|
| 1233 |
+
padding: var(--space-md);
|
| 1234 |
+
background: rgba(255, 255, 255, 0.7);
|
| 1235 |
+
backdrop-filter: blur(8px);
|
| 1236 |
+
-webkit-backdrop-filter: blur(8px);
|
| 1237 |
+
border: 1px solid var(--border-default);
|
| 1238 |
+
border-top: 3px solid var(--superman-blue);
|
| 1239 |
+
border-radius: var(--radius-md);
|
| 1240 |
+
text-align: center;
|
| 1241 |
+
box-shadow: var(--shadow-sm);
|
| 1242 |
+
transition: all var(--transition-base);
|
| 1243 |
+
}
|
| 1244 |
+
|
| 1245 |
+
.stat-card:hover {
|
| 1246 |
+
box-shadow: var(--shadow-md);
|
| 1247 |
+
transform: translateY(-1px);
|
| 1248 |
+
}
|
| 1249 |
+
|
| 1250 |
+
/* Stat values: Red/Gold monospace */
|
| 1251 |
+
.stat-card .stat-value {
|
| 1252 |
+
font-family: var(--font-heading);
|
| 1253 |
+
font-size: 1.8rem;
|
| 1254 |
+
font-weight: 700;
|
| 1255 |
+
color: var(--superman-red);
|
| 1256 |
+
}
|
| 1257 |
+
|
| 1258 |
+
.stat-card:nth-child(even) .stat-value {
|
| 1259 |
+
color: var(--superman-yellow);
|
| 1260 |
+
}
|
| 1261 |
+
|
| 1262 |
+
.stat-card .stat-label {
|
| 1263 |
+
font-family: var(--font-heading);
|
| 1264 |
+
font-size: 0.75rem;
|
| 1265 |
+
text-transform: uppercase;
|
| 1266 |
+
letter-spacing: 0.08em;
|
| 1267 |
+
color: var(--text-tertiary);
|
| 1268 |
+
margin-top: 2px;
|
| 1269 |
+
}
|
| 1270 |
+
|
| 1271 |
+
/* --- Chunk List --- */
|
| 1272 |
+
.chunk-list {
|
| 1273 |
+
display: flex;
|
| 1274 |
+
flex-direction: column;
|
| 1275 |
+
gap: var(--space-sm);
|
| 1276 |
+
}
|
| 1277 |
+
|
| 1278 |
+
.chunk-item {
|
| 1279 |
+
padding: var(--space-md);
|
| 1280 |
+
padding-left: calc(var(--space-md) + 28px); /* Room for the index circle */
|
| 1281 |
+
background-color: #ffffff;
|
| 1282 |
+
border: 1px solid #e5e7eb;
|
| 1283 |
+
border-left: 4px solid var(--superman-blue); /* Default alternating later */
|
| 1284 |
+
border-radius: 4px;
|
| 1285 |
+
cursor: pointer;
|
| 1286 |
+
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease, background 0.2s ease;
|
| 1287 |
+
position: relative;
|
| 1288 |
+
overflow: hidden;
|
| 1289 |
+
}
|
| 1290 |
+
|
| 1291 |
+
/* Alternate Left Borders */
|
| 1292 |
+
.chunk-item:nth-child(3n+1) { border-left-color: var(--superman-blue); }
|
| 1293 |
+
.chunk-item:nth-child(3n+2) { border-left-color: var(--superman-red); }
|
| 1294 |
+
.chunk-item:nth-child(3n+3) { border-left-color: var(--superman-yellow); }
|
| 1295 |
+
|
| 1296 |
+
.chunk-item:hover {
|
| 1297 |
+
transform: translateY(-2px);
|
| 1298 |
+
box-shadow: 0 10px 15px -3px rgba(37, 99, 235, 0.15);
|
| 1299 |
+
}
|
| 1300 |
+
|
| 1301 |
+
/* Active chunk */
|
| 1302 |
+
.chunk-item.active {
|
| 1303 |
+
background: rgba(37, 99, 235, 0.05);
|
| 1304 |
+
}
|
| 1305 |
+
|
| 1306 |
+
.chunk-item.active::before {
|
| 1307 |
+
background: var(--superman-red);
|
| 1308 |
+
}
|
| 1309 |
+
|
| 1310 |
+
/* Parent chunk styling */
|
| 1311 |
+
.chunk-item[data-level="0"] {
|
| 1312 |
+
border-left: 3px solid var(--superman-blue);
|
| 1313 |
+
}
|
| 1314 |
+
|
| 1315 |
+
.chunk-item[data-level="0"]::before {
|
| 1316 |
+
background: var(--superman-blue);
|
| 1317 |
+
}
|
| 1318 |
+
|
| 1319 |
+
/* Child chunk styling */
|
| 1320 |
+
.chunk-item[data-level="1"] {
|
| 1321 |
+
margin-left: var(--space-lg);
|
| 1322 |
+
border-left: 3px solid var(--superman-yellow);
|
| 1323 |
+
opacity: 0.85;
|
| 1324 |
+
}
|
| 1325 |
+
|
| 1326 |
+
.chunk-item[data-level="1"]::before {
|
| 1327 |
+
background: var(--superman-yellow);
|
| 1328 |
+
}
|
| 1329 |
+
|
| 1330 |
+
.chunk-item-header {
|
| 1331 |
+
display: flex;
|
| 1332 |
+
align-items: center;
|
| 1333 |
+
justify-content: space-between;
|
| 1334 |
+
margin-bottom: var(--space-sm);
|
| 1335 |
+
}
|
| 1336 |
+
|
| 1337 |
+
/* Chunk ID: Blue mono label */
|
| 1338 |
+
.chunk-item-id {
|
| 1339 |
+
font-family: var(--font-mono);
|
| 1340 |
+
font-size: 0.7rem;
|
| 1341 |
+
font-weight: 600;
|
| 1342 |
+
color: var(--superman-blue);
|
| 1343 |
+
}
|
| 1344 |
+
|
| 1345 |
+
.chunk-item.active .chunk-item-id {
|
| 1346 |
+
color: var(--superman-red);
|
| 1347 |
+
}
|
| 1348 |
+
|
| 1349 |
+
.chunk-item-badges {
|
| 1350 |
+
display: flex;
|
| 1351 |
+
gap: var(--space-xs);
|
| 1352 |
+
}
|
| 1353 |
+
|
| 1354 |
+
.chunk-badge {
|
| 1355 |
+
font-family: var(--font-mono);
|
| 1356 |
+
font-size: 0.6rem;
|
| 1357 |
+
padding: 1px 6px;
|
| 1358 |
+
border-radius: 99px;
|
| 1359 |
+
background: var(--bg-surface-overlay);
|
| 1360 |
+
color: var(--text-tertiary);
|
| 1361 |
+
border: 1px solid var(--border-subtle);
|
| 1362 |
+
}
|
| 1363 |
+
|
| 1364 |
+
/* Token badge: Blue tinted */
|
| 1365 |
+
.chunk-badge.tokens {
|
| 1366 |
+
color: var(--superman-blue);
|
| 1367 |
+
background: var(--superman-blue-muted);
|
| 1368 |
+
border-color: rgba(37, 99, 235, 0.2);
|
| 1369 |
+
}
|
| 1370 |
+
|
| 1371 |
+
.chunk-item-text {
|
| 1372 |
+
font-family: var(--font-mono);
|
| 1373 |
+
font-size: 0.72rem;
|
| 1374 |
+
color: var(--text-secondary);
|
| 1375 |
+
line-height: 1.6;
|
| 1376 |
+
max-height: 60px;
|
| 1377 |
+
overflow: hidden;
|
| 1378 |
+
position: relative;
|
| 1379 |
+
}
|
| 1380 |
+
|
| 1381 |
+
.chunk-item-text::after {
|
| 1382 |
+
content: '';
|
| 1383 |
+
position: absolute;
|
| 1384 |
+
bottom: 0;
|
| 1385 |
+
left: 0;
|
| 1386 |
+
right: 0;
|
| 1387 |
+
height: 24px;
|
| 1388 |
+
background: linear-gradient(transparent, var(--bg-surface));
|
| 1389 |
+
pointer-events: none;
|
| 1390 |
+
}
|
| 1391 |
+
|
| 1392 |
+
.chunk-item.active .chunk-item-text::after {
|
| 1393 |
+
background: linear-gradient(transparent, rgba(239, 68, 68, 0.06));
|
| 1394 |
+
}
|
| 1395 |
+
|
| 1396 |
+
.chunk-item-meta {
|
| 1397 |
+
display: flex;
|
| 1398 |
+
gap: var(--space-md);
|
| 1399 |
+
margin-top: var(--space-sm);
|
| 1400 |
+
font-size: 0.65rem;
|
| 1401 |
+
color: var(--text-tertiary);
|
| 1402 |
+
font-family: var(--font-mono);
|
| 1403 |
+
}
|
| 1404 |
+
|
| 1405 |
+
/* Parent-child connector label */
|
| 1406 |
+
.parent-child-label {
|
| 1407 |
+
font-size: 0.6rem;
|
| 1408 |
+
font-weight: 600;
|
| 1409 |
+
text-transform: uppercase;
|
| 1410 |
+
letter-spacing: 0.1em;
|
| 1411 |
+
color: var(--superman-blue);
|
| 1412 |
+
padding: var(--space-xs) 0;
|
| 1413 |
+
margin-top: var(--space-sm);
|
| 1414 |
+
}
|
| 1415 |
+
|
| 1416 |
+
/* --- Config: Parent-Child Fields --- */
|
| 1417 |
+
.parent-child-config {
|
| 1418 |
+
display: none;
|
| 1419 |
+
flex-direction: column;
|
| 1420 |
+
gap: var(--space-md);
|
| 1421 |
+
padding: var(--space-md);
|
| 1422 |
+
background: var(--bg-surface);
|
| 1423 |
+
border: 1px solid var(--border-default);
|
| 1424 |
+
border-radius: var(--radius-md);
|
| 1425 |
+
margin-top: var(--space-sm);
|
| 1426 |
+
}
|
| 1427 |
+
|
| 1428 |
+
.parent-child-config.visible {
|
| 1429 |
+
display: flex;
|
| 1430 |
+
}
|
| 1431 |
+
|
| 1432 |
+
.pc-section-label {
|
| 1433 |
+
font-size: 0.65rem;
|
| 1434 |
+
font-weight: 700;
|
| 1435 |
+
text-transform: uppercase;
|
| 1436 |
+
letter-spacing: 0.1em;
|
| 1437 |
+
padding-bottom: var(--space-xs);
|
| 1438 |
+
border-bottom: 1px solid var(--border-subtle);
|
| 1439 |
+
}
|
| 1440 |
+
|
| 1441 |
+
.pc-section-label.parent { color: var(--superman-blue); }
|
| 1442 |
+
.pc-section-label.child { color: var(--superman-yellow); }
|
| 1443 |
+
|
| 1444 |
+
/* ============================================================
|
| 1445 |
+
9. SELECT INPUT
|
| 1446 |
+
============================================================ */
|
| 1447 |
+
.select-input {
|
| 1448 |
+
width: 100%;
|
| 1449 |
+
padding: 8px 12px;
|
| 1450 |
+
background: var(--bg-input);
|
| 1451 |
+
border: 1px solid var(--border-default);
|
| 1452 |
+
border-radius: var(--radius-md);
|
| 1453 |
+
color: var(--text-primary);
|
| 1454 |
+
outline: none;
|
| 1455 |
+
font-family: var(--font-sans);
|
| 1456 |
+
font-size: 0.85rem;
|
| 1457 |
+
cursor: pointer;
|
| 1458 |
+
transition: border-color var(--transition-base);
|
| 1459 |
+
}
|
| 1460 |
+
.select-input:focus {
|
| 1461 |
+
border-color: var(--superman-blue);
|
| 1462 |
+
}
|
| 1463 |
+
|
| 1464 |
+
/* ============================================================
|
| 1465 |
+
10. ANIMATIONS & SKELETONS
|
| 1466 |
+
============================================================ */
|
| 1467 |
+
@keyframes fadeInUp {
|
| 1468 |
+
from {
|
| 1469 |
+
opacity: 0;
|
| 1470 |
+
transform: translateY(8px);
|
| 1471 |
+
}
|
| 1472 |
+
to {
|
| 1473 |
+
opacity: 1;
|
| 1474 |
+
transform: translateY(0);
|
| 1475 |
+
}
|
| 1476 |
+
}
|
| 1477 |
+
|
| 1478 |
+
.chunk-item {
|
| 1479 |
+
animation: fadeInUp var(--transition-slow) both;
|
| 1480 |
+
}
|
| 1481 |
+
|
| 1482 |
+
.chunk-item:nth-child(1) { animation-delay: 0ms; }
|
| 1483 |
+
.chunk-item:nth-child(2) { animation-delay: 40ms; }
|
| 1484 |
+
.chunk-item:nth-child(3) { animation-delay: 80ms; }
|
| 1485 |
+
.chunk-item:nth-child(4) { animation-delay: 120ms; }
|
| 1486 |
+
.chunk-item:nth-child(5) { animation-delay: 160ms; }
|
| 1487 |
+
.chunk-item:nth-child(6) { animation-delay: 200ms; }
|
| 1488 |
+
.chunk-item:nth-child(7) { animation-delay: 240ms; }
|
| 1489 |
+
.chunk-item:nth-child(8) { animation-delay: 280ms; }
|
| 1490 |
+
|
| 1491 |
+
/* 1. Heat Vision Pulse (Applied to the Sonar Probe node in Canvas via JS, but CSS provided if DOM used) */
|
| 1492 |
+
@keyframes radar-pulse {
|
| 1493 |
+
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); }
|
| 1494 |
+
70% { transform: scale(1); box-shadow: 0 0 0 20px rgba(239, 68, 68, 0); }
|
| 1495 |
+
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
|
| 1496 |
+
}
|
| 1497 |
+
.sonar-probe {
|
| 1498 |
+
background-color: #ef4444;
|
| 1499 |
+
border-radius: 50%;
|
| 1500 |
+
animation: radar-pulse 2s infinite;
|
| 1501 |
+
}
|
| 1502 |
+
|
| 1503 |
+
/* 2. Super Speed Data Flow (Applied to the SVG connecting lines if DOM used) */
|
| 1504 |
+
@keyframes data-flow {
|
| 1505 |
+
to { stroke-dashoffset: -20; }
|
| 1506 |
+
}
|
| 1507 |
+
.connection-line {
|
| 1508 |
+
stroke: #f59e0b;
|
| 1509 |
+
stroke-width: 2;
|
| 1510 |
+
stroke-dasharray: 5, 5;
|
| 1511 |
+
animation: data-flow 0.8s linear infinite;
|
| 1512 |
+
}
|
| 1513 |
+
|
| 1514 |
+
/* 3. Yellow Sun Glow (Applied to the Rank 1 retrieved node if DOM used) */
|
| 1515 |
+
@keyframes sun-glow {
|
| 1516 |
+
0%, 100% { box-shadow: 0 0 5px rgba(245, 158, 11, 0.6); }
|
| 1517 |
+
50% { box-shadow: 0 0 15px #f59e0b, 0 0 25px rgba(245, 158, 11, 0.4); }
|
| 1518 |
+
}
|
| 1519 |
+
.rank-1-node {
|
| 1520 |
+
background-color: #f59e0b;
|
| 1521 |
+
border-radius: 50%;
|
| 1522 |
+
animation: sun-glow 3s ease-in-out infinite;
|
| 1523 |
+
}
|
| 1524 |
+
|
| 1525 |
+
/* Heat Vision Skeleton Animation */
|
| 1526 |
+
@keyframes heat-sweep {
|
| 1527 |
+
0% { background-position: -200% 0; }
|
| 1528 |
+
100% { background-position: 200% 0; }
|
| 1529 |
+
}
|
| 1530 |
+
|
| 1531 |
+
.skeleton-loader {
|
| 1532 |
+
height: 60px; /* Adjust based on your text line height */
|
| 1533 |
+
border-radius: 4px;
|
| 1534 |
+
/* Sweeps a soft red gradient over a standard grey skeleton base */
|
| 1535 |
+
background: linear-gradient(90deg, #f3f4f6 25%, rgba(239, 68, 68, 0.15) 50%, #f3f4f6 75%);
|
| 1536 |
+
background-size: 200% 100%;
|
| 1537 |
+
animation: heat-sweep 1.5s infinite linear;
|
| 1538 |
+
}
|
| 1539 |
+
|
| 1540 |
+
/* ============================================================
|
| 1541 |
+
11. RESPONSIVE
|
| 1542 |
+
============================================================ */
|
| 1543 |
+
@media (max-width: 1200px) {
|
| 1544 |
+
.app-main {
|
| 1545 |
+
grid-template-columns: 260px 1fr 300px;
|
| 1546 |
+
}
|
| 1547 |
+
}
|
| 1548 |
+
|
| 1549 |
+
@media (max-width: 900px) {
|
| 1550 |
+
.app-main {
|
| 1551 |
+
grid-template-columns: 1fr;
|
| 1552 |
+
grid-template-rows: auto 1fr auto;
|
| 1553 |
+
}
|
| 1554 |
+
|
| 1555 |
+
.panel {
|
| 1556 |
+
height: auto;
|
| 1557 |
+
max-height: 50vh;
|
| 1558 |
+
border-right: none;
|
| 1559 |
+
border-bottom: 1px solid var(--border-default);
|
| 1560 |
+
}
|
| 1561 |
+
}
|
| 1562 |
+
|
| 1563 |
+
/* ============================================================
|
| 1564 |
+
12. AI REFEREE (TASK 5.1)
|
| 1565 |
+
============================================================ */
|
| 1566 |
+
.arena-referee-panel {
|
| 1567 |
+
margin-top: 2rem;
|
| 1568 |
+
padding: 1.5rem;
|
| 1569 |
+
border-radius: 8px;
|
| 1570 |
+
background-color: var(--bg-surface-raised);
|
| 1571 |
+
border: 1px solid var(--border-default);
|
| 1572 |
+
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.02);
|
| 1573 |
+
}
|
| 1574 |
+
|
| 1575 |
+
.referee-divider {
|
| 1576 |
+
display: flex;
|
| 1577 |
+
align-items: center;
|
| 1578 |
+
text-align: center;
|
| 1579 |
+
margin-bottom: 1.5rem;
|
| 1580 |
+
}
|
| 1581 |
+
|
| 1582 |
+
.referee-divider::before,
|
| 1583 |
+
.referee-divider::after {
|
| 1584 |
+
content: '';
|
| 1585 |
+
flex: 1;
|
| 1586 |
+
border-bottom: 1px dashed var(--border-default);
|
| 1587 |
+
}
|
| 1588 |
+
|
| 1589 |
+
.referee-badge {
|
| 1590 |
+
padding: 0.25rem 0.75rem;
|
| 1591 |
+
font-family: 'Roboto Condensed', sans-serif;
|
| 1592 |
+
font-weight: 700;
|
| 1593 |
+
font-size: 0.85rem;
|
| 1594 |
+
color: var(--superman-blue);
|
| 1595 |
+
background-color: var(--superman-blue-muted);
|
| 1596 |
+
border-radius: 50px;
|
| 1597 |
+
border: 1px solid rgba(37, 99, 235, 0.2);
|
| 1598 |
+
letter-spacing: 0.05em;
|
| 1599 |
+
margin: 0 1rem;
|
| 1600 |
+
}
|
| 1601 |
+
|
| 1602 |
+
.referee-prompt-text {
|
| 1603 |
+
text-align: center;
|
| 1604 |
+
color: var(--text-secondary);
|
| 1605 |
+
font-size: 0.9rem;
|
| 1606 |
+
margin-bottom: 1rem;
|
| 1607 |
+
}
|
| 1608 |
+
|
| 1609 |
+
.referee-status-text {
|
| 1610 |
+
text-align: center;
|
| 1611 |
+
font-weight: 500;
|
| 1612 |
+
color: var(--superman-blue);
|
| 1613 |
+
font-size: 0.9rem;
|
| 1614 |
+
animation: pulse 1.5s infinite ease-in-out;
|
| 1615 |
+
}
|
| 1616 |
+
|
| 1617 |
+
.referee-verdict-box {
|
| 1618 |
+
animation: fadeIn 0.4s ease-out;
|
| 1619 |
+
}
|
| 1620 |
+
|
| 1621 |
+
/* Verdict banner */
|
| 1622 |
+
.verdict-banner {
|
| 1623 |
+
display: flex;
|
| 1624 |
+
align-items: center;
|
| 1625 |
+
justify-content: space-between;
|
| 1626 |
+
padding: 1rem 1.5rem;
|
| 1627 |
+
border-radius: 6px;
|
| 1628 |
+
border: 1.5px solid;
|
| 1629 |
+
margin-bottom: 1.5rem;
|
| 1630 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03);
|
| 1631 |
+
}
|
| 1632 |
+
|
| 1633 |
+
.verdict-title {
|
| 1634 |
+
font-family: 'Roboto Condensed', sans-serif;
|
| 1635 |
+
font-weight: 700;
|
| 1636 |
+
font-size: 1.2rem;
|
| 1637 |
+
letter-spacing: 0.02em;
|
| 1638 |
+
}
|
| 1639 |
+
|
| 1640 |
+
.verdict-confidence {
|
| 1641 |
+
font-size: 0.85rem;
|
| 1642 |
+
font-weight: 600;
|
| 1643 |
+
background-color: rgba(255, 255, 255, 0.7);
|
| 1644 |
+
padding: 0.2rem 0.5rem;
|
| 1645 |
+
border-radius: 4px;
|
| 1646 |
+
border: 1px solid rgba(0, 0, 0, 0.05);
|
| 1647 |
+
}
|
| 1648 |
+
|
| 1649 |
+
/* Verdict reason */
|
| 1650 |
+
.verdict-reason {
|
| 1651 |
+
background-color: var(--bg-surface);
|
| 1652 |
+
padding: 1.25rem;
|
| 1653 |
+
border-radius: 6px;
|
| 1654 |
+
border: 1px solid var(--border-default);
|
| 1655 |
+
margin-bottom: 1.5rem;
|
| 1656 |
+
}
|
| 1657 |
+
|
| 1658 |
+
.verdict-reason > strong {
|
| 1659 |
+
display: block;
|
| 1660 |
+
font-size: 0.85rem;
|
| 1661 |
+
color: var(--text-tertiary);
|
| 1662 |
+
text-transform: uppercase;
|
| 1663 |
+
letter-spacing: 0.05em;
|
| 1664 |
+
margin-bottom: 0.5rem;
|
| 1665 |
+
}
|
| 1666 |
+
|
| 1667 |
+
.verdict-reason p {
|
| 1668 |
+
font-size: 0.95rem;
|
| 1669 |
+
color: var(--text-primary);
|
| 1670 |
+
line-height: 1.5;
|
| 1671 |
+
margin-bottom: 0.75rem;
|
| 1672 |
+
font-style: italic;
|
| 1673 |
+
}
|
| 1674 |
+
|
| 1675 |
+
.deciding-factor {
|
| 1676 |
+
font-size: 0.8rem;
|
| 1677 |
+
color: var(--text-tertiary);
|
| 1678 |
+
display: flex;
|
| 1679 |
+
align-items: center;
|
| 1680 |
+
gap: 0.5rem;
|
| 1681 |
+
}
|
| 1682 |
+
|
| 1683 |
+
.deciding-factor code {
|
| 1684 |
+
background-color: var(--bg-surface-raised);
|
| 1685 |
+
padding: 0.1rem 0.4rem;
|
| 1686 |
+
border-radius: 4px;
|
| 1687 |
+
font-family: 'JetBrains Mono', monospace;
|
| 1688 |
+
color: var(--superman-red);
|
| 1689 |
+
font-weight: 600;
|
| 1690 |
+
}
|
| 1691 |
+
|
| 1692 |
+
/* Scorecard Table */
|
| 1693 |
+
.referee-scores-table {
|
| 1694 |
+
background-color: var(--bg-surface);
|
| 1695 |
+
border: 1px solid var(--border-default);
|
| 1696 |
+
border-radius: 6px;
|
| 1697 |
+
overflow: hidden;
|
| 1698 |
+
margin-bottom: 1.5rem;
|
| 1699 |
+
}
|
| 1700 |
+
|
| 1701 |
+
.scores-table-header {
|
| 1702 |
+
display: grid;
|
| 1703 |
+
grid-template-columns: 150px 70px 1fr 70px;
|
| 1704 |
+
padding: 0.75rem 1rem;
|
| 1705 |
+
background-color: var(--bg-surface-raised);
|
| 1706 |
+
border-bottom: 1px solid var(--border-default);
|
| 1707 |
+
font-weight: 600;
|
| 1708 |
+
font-size: 0.8rem;
|
| 1709 |
+
color: var(--text-tertiary);
|
| 1710 |
+
text-transform: uppercase;
|
| 1711 |
+
letter-spacing: 0.05em;
|
| 1712 |
+
}
|
| 1713 |
+
|
| 1714 |
+
.scores-table-row {
|
| 1715 |
+
display: grid;
|
| 1716 |
+
grid-template-columns: 150px 70px 1fr 70px;
|
| 1717 |
+
align-items: center;
|
| 1718 |
+
padding: 0.75rem 1rem;
|
| 1719 |
+
border-bottom: 1px solid var(--border-default);
|
| 1720 |
+
font-size: 0.9rem;
|
| 1721 |
+
}
|
| 1722 |
+
|
| 1723 |
+
.scores-table-row:last-child {
|
| 1724 |
+
border-bottom: none;
|
| 1725 |
+
}
|
| 1726 |
+
|
| 1727 |
+
.overall-row {
|
| 1728 |
+
background-color: rgba(37, 99, 235, 0.02);
|
| 1729 |
+
border-top: 1.5px solid var(--border-default);
|
| 1730 |
+
}
|
| 1731 |
+
|
| 1732 |
+
.score-col-label {
|
| 1733 |
+
font-weight: 500;
|
| 1734 |
+
color: var(--text-primary);
|
| 1735 |
+
}
|
| 1736 |
+
|
| 1737 |
+
.score-col-val {
|
| 1738 |
+
text-align: center;
|
| 1739 |
+
font-weight: 600;
|
| 1740 |
+
}
|
| 1741 |
+
|
| 1742 |
+
.score-col-bar {
|
| 1743 |
+
padding: 0 1rem;
|
| 1744 |
+
}
|
| 1745 |
+
|
| 1746 |
+
.score-progress-bar {
|
| 1747 |
+
height: 6px;
|
| 1748 |
+
background-color: #e2e8f0;
|
| 1749 |
+
border-radius: 10px;
|
| 1750 |
+
overflow: hidden;
|
| 1751 |
+
display: grid;
|
| 1752 |
+
grid-template-columns: 1fr 1fr;
|
| 1753 |
+
gap: 2px;
|
| 1754 |
+
}
|
| 1755 |
+
|
| 1756 |
+
.score-val-a {
|
| 1757 |
+
background-color: var(--superman-yellow);
|
| 1758 |
+
height: 100%;
|
| 1759 |
+
border-radius: 10px 0 0 10px;
|
| 1760 |
+
justify-self: end;
|
| 1761 |
+
}
|
| 1762 |
+
|
| 1763 |
+
.score-val-b {
|
| 1764 |
+
background-color: var(--superman-blue);
|
| 1765 |
+
height: 100%;
|
| 1766 |
+
border-radius: 0 10px 10px 0;
|
| 1767 |
+
justify-self: start;
|
| 1768 |
+
}
|
| 1769 |
+
|
| 1770 |
+
.overall-progress-bar {
|
| 1771 |
+
height: 10px;
|
| 1772 |
+
background-color: #e2e8f0;
|
| 1773 |
+
border-radius: 10px;
|
| 1774 |
+
overflow: hidden;
|
| 1775 |
+
display: grid;
|
| 1776 |
+
grid-template-columns: 1fr 1fr;
|
| 1777 |
+
gap: 2px;
|
| 1778 |
+
}
|
| 1779 |
+
|
| 1780 |
+
.overall-val-a {
|
| 1781 |
+
background-color: var(--superman-yellow);
|
| 1782 |
+
height: 100%;
|
| 1783 |
+
border-radius: 10px 0 0 10px;
|
| 1784 |
+
justify-self: end;
|
| 1785 |
+
}
|
| 1786 |
+
|
| 1787 |
+
.overall-val-b {
|
| 1788 |
+
background-color: var(--superman-blue);
|
| 1789 |
+
height: 100%;
|
| 1790 |
+
border-radius: 0 10px 10px 0;
|
| 1791 |
+
justify-self: start;
|
| 1792 |
+
}
|
| 1793 |
+
|
| 1794 |
+
/* Strengths & Weaknesses Columns */
|
| 1795 |
+
.verdict-details-grid {
|
| 1796 |
+
display: grid;
|
| 1797 |
+
grid-template-columns: 1fr 1fr;
|
| 1798 |
+
gap: 1.5rem;
|
| 1799 |
+
}
|
| 1800 |
+
|
| 1801 |
+
.verdict-details-col {
|
| 1802 |
+
background-color: var(--bg-surface);
|
| 1803 |
+
border: 1px solid var(--border-default);
|
| 1804 |
+
padding: 1.25rem;
|
| 1805 |
+
border-radius: 6px;
|
| 1806 |
+
}
|
| 1807 |
+
|
| 1808 |
+
.verdict-details-col h4 {
|
| 1809 |
+
font-family: 'Roboto Condensed', sans-serif;
|
| 1810 |
+
font-weight: 700;
|
| 1811 |
+
margin-bottom: 1rem;
|
| 1812 |
+
font-size: 1rem;
|
| 1813 |
+
border-bottom: 1.5px solid var(--border-default);
|
| 1814 |
+
padding-bottom: 0.5rem;
|
| 1815 |
+
}
|
| 1816 |
+
|
| 1817 |
+
.verdict-details-col:first-child h4 {
|
| 1818 |
+
color: var(--warning-color);
|
| 1819 |
+
border-bottom-color: rgba(245, 158, 11, 0.2);
|
| 1820 |
+
}
|
| 1821 |
+
|
| 1822 |
+
.verdict-details-col:last-child h4 {
|
| 1823 |
+
color: var(--info-color);
|
| 1824 |
+
border-bottom-color: rgba(59, 130, 246, 0.2);
|
| 1825 |
+
}
|
| 1826 |
+
|
| 1827 |
+
.verdict-list-title {
|
| 1828 |
+
font-size: 0.75rem;
|
| 1829 |
+
font-weight: 700;
|
| 1830 |
+
text-transform: uppercase;
|
| 1831 |
+
letter-spacing: 0.05em;
|
| 1832 |
+
margin-bottom: 0.5rem;
|
| 1833 |
+
}
|
| 1834 |
+
|
| 1835 |
+
.strength-list-title {
|
| 1836 |
+
color: #16a34a;
|
| 1837 |
+
}
|
| 1838 |
+
|
| 1839 |
+
.weakness-list-title {
|
| 1840 |
+
color: #dc2626;
|
| 1841 |
+
margin-top: 1rem;
|
| 1842 |
+
}
|
| 1843 |
+
|
| 1844 |
+
.verdict-details-col ul {
|
| 1845 |
+
padding-left: 1.25rem;
|
| 1846 |
+
margin: 0;
|
| 1847 |
+
}
|
| 1848 |
+
|
| 1849 |
+
.verdict-details-col li {
|
| 1850 |
+
font-size: 0.85rem;
|
| 1851 |
+
color: var(--text-secondary);
|
| 1852 |
+
line-height: 1.4;
|
| 1853 |
+
margin-bottom: 0.4rem;
|
| 1854 |
+
}
|
| 1855 |
+
|
| 1856 |
+
@keyframes fadeIn {
|
| 1857 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 1858 |
+
to { opacity: 1; transform: translateY(0); }
|
| 1859 |
+
}
|
| 1860 |
+
|
| 1861 |
+
|
| 1862 |
+
|