from __future__ import annotations

import hashlib
import html
import os
import re
import time

os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")

import spaces  # noqa: E402  (must be imported before torch)
import gradio as gr  # noqa: E402
import numpy as np  # noqa: E402
import torch  # noqa: E402
from sentence_transformers import SentenceTransformer  # noqa: E402


MODEL_ID = "Qwen/Qwen3-Embedding-8B"
MODEL_SHORT = "Qwen3-Embedding-8B"
MAX_DOCS = 16
MAX_SEQ_LENGTH = 2048
FULL_DIM = 4096
DEFAULT_DIMS = 1024
DEFAULT_TOP_K = 5
DEFAULT_INSTRUCTION = (
    "Given a web search query, retrieve relevant passages that answer the query"
)

DEFAULT_CORPUS = "\n\n".join(
    [
        (
            "Qwen3-Embedding-8B is an 8-billion-parameter multilingual text embedding "
            "model. It supports more than 100 languages, contexts up to 32K tokens, "
            "and user-defined output dimensions from 32 to 4096 through Matryoshka "
            "representation learning."
        ),
        (
            "Beijing is the capital of China. It is home to the Forbidden City, the "
            "Temple of Heaven, and dozens of universities and research institutes."
        ),
        (
            "France is known for regional cuisines built around bread, cheese, wine, "
            "and seasonal produce. A traditional baguette has a crisp crust and a "
            "soft, airy interior."
        ),
        (
            "Python's asyncio library lets a program coordinate thousands of network "
            "requests concurrently on a single thread. Coroutines yield control while "
            "waiting for I/O, so other tasks can make progress instead of blocking."
        ),
        (
            "Rust's ownership rules ensure that every value has one owner at a time. "
            "References are checked at compile time, which prevents use-after-free "
            "bugs and data races without a garbage collector."
        ),
        (
            "The 2008 financial crisis began with losses on subprime mortgages and "
            "spread through highly leveraged financial institutions. Credit markets "
            "froze, asset prices fell sharply, and governments responded with "
            "emergency lending and stimulus programs."
        ),
        (
            "Photosynthesis is the process by which plants, algae, and some bacteria "
            "convert light energy into chemical energy. Chlorophyll absorbs sunlight, "
            "and the resulting reactions produce glucose and release oxygen."
        ),
        (
            "The Solar System formed about 4.6 billion years ago from a collapsing "
            "cloud of gas and dust. It contains eight planets, numerous moons, "
            "asteroids, comets, and the Sun at its center."
        ),
        (
            "麻婆豆腐是一道经典川菜，主要食材是嫩豆腐、牛肉末、豆瓣酱和花椒。"
            "成菜麻辣鲜香，花椒带来的麻味是它最鲜明的特征之一。"
        ),
        (
            "A vector database stores high-dimensional embeddings and retrieves "
            "nearest neighbors efficiently. It is commonly used for semantic search, "
            "recommendation, clustering, and retrieval-augmented generation."
        ),
    ]
)

