| | import streamlit as st |
| | import requests |
| | import json |
| | import os |
| | from pathlib import Path |
| |
|
| | |
| | st.set_page_config( |
| | page_title="Code Search", |
| | page_icon="⌕", |
| | layout="wide", |
| | initial_sidebar_state="expanded", |
| | ) |
| |
|
| | |
| | st.markdown(""" |
| | <style> |
| | @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600&family=Space+Grotesk:wght@300;400;500;600;700&display=swap'); |
| | |
| | /* ── Root theme ── */ |
| | :root { |
| | --bg: #0d1117; |
| | --bg2: #161b22; |
| | --bg3: #21262d; |
| | --border: #30363d; |
| | --accent: #58a6ff; |
| | --accent2: #3fb950; |
| | --accent3: #f78166; |
| | --accent4: #d2a8ff; |
| | --text: #e6edf3; |
| | --text2: #8b949e; |
| | --text3: #484f58; |
| | --mono: 'JetBrains Mono', monospace; |
| | --sans: 'Space Grotesk', sans-serif; |
| | } |
| | |
| | /* ── Global resets ── */ |
| | html, body, [class*="css"] { |
| | font-family: var(--sans) !important; |
| | background-color: var(--bg) !important; |
| | color: var(--text) !important; |
| | } |
| | .stApp { background-color: var(--bg) !important; } |
| | |
| | /* ── Sidebar ── */ |
| | [data-testid="stSidebar"] { |
| | background-color: var(--bg2) !important; |
| | border-right: 1px solid var(--border) !important; |
| | } |
| | [data-testid="stSidebar"] * { color: var(--text) !important; } |
| | |
| | /* ── Inputs ── */ |
| | .stTextInput input, .stTextArea textarea, .stSelectbox select, |
| | [data-testid="stTextInput"] input, [data-baseweb="input"] input { |
| | background-color: var(--bg3) !important; |
| | border: 1px solid var(--border) !important; |
| | border-radius: 6px !important; |
| | color: var(--text) !important; |
| | font-family: var(--mono) !important; |
| | font-size: 13px !important; |
| | } |
| | .stTextInput input:focus, .stTextArea textarea:focus { |
| | border-color: var(--accent) !important; |
| | box-shadow: 0 0 0 3px rgba(88,166,255,0.1) !important; |
| | } |
| | |
| | /* ── Buttons ── */ |
| | .stButton > button { |
| | background-color: var(--accent) !important; |
| | color: #0d1117 !important; |
| | border: none !important; |
| | border-radius: 6px !important; |
| | font-family: var(--mono) !important; |
| | font-weight: 600 !important; |
| | font-size: 13px !important; |
| | padding: 8px 20px !important; |
| | transition: all 0.2s !important; |
| | } |
| | .stButton > button:hover { |
| | background-color: #79b8ff !important; |
| | transform: translateY(-1px) !important; |
| | box-shadow: 0 4px 12px rgba(88,166,255,0.3) !important; |
| | } |
| | .stButton > button[kind="secondary"] { |
| | background-color: var(--bg3) !important; |
| | color: var(--text) !important; |
| | border: 1px solid var(--border) !important; |
| | } |
| | .stButton > button[kind="secondary"]:hover { |
| | border-color: var(--accent3) !important; |
| | color: var(--accent3) !important; |
| | background-color: rgba(247,129,102,0.1) !important; |
| | transform: none !important; |
| | box-shadow: none !important; |
| | } |
| | |
| | /* ── Tabs ── */ |
| | .stTabs [data-baseweb="tab-list"] { |
| | background-color: var(--bg2) !important; |
| | border-bottom: 1px solid var(--border) !important; |
| | gap: 0 !important; |
| | } |
| | .stTabs [data-baseweb="tab"] { |
| | background-color: transparent !important; |
| | color: var(--text2) !important; |
| | font-family: var(--mono) !important; |
| | font-size: 13px !important; |
| | padding: 12px 20px !important; |
| | border-radius: 0 !important; |
| | border-bottom: 2px solid transparent !important; |
| | } |
| | .stTabs [aria-selected="true"] { |
| | background-color: transparent !important; |
| | color: var(--accent) !important; |
| | border-bottom: 2px solid var(--accent) !important; |
| | } |
| | .stTabs [data-baseweb="tab-panel"] { |
| | background-color: var(--bg) !important; |
| | padding-top: 24px !important; |
| | } |
| | |
| | /* ── Metric cards ── */ |
| | [data-testid="metric-container"] { |
| | background-color: var(--bg2) !important; |
| | border: 1px solid var(--border) !important; |
| | border-radius: 8px !important; |
| | padding: 16px !important; |
| | } |
| | [data-testid="metric-container"] label { |
| | color: var(--text2) !important; |
| | font-family: var(--mono) !important; |
| | font-size: 11px !important; |
| | text-transform: uppercase !important; |
| | letter-spacing: 0.08em !important; |
| | } |
| | [data-testid="metric-container"] [data-testid="stMetricValue"] { |
| | color: var(--accent) !important; |
| | font-family: var(--mono) !important; |
| | font-size: 28px !important; |
| | font-weight: 600 !important; |
| | } |
| | |
| | /* ── File uploader ── */ |
| | [data-testid="stFileUploader"] { |
| | background-color: var(--bg2) !important; |
| | border: 1px dashed var(--border) !important; |
| | border-radius: 8px !important; |
| | } |
| | [data-testid="stFileUploader"]:hover { |
| | border-color: var(--accent) !important; |
| | } |
| | |
| | /* ── Success / error / info boxes ── */ |
| | .stSuccess { background-color: rgba(63,185,80,0.1) !important; border-left: 3px solid var(--accent2) !important; } |
| | .stError { background-color: rgba(247,129,102,0.1) !important; border-left: 3px solid var(--accent3) !important; } |
| | .stInfo { background-color: rgba(88,166,255,0.08) !important; border-left: 3px solid var(--accent) !important; } |
| | .stWarning { background-color: rgba(210,168,255,0.1) !important; border-left: 3px solid var(--accent4) !important; } |
| | |
| | /* ── JSON / code blocks ── */ |
| | .stJson, pre, code { |
| | background-color: var(--bg2) !important; |
| | border: 1px solid var(--border) !important; |
| | border-radius: 6px !important; |
| | font-family: var(--mono) !important; |
| | font-size: 12px !important; |
| | } |
| | |
| | /* ── Divider ── */ |
| | hr { border-color: var(--border) !important; margin: 24px 0 !important; } |
| | |
| | /* ── Result cards ── */ |
| | .result-card { |
| | background: var(--bg2); |
| | border: 1px solid var(--border); |
| | border-radius: 8px; |
| | padding: 16px; |
| | margin-bottom: 12px; |
| | font-family: var(--mono); |
| | font-size: 13px; |
| | transition: border-color 0.2s; |
| | } |
| | .result-card:hover { border-color: var(--accent); } |
| | .result-rank { |
| | display: inline-block; |
| | background: var(--accent); |
| | color: #0d1117; |
| | border-radius: 4px; |
| | padding: 2px 8px; |
| | font-size: 11px; |
| | font-weight: 700; |
| | margin-bottom: 8px; |
| | } |
| | .result-score { |
| | float: right; |
| | color: var(--accent2); |
| | font-size: 12px; |
| | font-weight: 600; |
| | } |
| | .result-text { |
| | white-space: pre-wrap; |
| | color: var(--text); |
| | line-height: 1.6; |
| | margin-top: 8px; |
| | border-top: 1px solid var(--border); |
| | padding-top: 8px; |
| | } |
| | |
| | /* ── Doc table ── */ |
| | .doc-row { |
| | display: flex; |
| | align-items: center; |
| | background: var(--bg2); |
| | border: 1px solid var(--border); |
| | border-radius: 6px; |
| | padding: 10px 16px; |
| | margin-bottom: 8px; |
| | gap: 12px; |
| | font-family: var(--mono); |
| | font-size: 13px; |
| | } |
| | .doc-id { color: var(--accent); flex: 1; font-weight: 500; } |
| | .doc-chunks { color: var(--text2); font-size: 11px; } |
| | |
| | /* ── Header ── */ |
| | .page-header { |
| | display: flex; |
| | align-items: center; |
| | gap: 12px; |
| | margin-bottom: 32px; |
| | padding-bottom: 20px; |
| | border-bottom: 1px solid var(--border); |
| | } |
| | .page-header h1 { |
| | font-family: var(--mono) !important; |
| | font-size: 22px !important; |
| | font-weight: 600 !important; |
| | color: var(--text) !important; |
| | margin: 0 !important; |
| | letter-spacing: -0.02em; |
| | } |
| | .page-header .subtitle { |
| | color: var(--text2); |
| | font-size: 13px; |
| | font-family: var(--mono); |
| | margin-top: 2px; |
| | } |
| | .status-dot { |
| | width: 8px; height: 8px; |
| | border-radius: 50%; |
| | display: inline-block; |
| | margin-right: 6px; |
| | } |
| | .status-dot.online { background: #3fb950; box-shadow: 0 0 6px #3fb950; } |
| | .status-dot.offline { background: #f78166; box-shadow: 0 0 6px #f78166; } |
| | |
| | /* ── Slider ── */ |
| | [data-testid="stSlider"] .st-emotion-cache-1gv3bxi { color: var(--text2) !important; } |
| | </style> |
| | """, unsafe_allow_html=True) |
| |
|
| |
|
| | |
| | def api(method: str, path: str, **kwargs) -> requests.Response: |
| | base = st.session_state.get("api_url", "").rstrip("/") |
| | timeout = kwargs.pop("timeout", 60) |
| | return requests.request(method, f"{base}{path}", timeout=timeout, **kwargs) |
| |
|
| |
|
| | def health_check(url: str) -> bool: |
| | try: |
| | r = requests.get(url.rstrip("/") + "/health", timeout=5) |
| | return r.ok and r.json().get("models_loaded", False) |
| | except Exception: |
| | return False |
| |
|
| |
|
| | def render_result_card(r: dict): |
| | st.markdown(f""" |
| | <div class="result-card"> |
| | <span class="result-rank">#{r['rank']}</span> |
| | <span class="result-score">score {r['score']:.4f}</span> |
| | <div class="result-text">{r['text']}</div> |
| | </div> |
| | """, unsafe_allow_html=True) |
| |
|
| |
|
| | |
| | with st.sidebar: |
| | st.markdown("### ⌕ Code Search") |
| | st.markdown("---") |
| |
|
| | api_url = st.text_input( |
| | "API URL", |
| | value=st.session_state.get("api_url", os.getenv("API_URL", "")), |
| | placeholder="https://your-space.hf.space", |
| | help="Your Code Search API HuggingFace Space URL", |
| | ) |
| | st.session_state["api_url"] = api_url |
| |
|
| | if api_url: |
| | is_up = health_check(api_url) |
| | dot = "online" if is_up else "offline" |
| | label = "API online · models loaded" if is_up else "API offline or unreachable" |
| | st.markdown( |
| | f'<span class="status-dot {dot}"></span>' |
| | f'<span style="font-family:var(--mono);font-size:12px;color:var(--text2)">{label}</span>', |
| | unsafe_allow_html=True, |
| | ) |
| |
|
| | st.markdown("---") |
| | st.markdown( |
| | '<span style="font-family:var(--mono);font-size:11px;color:var(--text3)">' |
| | "jina-embeddings-v2-base-code<br>FAISS · AST chunking · /data persistence" |
| | "</span>", |
| | unsafe_allow_html=True, |
| | ) |
| |
|
| |
|
| | |
| | st.markdown(""" |
| | <div class="page-header"> |
| | <div> |
| | <h1>⌕ Code Search</h1> |
| | <div class="subtitle">semantic code search · powered by jina-embeddings-v2-base-code</div> |
| | </div> |
| | </div> |
| | """, unsafe_allow_html=True) |
| |
|
| | if not api_url: |
| | st.info("👈 Enter your API URL in the sidebar to get started.") |
| | st.stop() |
| |
|
| |
|
| | |
| | t_search, t_index, t_batch, t_docs, t_embed = st.tabs([ |
| | "🔍 Search", |
| | "📄 Index File", |
| | "📦 Batch Index", |
| | "🗂 Documents", |
| | "🧮 Embed", |
| | ]) |
| |
|
| |
|
| | |
| | |
| | |
| | with t_search: |
| | st.markdown("#### Search an indexed codebase") |
| |
|
| | |
| | doc_ids = [] |
| | try: |
| | docs_resp = api("GET", "/documents", timeout=10) |
| | if docs_resp.ok: |
| | doc_ids = [d["doc_id"] for d in docs_resp.json().get("documents", [])] |
| | except Exception: |
| | pass |
| |
|
| | col1, col2 = st.columns([2, 1]) |
| | with col1: |
| | if doc_ids: |
| | doc_id = st.selectbox("Document / Project ID", options=doc_ids) |
| | else: |
| | doc_id = st.text_input("Document / Project ID", placeholder="my_project") |
| | with col2: |
| | top_k = st.slider("Top K results", min_value=1, max_value=20, value=5) |
| |
|
| | query = st.text_area( |
| | "Query", |
| | placeholder="e.g. fetch user from database\nor: async function that handles authentication", |
| | height=80, |
| | ) |
| |
|
| | if st.button("Search ⌕", use_container_width=True): |
| | if not query.strip(): |
| | st.warning("Enter a query.") |
| | elif not doc_id: |
| | st.warning("Enter a document ID.") |
| | else: |
| | with st.spinner("Searching…"): |
| | try: |
| | r = api("POST", "/search", json={ |
| | "doc_id": doc_id, |
| | "query": query.strip(), |
| | "top_k": top_k, |
| | }) |
| | if r.ok: |
| | data = r.json() |
| | results = data.get("results", []) |
| | st.markdown(f"**{len(results)} results** for `{query[:60]}`") |
| | st.markdown("---") |
| | for res in results: |
| | render_result_card(res) |
| | else: |
| | st.error(f"API error {r.status_code}: {r.text}") |
| | except Exception as e: |
| | st.error(f"Request failed: {e}") |
| |
|
| |
|
| | |
| | |
| | |
| | with t_index: |
| | st.markdown("#### Index a single source file") |
| |
|
| | uploaded = st.file_uploader( |
| | "Upload file", |
| | type=["py","js","ts","tsx","jsx","go","rs","java","cpp","c","cs","rb","php","md","txt"], |
| | help="Supported: Python, JS, TS, Go, Rust, Java, C/C++, C#, Ruby, PHP, Markdown, text", |
| | ) |
| |
|
| | col1, col2 = st.columns(2) |
| | with col1: |
| | custom_id = st.text_input( |
| | "Custom doc_id (optional)", |
| | placeholder="Leave blank to use filename", |
| | ) |
| |
|
| | if uploaded: |
| | st.markdown( |
| | f'<div class="doc-row"><span class="doc-id">{uploaded.name}</span>' |
| | f'<span class="doc-chunks">{uploaded.size:,} bytes</span></div>', |
| | unsafe_allow_html=True, |
| | ) |
| |
|
| | if st.button("Index File →", use_container_width=True): |
| | if not uploaded: |
| | st.warning("Upload a file first.") |
| | else: |
| | with st.spinner(f"Indexing `{uploaded.name}`…"): |
| | try: |
| | files = {"file": (uploaded.name, uploaded.getvalue(), "text/plain")} |
| | data = {"doc_id": custom_id.strip()} |
| | r = api("POST", "/index", files=files, data=data, timeout=120) |
| | if r.ok: |
| | d = r.json() |
| | c1, c2, c3 = st.columns(3) |
| | c1.metric("doc_id", d["doc_id"]) |
| | c2.metric("Chunks", d["chunks_indexed"]) |
| | c3.metric("Status", "✓ indexed") |
| | st.success(d["message"]) |
| | else: |
| | st.error(f"API error {r.status_code}: {r.text}") |
| | except Exception as e: |
| | st.error(f"Request failed: {e}") |
| |
|
| |
|
| | |
| | |
| | |
| | with t_batch: |
| | st.markdown("#### Batch index an entire project") |
| | st.markdown( |
| | '<span style="font-family:var(--mono);font-size:12px;color:var(--text2)">' |
| | "Upload multiple files — they will all be indexed under one shared doc_id.</span>", |
| | unsafe_allow_html=True, |
| | ) |
| |
|
| | batch_files = st.file_uploader( |
| | "Upload files", |
| | accept_multiple_files=True, |
| | type=["py","js","ts","tsx","jsx","go","rs","java","cpp","c","cs","rb","php","md","txt"], |
| | key="batch_uploader", |
| | ) |
| |
|
| | col1, col2 = st.columns([2, 1]) |
| | with col1: |
| | batch_id = st.text_input("Project doc_id", placeholder="my_project", key="batch_id") |
| | with col2: |
| | replace = st.checkbox("Replace existing index", value=True) |
| |
|
| | if batch_files: |
| | st.markdown(f"**{len(batch_files)} file(s) queued:**") |
| | for f in batch_files: |
| | st.markdown( |
| | f'<div class="doc-row">' |
| | f'<span class="doc-id">{f.name}</span>' |
| | f'<span class="doc-chunks">{f.size:,} bytes</span>' |
| | f'</div>', |
| | unsafe_allow_html=True, |
| | ) |
| |
|
| | if st.button("Batch Index →", use_container_width=True, key="batch_btn"): |
| | if not batch_files: |
| | st.warning("Upload at least one file.") |
| | elif not batch_id.strip(): |
| | st.warning("Enter a project doc_id.") |
| | else: |
| | payload = { |
| | "doc_id": batch_id.strip(), |
| | "replace": replace, |
| | "files": [ |
| | {"filename": f.name, "content": f.getvalue().decode("utf-8", errors="replace")} |
| | for f in batch_files |
| | ], |
| | } |
| | with st.spinner(f"Indexing {len(batch_files)} files into `{batch_id}`…"): |
| | try: |
| | r = api("POST", "/index/batch", json=payload, timeout=300) |
| | if r.ok: |
| | d = r.json() |
| | c1, c2, c3 = st.columns(3) |
| | c1.metric("doc_id", d["doc_id"]) |
| | c2.metric("Files indexed", d["files_indexed"]) |
| | c3.metric("Chunks indexed", d["chunks_indexed"]) |
| | st.success("Batch index complete!") |
| | else: |
| | st.error(f"API error {r.status_code}: {r.text}") |
| | except Exception as e: |
| | st.error(f"Request failed: {e}") |
| |
|
| |
|
| | |
| | |
| | |
| | with t_docs: |
| | st.markdown("#### Indexed documents") |
| |
|
| | col_refresh, _ = st.columns([1, 4]) |
| | with col_refresh: |
| | refresh = st.button("↻ Refresh", key="refresh_docs") |
| |
|
| | try: |
| | r = api("GET", "/documents", timeout=10) |
| | if r.ok: |
| | docs = r.json().get("documents", []) |
| | if not docs: |
| | st.info("No documents indexed yet. Use the Index or Batch Index tabs.") |
| | else: |
| | total_chunks = sum(d["chunks"] for d in docs) |
| | m1, m2 = st.columns(2) |
| | m1.metric("Documents", len(docs)) |
| | m2.metric("Total chunks", total_chunks) |
| | st.markdown("---") |
| |
|
| | for doc in docs: |
| | col_info, col_del = st.columns([5, 1]) |
| | with col_info: |
| | st.markdown( |
| | f'<div class="doc-row">' |
| | f'<span class="doc-id">📁 {doc["doc_id"]}</span>' |
| | f'<span class="doc-chunks">{doc["chunks"]:,} chunks</span>' |
| | f'</div>', |
| | unsafe_allow_html=True, |
| | ) |
| | with col_del: |
| | if st.button("Delete", key=f"del_{doc['doc_id']}", type="secondary"): |
| | try: |
| | dr = api("DELETE", f"/documents/{doc['doc_id']}", timeout=10) |
| | if dr.ok: |
| | st.success(f"Deleted `{doc['doc_id']}`") |
| | st.rerun() |
| | else: |
| | st.error(dr.text) |
| | except Exception as e: |
| | st.error(str(e)) |
| | else: |
| | st.error(f"API error {r.status_code}: {r.text}") |
| | except Exception as e: |
| | st.error(f"Could not reach API: {e}") |
| |
|
| |
|
| | |
| | |
| | |
| | with t_embed: |
| | st.markdown("#### Embed arbitrary texts") |
| | st.markdown( |
| | '<span style="font-family:var(--mono);font-size:12px;color:var(--text2)">' |
| | "Returns raw 768-dim float vectors. One text per line.</span>", |
| | unsafe_allow_html=True, |
| | ) |
| |
|
| | raw_texts = st.text_area( |
| | "Texts (one per line)", |
| | placeholder="def getUserById(id):\n return db.query(User).filter(User.id == id).first()\nfetch user from database", |
| | height=160, |
| | ) |
| |
|
| | if st.button("Embed →", use_container_width=True): |
| | texts = [t.strip() for t in raw_texts.strip().splitlines() if t.strip()] |
| | if not texts: |
| | st.warning("Enter at least one text.") |
| | elif len(texts) > 64: |
| | st.warning("Maximum 64 texts per request.") |
| | else: |
| | with st.spinner(f"Embedding {len(texts)} text(s)…"): |
| | try: |
| | r = api("POST", "/embed", json={"texts": texts}, timeout=60) |
| | if r.ok: |
| | d = r.json() |
| | embs = d["embeddings"] |
| | st.metric("Dimensions", d["dimensions"]) |
| | st.markdown(f"**{len(embs)} embedding(s) returned**") |
| | st.markdown("---") |
| | for i, (txt, vec) in enumerate(zip(texts, embs)): |
| | with st.expander(f"[{i}] `{txt[:60]}{'…' if len(txt)>60 else ''}`"): |
| | preview = vec[:16] |
| | st.markdown( |
| | f'<code style="font-size:11px;line-height:1.8">' |
| | f'[{", ".join(f"{v:.5f}" for v in preview)}, …]<br>' |
| | f'<span style="color:var(--text3)">dim 768 · showing first 16</span>' |
| | f'</code>', |
| | unsafe_allow_html=True, |
| | ) |
| | if st.toggle("Show full vector", key=f"full_{i}"): |
| | st.json(vec) |
| | else: |
| | st.error(f"API error {r.status_code}: {r.text}") |
| | except Exception as e: |
| | st.error(f"Request failed: {e}") |