UI Híbrida Oráculo de EL — Caja de Búsqueda del Workspace
PR #51 · 2026-05-19 · Cierra Fase 1 del plan Oráculo
Archivos:src/components/shell/SearchInline.tsx,src/styles/globals.css
Qué es
La caja de búsqueda global del Workspace (SearchInline) evoluciona de un buscador fuzzy local a un sistema híbrido que combina dos modos de consulta:
| Modo | Motor | Latencia | Cuándo se activa |
|---|---|---|---|
| Search | Fuse.js (local, /search-index.json) | <10 ms | Queries cortas, sin interrogación |
| Oracle | POST /api/oraculo/ask → Gemma 4 + grafo Bibliotecario | ~1-3 s | Queries que parecen preguntas |
La heurística de detección automática (shouldAskOracle) evalúa en modo auto (por defecto): si la query tiene ≥4 palabras, termina en ?/¿, o empieza por palabra interrogativa (qué/cómo/dónde/why/what…). Umbral mínimo de 8 chars para no disparar Gemma 4 con términos cortos.
Componentes de la feature
Detección híbrida (shouldAskOracle)
function shouldAskOracle(query: string, mode: SearchMode): boolean {
if (mode === 'search') return false;
const q = query.trim();
if (q.length < ORACLE_MIN_CHARS) return false; // 8 chars
if (mode === 'oracle') return true;
// auto: heurística
if (q.endsWith('?') || q.endsWith('¿')) return true;
if (q.split(/\s+/).length >= 4) return true;
if (INTERROGATIVE_RE.test(q)) return true;
return false;
}
Toggle manual de modo
Tres chips en el header del dropdown:
- Auto (default) — el sistema decide según la heurística.
- Buscar — solo Fuse, nunca Oráculo.
- Preguntar — fuerza Oráculo cuando hay query.
Debounce + AbortController
ORACLE_DEBOUNCE_MS = 450. Cada keystroke cancela el request anterior via AbortController. Evita tormenta de llamadas a Gemma 4 mientras el usuario sigue escribiendo. Los AbortError se descartan silenciosamente.
Render markdown
Usa marked (GFM habilitado, breaks: false) para renderizar la respuesta del Oráculo como HTML. Sin DOMPurify: el corpus es interno y la respuesta viene constrained por JSON schema. Mismo patrón que NoteEditor del workspace.
Chips de fuentes clickables
sourceToHref(path) mapea rutas del grafo Bibliotecario a URLs navegables:
src/content/wiki/<slug>.md→/wiki/<slug>(chip con link)- Otros paths → chip informativo sin link
UI/UX
- Caja ampliada de 240 px → 360 px
min-widthpara invitar a preguntas largas. - Loader inline: “El Oráculo está pensando…”
- Mensaje de error user-friendly si el endpoint falla.
- Metadata de respuesta: nº de fuentes y duración en ms.
Estilos nuevos (globals.css)
14 clases nuevas para la sección Oráculo + 5 clases de refactor de inline styles preexistentes:
| Grupo | Clases |
|---|---|
| Toggle de modo | .search-inline-mode-toggle, .search-inline-mode-chip, .search-inline-mode-chip.active |
| Respuesta Oráculo | .search-inline-oracle, .search-inline-oracle-answer, .search-inline-oracle-loading, .search-inline-oracle-error, .search-inline-oracle-meta |
| Fuentes | .search-inline-oracle-sources, .search-inline-oracle-chip, .search-inline-oracle-chip.linkable |
| Refactor | .search-inline-result-main, .search-inline-result-title, .search-inline-result-path, .search-inline-result-enter, .search-inline-history-term, .search-inline-empty |
Todos los estilos usan tokens CSS existentes (--overlay-3, --accent, --text-sm, --fg-dim, --border-soft, --danger, --font-mono). Sin valores hardcodeados salvo el fallback #2962ff del --accent.
Flujo de datos
Usuario escribe query
│
▼
shouldAskOracle(query, mode)?
├─ No → Fuse.js (local) → lista de páginas
└─ Sí → debounce 450ms
│
▼
AbortController nuevo
│
▼
POST /api/oraculo/ask
{ question: query }
│
▼
OracleResponse {
answer: string, ← marked.parse() → HTML
sources: [{title, path, relevance}],
model: "gemma-4-...",
chunks_used: N,
duration_ms: N
}
│
▼
Render: respuesta MD + chips de fuentes
Estado del plan Oráculo
| Fase | Descripción | PR | Estado |
|---|---|---|---|
| Fase 1 | Endpoint /api/oraculo/ask (backend RAG) | #50 | ✅ |
| Fase 1 | UI híbrida caja de búsqueda | #51 | ✅ |
| Fase 2 | TBD | — | pendiente |
Decisiones técnicas
- Sin DOMPurify: decisión consciente. El HTML renderizado viene de Gemma 4 sobre corpus propio, constrained por schema. Si el corpus se abriera a contenido externo, revaluar.
- marked sin sanitize: ver nota anterior. Consistente con
NoteEditor. - Debounce 450 ms: balance entre respuesta ágil y no saturar Gemma 4. Ajustable en
ORACLE_DEBOUNCE_MS. - Min 8 chars: evita disparar el LLM por búsquedas como “racks” o “ip” que son claramente fuzzy.
Véase también
- [[entity—workspace—component—search-inline]]
- [[feature—workspace—oraculo-endpoint]]