CSS = """
:root {
  --ink: #172033;
  --muted: #5f6b82;
  --line: rgba(99, 102, 241, 0.16);
  --panel: rgba(255, 255, 255, 0.76);
  --panel-solid: #ffffff;
  --accent: #4f46e5;
  --accent-2: #0891b2;
}

.gradio-container {
  background:
    radial-gradient(circle at 8% 0%, rgba(129, 140, 248, 0.18), transparent 34rem),
    radial-gradient(circle at 94% 8%, rgba(34, 211, 238, 0.16), transparent 30rem),
    #f7f8fc;
}

.dark .gradio-container {
  color: var(--body-text-color);
  background:
    radial-gradient(circle at 8% 0%, rgba(79, 70, 229, 0.24), transparent 34rem),
    radial-gradient(circle at 94% 8%, rgba(8, 145, 178, 0.18), transparent 30rem),
    #0b1020;
}

#col-container {
  max-width: 1180px;
  margin: 0 auto;
}

main, .gradio-container, .gradio-container .fillable {
  max-width: 1240px;
}

.hero {
  position: relative;
  overflow: hidden;
  padding: 30px 32px 26px;
  margin: 10px 0 18px;
  border: 1px solid var(--line);
  border-radius: 22px;
  background: linear-gradient(135deg, rgba(255,255,255,.92), rgba(245,247,255,.74));
  box-shadow: 0 18px 60px rgba(49, 46, 129, 0.09);
}

.dark .hero {
  background: linear-gradient(135deg, rgba(17,24,39,.92), rgba(19,27,51,.78));
  box-shadow: 0 18px 60px rgba(0, 0, 0, 0.28);
}

.hero::after {
  content: "";
  position: absolute;
  width: 280px;
  height: 280px;
  right: -92px;
  top: -120px;
  border-radius: 999px;
  background: conic-gradient(from 120deg, rgba(79,70,229,.28), rgba(8,145,178,.08), rgba(79,70,229,.28));
  filter: blur(2px);
}

.eyebrow {
  position: relative;
  z-index: 1;
  margin: 0 0 10px;
  font-size: 12px;
  font-weight: 750;
  letter-spacing: .16em;
  color: var(--accent);
  text-transform: uppercase;
}

.hero h1 {
  position: relative;
  z-index: 1;
  margin: 0;
  max-width: 780px;
  font-size: clamp(30px, 5vw, 48px);
  line-height: 1.02;
  letter-spacing: -0.045em;
  color: var(--ink);
}

.dark .hero h1 { color: #f8fafc; }

.hero h1 span {
  background: linear-gradient(90deg, #4f46e5, #0891b2 64%, #0d9488);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.hero p {
  position: relative;
  z-index: 1;
  max-width: 760px;
  margin: 14px 0 18px;
  font-size: 15px;
  line-height: 1.65;
  color: var(--muted);
}

.dark .hero p { color: #b8c2d9; }

.badges {
  position: relative;
  z-index: 1;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 6px 10px;
  border: 1px solid rgba(79, 70, 229, 0.16);
  border-radius: 999px;
  background: rgba(255,255,255,.7);
  color: #3f4b63;
  font-size: 12px;
  font-weight: 650;
}

.dark .badge {
  border-color: rgba(165, 180, 252, .18);
  background: rgba(30, 41, 59, .66);
  color: #d9e1f2;
}

.panel-title {
  margin: 2px 0 8px;
  font-size: 13px;
  font-weight: 780;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
}

.panel-subtitle {
  margin: -2px 0 12px;
  color: var(--muted);
  font-size: 12px;
  line-height: 1.55;
}

.run-stats {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px;
  margin: 4px 0 12px;
}

.stat {
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 13px;
  background: var(--panel);
}

.dark .stat { background: rgba(15, 23, 42, .62); }

.stat span {
  display: block;
  margin-bottom: 3px;
  font-size: 10px;
  font-weight: 720;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
}

.stat strong {
  display: block;
  font-size: 15px;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.dark .stat strong { color: #f1f5f9; }

.result-card {
  position: relative;
  padding: 15px 16px 14px;
  margin: 0 0 10px;
  border: 1px solid var(--line);
  border-radius: 15px;
  background: var(--panel);
  box-shadow: 0 7px 22px rgba(30, 41, 59, .045);
}

.dark .result-card {
  background: rgba(15, 23, 42, .66);
  box-shadow: 0 7px 22px rgba(0, 0, 0, .14);
}

.result-card.top {
  border-color: rgba(8, 145, 178, .42);
  box-shadow: 0 9px 28px rgba(8, 145, 178, .11);
}

.result-head {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-bottom: 9px;
}

.rank {
  display: inline-grid;
  place-items: center;
  width: 27px;
  height: 27px;
  border-radius: 8px;
  background: rgba(79, 70, 229, .09);
  color: var(--accent);
  font-size: 12px;
  font-weight: 820;
}

.score {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  font-weight: 760;
  color: #0e7490;
}

.dark .score { color: #67e8f9; }

.result-card p {
  margin: 0;
  color: var(--ink);
  font-size: 14px;
  line-height: 1.58;
}

.dark .result-card p { color: #e2e8f0; }

.score-track {
  height: 4px;
  margin-top: 11px;
  overflow: hidden;
  border-radius: 999px;
  background: rgba(99, 102, 241, .10);
}

.score-fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, #6366f1, #06b6d4);
}

.map-wrap {
  margin-top: 10px;
  padding: 13px 14px 9px;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--panel);
}

.dark .map-wrap { background: rgba(15, 23, 42, .62); }

.map-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 3px;
}

.map-head strong { font-size: 13px; color: var(--ink); }
.dark .map-head strong { color: #f1f5f9; }
.map-head span { font-size: 11px; color: var(--muted); }

.empty-state {
  padding: 70px 28px;
  text-align: center;
  border: 1px dashed rgba(99, 102, 241, .28);
  border-radius: 18px;
  background: rgba(255,255,255,.42);
}

.dark .empty-state { background: rgba(15, 23, 42, .38); }

.empty-icon {
  display: grid;
  place-items: center;
  width: 50px;
  height: 50px;
  margin: 0 auto 13px;
  border-radius: 15px;
  background: linear-gradient(135deg, rgba(99,102,241,.16), rgba(6,182,212,.14));
  font-size: 23px;
}

.empty-state strong {
  display: block;
  margin-bottom: 6px;
  font-size: 16px;
  color: var(--ink);
}

.dark .empty-state strong { color: #f1f5f9; }
.empty-state p { margin: 0; color: var(--muted); font-size: 13px; }

.footer-note {
  margin-top: 18px;
  text-align: center;
  color: var(--muted);
  font-size: 12px;
}

.footer-note a { color: var(--accent); text-decoration: none; font-weight: 650; }

@media (max-width: 720px) {
  .hero { padding: 23px 20px 20px; border-radius: 17px; }
  .run-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .empty-state { padding: 44px 18px; }
}
"""

HERO = """
<section class="hero">
  <div class="eyebrow">Multilingual semantic retrieval</div>
  <h1>Search by <span>meaning</span>, not keywords.</h1>
  <p>
    Paste your own passages, describe what you are looking for, and rank every
    passage with Qwen3-Embedding-8B. Try a cross-lingual query or inspect how
    adjustable embedding dimensions change the geometry.
  </p>
  <div class="badges">
    <span class="badge">8B parameters</span>
    <span class="badge">4,096 dimensions</span>
    <span class="badge">100+ languages</span>
    <span class="badge">32K context</span>
    <span class="badge">ZeroGPU</span>
  </div>
</section>
"""

RESULTS_PLACEHOLDER = """
<div class="empty-state">
  <div class="empty-icon">🧭</div>
  <strong>Your semantic search results will appear here</strong>
  <p>Enter a query and run the search. The default collection includes English, Chinese, and technical text.</p>
</div>
"""

MAP_PLACEHOLDER = ""
STATUS_PLACEHOLDER = ""


def _split_passages(corpus: str) -> list[str]:
    text = (corpus or "").strip()
    if not text:
        return []
    return [part.strip() for part in re.split(r"\n\s*\n", text) if part.strip()]


def _fingerprint(passages: list[str]) -> str:
    payload = "\n\x1f\n".join(passages).encode("utf-8")
    return hashlib.sha256(payload).hexdigest()


def _normalize_rows(vectors: np.ndarray) -> np.ndarray:
    vectors = np.asarray(vectors, dtype=np.float32)
    norms = np.linalg.norm(vectors, axis=-1, keepdims=True)
    return vectors / np.maximum(norms, 1e-12)


def _truncate(vectors: np.ndarray, dims: int) -> np.ndarray:
    return _normalize_rows(np.asarray(vectors, dtype=np.float32)[..., :dims])


def _empty_outputs(message: str):
    notice = f"""
    <div class="empty-state">
      <div class="empty-icon">💡</div>
      <strong>{html.escape(message)}</strong>
      <p>Adjust the input and run the search again.</p>
    </div>
    """
    return notice, MAP_PLACEHOLDER, STATUS_PLACEHOLDER, None


def _score_color(score: float) -> str:
    if score >= 0.68:
        return "#0891b2"
    if score >= 0.48:
        return "#d97706"
    return "#94a3b8"


def _render_results(passages: list[str], scores: np.ndarray, top_k: int) -> str:
    order = np.argsort(-scores)[:top_k]
    cards = []
    for rank, idx in enumerate(order, start=1):
        score = float(scores[idx])
        snippet = passages[int(idx)]
        if len(snippet) > 420:
            snippet = snippet[:417].rstrip() + "…"
        cards.append(
            f"""
            <article class="result-card{' top' if rank == 1 else ''}">
              <div class="result-head">
                <span class="rank">{rank}</span>
                <span style="font-size:12px;color:#64748b;font-weight:650;">Passage {int(idx) + 1}</span>
                <span class="score">{score:.3f}</span>
              </div>
              <p>{html.escape(snippet)}</p>
              <div class="score-track">
                <span class="score-fill" style="width:{max(2.0, min(100.0, score * 100)):.1f}%"></span>
              </div>
            </article>
            """
        )
    return "".join(cards)


def _render_map(
    doc_vectors: np.ndarray,
    query_vector: np.ndarray,
    scores: np.ndarray,
    passages: list[str],
) -> str:
    if len(doc_vectors) < 2:
        return ""

    matrix = np.vstack([doc_vectors, query_vector[None, :]]).astype(np.float64)
    matrix -= matrix.mean(axis=0, keepdims=True)

    try:
        _, singular_values, vt = np.linalg.svd(matrix, full_matrices=False)
        coords = matrix @ vt[:2].T
        if coords.shape[1] == 1:
            coords = np.column_stack([coords[:, 0], np.zeros(len(coords))])
    except np.linalg.LinAlgError:
        coords = matrix[:, :2]

    x = coords[:, 0]
    y = coords[:, 1]
    x_span = max(float(x.max() - x.min()), 1e-9)
    y_span = max(float(y.max() - y.min()), 1e-9)
    px = 54 + (x - x.min()) / x_span * 532
    py = 210 - (y - y.min()) / y_span * 140

    doc_points = px[:-1]
    doc_y = py[:-1]
    query_x, query_y = float(px[-1]), float(py[-1])
    best_idx = int(np.argmax(scores))

    links = (
        f'<line x1="{query_x:.1f}" y1="{query_y:.1f}" '
        f'x2="{doc_points[best_idx]:.1f}" y2="{doc_y[best_idx]:.1f}" '
        'stroke="#a5b4fc" stroke-width="1.4" stroke-dasharray="4 4" opacity=".8"/>'
    )

    doc_nodes = []
    for i, (cx, cy, score) in enumerate(zip(doc_points, doc_y, scores), start=1):
        color = _score_color(float(score))
        tip = html.escape(
            f"Passage {i} · similarity {float(score):.3f}\n{passages[i - 1][:160]}"
        )
        doc_nodes.append(
            f'<g><title>{tip}</title>'
            f'<circle cx="{cx:.1f}" cy="{cy:.1f}" r="11" fill="{color}" opacity=".92"/>'
            f'<text x="{cx:.1f}" y="{cy + 3.6:.1f}" text-anchor="middle" '
            f'font-size="9.5" font-weight="700" fill="white">{i}</text></g>'
        )

    return f"""
    <div class="map-wrap">
      <div class="map-head">
        <strong>Semantic map</strong>
        <span>PCA projection · position shows meaning, color shows similarity</span>
      </div>
      <svg viewBox="0 0 640 250" width="100%" role="img" aria-label="Two-dimensional PCA projection of the query and passages">
        <defs>
          <linearGradient id="mapBg" x1="0" y1="0" x2="1" y2="1">
            <stop offset="0%" stop-color="#6366f1" stop-opacity=".06"/>
            <stop offset="100%" stop-color="#06b6d4" stop-opacity=".03"/>
          </linearGradient>
          <filter id="glow" x="-80%" y="-80%" width="260%" height="260%">
            <feGaussianBlur stdDeviation="5" result="blur"/>
          </filter>
        </defs>
        <rect x="0" y="0" width="640" height="226" rx="12" fill="url(#mapBg)"/>
        <g opacity=".22" stroke="#94a3b8" stroke-width=".65">
          <line x1="30" y1="45" x2="610" y2="45"/>
          <line x1="30" y1="115" x2="610" y2="115"/>
          <line x1="30" y1="185" x2="610" y2="185"/>
          <line x1="170" y1="22" x2="170" y2="208"/>
          <line x1="320" y1="22" x2="320" y2="208"/>
          <line x1="470" y1="22" x2="470" y2="208"/>
        </g>
        {links}
        {''.join(doc_nodes)}
        <circle cx="{query_x:.1f}" cy="{query_y:.1f}" r="19" fill="#8b5cf6" opacity=".2" filter="url(#glow)"/>
        <circle cx="{query_x:.1f}" cy="{query_y:.1f}" r="10" fill="#7c3aed" stroke="white" stroke-width="2"/>
        <text x="{query_x:.1f}" y="{query_y + 3.5:.1f}" text-anchor="middle" font-size="9" font-weight="800" fill="white">Q</text>
        <g transform="translate(34, 232)">
          <circle cx="4" cy="4" r="4" fill="#7c3aed"/>
          <text x="13" y="7.5" font-size="10.5" fill="#64748b">query</text>
          <circle cx="68" cy="4" r="4" fill="#0891b2"/>
          <text x="77" y="7.5" font-size="10.5" fill="#64748b">strong match</text>
          <circle cx="164" cy="4" r="4" fill="#d97706"/>
          <text x="173" y="7.5" font-size="10.5" fill="#64748b">partial match</text>
          <circle cx="264" cy="4" r="4" fill="#94a3b8"/>
          <text x="273" y="7.5" font-size="10.5" fill="#64748b">weak match</text>
        </g>
      </svg>
    </div>
    """


def _render_stats(
    dims: int,
    passage_count: int,
    top_k: int,
    elapsed: float,
    cache_hit: bool,
) -> str:
    cache_label = "reused" if cache_hit else f"{passage_count} encoded"
    return f"""
    <div class="run-stats">
      <div class="stat"><span>Embedding</span><strong>{dims} dims</strong></div>
      <div class="stat"><span>Corpus</span><strong>{passage_count} passages</strong></div>
      <div class="stat"><span>Shown</span><strong>top {top_k}</strong></div>
      <div class="stat"><span>Latency</span><strong>{elapsed:.2f}s · {cache_label}</strong></div>
    </div>
    """


model = SentenceTransformer(
    MODEL_ID,
    device="cpu",
    model_kwargs={
        "dtype": torch.bfloat16,
        "attn_implementation": "sdpa",
    },
)
model.max_seq_length = MAX_SEQ_LENGTH
model = model.to("cuda")


@spaces.GPU(duration=60)
def semantic_search(
    query: str,
    corpus: str,
    instruction: str,
    dims: int,
    top_k: int,
    cache: dict | None,
):
    """Rank passages by semantic similarity to a query.

    Args:
        query: The search question or phrase.
        corpus: Passages separated by blank lines, up to 16 passages.
        instruction: Task instruction used to condition the query embedding.
        dims: Output embedding dimensions, from 256 to 4096.
        top_k: Number of ranked passages to show.
        cache: Internal per-session cache of corpus embeddings.

    Returns:
        Ranked result cards, a PCA semantic map, run statistics, and the cache.
    """
    query = (query or "").strip()
    if not query:
        return _empty_outputs("Add a query to search")

    passages = _split_passages(corpus)
    if not passages:
        return _empty_outputs("Add at least one passage")
    if len(passages) > MAX_DOCS:
        return _empty_outputs(f"Use at most {MAX_DOCS} passages")

    dims = int(dims)
    top_k = max(1, min(int(top_k), len(passages)))
    if dims not in {256, 512, 1024, 2048, 4096}:
        dims = DEFAULT_DIMS

    started = time.perf_counter()
    fingerprint = _fingerprint(passages)
    cache_hit = bool(
        cache
        and cache.get("fingerprint") == fingerprint
        and isinstance(cache.get("embeddings"), np.ndarray)
        and cache["embeddings"].shape == (len(passages), FULL_DIM)
    )

    if cache_hit:
        document_embeddings = cache["embeddings"]
    else:
        document_embeddings = model.encode(
            passages,
            batch_size=4,
            convert_to_numpy=True,
            normalize_embeddings=True,
            show_progress_bar=False,
        ).astype(np.float32)

    instruction = (instruction or "").strip()
    query_text = (
        f"Instruct: {instruction}\nQuery:{query}" if instruction else query
    )
    query_embedding = model.encode(
        [query_text],
        batch_size=1,
        convert_to_numpy=True,
        normalize_embeddings=True,
        show_progress_bar=False,
    )[0].astype(np.float32)

    document_vectors = _truncate(document_embeddings, dims)
    query_vector = _truncate(query_embedding[None, :], dims)[0]
    scores = document_vectors @ query_vector

    elapsed = time.perf_counter() - started
    results_html = _render_results(passages, scores, top_k)
    map_html = _render_map(document_vectors, query_vector, scores, passages)
    stats_html = _render_stats(dims, len(passages), top_k, elapsed, cache_hit)
    new_cache = {
        "fingerprint": fingerprint,
        "embeddings": np.ascontiguousarray(document_embeddings, dtype=np.float32),
    }
    return stats_html + results_html, map_html, "", new_cache


with gr.Blocks(
    theme=gr.themes.Citrus(),
    css=CSS,
    title=f"{MODEL_SHORT} · Semantic Search",
) as demo:
    with gr.Column(elem_id="col-container"):
        gr.HTML(HERO)

        with gr.Row(equal_height=False):
            with gr.Column(scale=5, min_width=330):
                gr.HTML('<div class="panel-title">Search setup</div>')
                query = gr.Textbox(
                    label="Query",
                    placeholder="e.g. How do I run thousands of network requests without blocking?",
                    lines=3,
                )
                corpus = gr.Textbox(
                    label="Passages",
                    info="Separate passages with a blank line. Up to 16 passages.",
                    value=DEFAULT_CORPUS,
                    lines=15,
                )
                with gr.Accordion("Advanced controls", open=False):
                    instruction = gr.Textbox(
                        label="Retrieval instruction",
                        value=DEFAULT_INSTRUCTION,
                        lines=3,
                        info="Instructions usually improve retrieval quality and are written in English.",
                    )
                    with gr.Row():
                        dims = gr.Dropdown(
                            choices=[256, 512, 1024, 2048, 4096],
                            value=DEFAULT_DIMS,
                            label="Embedding dimensions",
                        )
                        top_k = gr.Slider(
                            minimum=1,
                            maximum=10,
                            value=DEFAULT_TOP_K,
                            step=1,
                            label="Results",
                        )
                search_button = gr.Button(
                    "Search by meaning",
                    variant="primary",
                    size="lg",
                )
                gr.Examples(
                    examples=[
                        ["How can Python handle many network requests concurrently?"],
                        ["Why does Rust reject unsafe memory access at compile time?"],
                        ["哪道川菜以豆腐和花椒为主要特色？"],
                        ["How do plants turn sunlight into chemical energy?"],
                    ],
                    inputs=[query],
                    label="Try a query",
                    cache_examples=False,
                )
                cache_state = gr.State(None)

            with gr.Column(scale=7, min_width=380):
                gr.HTML('<div class="panel-title">Ranked passages</div>')
                gr.HTML(
                    '<div class="panel-subtitle">Cosine similarity after optional '
                    "dimension truncation and re-normalization.</div>"
                )
                status = gr.HTML(STATUS_PLACEHOLDER)
                results = gr.HTML(RESULTS_PLACEHOLDER)
                semantic_map = gr.HTML(MAP_PLACEHOLDER)

        gr.HTML(
            f"""
            <div class="footer-note">
              Powered by <a href="https://huggingface.co/{MODEL_ID}" target="_blank" rel="noreferrer">{MODEL_ID}</a>
              · Runs on Hugging Face ZeroGPU · No sign-in required
            </div>
            """
        )

    search_button.click(
        fn=semantic_search,
        inputs=[query, corpus, instruction, dims, top_k, cache_state],
        outputs=[results, semantic_map, status, cache_state],
        api_name="search",
    )
    query.submit(
        fn=semantic_search,
        inputs=[query, corpus, instruction, dims, top_k, cache_state],
        outputs=[results, semantic_map, status, cache_state],
        api_name="search_on_enter",
    )
    corpus.change(
        fn=lambda: None,
        inputs=None,
        outputs=cache_state,
    )

    demo.queue(default_concurrency_limit=1, max_size=24)
    demo.launch(mcp_server=True)
