feat(diag): Advanced Search mit AND/OR + mehrere Begriffe
Klappbares Panel unter dem Suchbalken — Stefan kann bis zu 3 Begriffe eingeben und mit AND/OR verknuepfen, links nach rechts ausgewertet. Backend bleibt simpel: pro Begriff einmal /memory/search-text aufgerufen, die Treffer-Set-IDs werden client-seitig per AND (intersect) oder OR (union) kombiniert. UI: - "⌃ Erweitert" Button rechts neben ✕ klappt das Panel auf - 3 Eingabefelder mit 2 Operator-Dropdowns dazwischen (UND/ODER) - "Suchen"-Button im Panel - "Felder leeren" reseted - Leere Felder werden ignoriert — sind nur 2 belegt, gibt's nur 1 Operator - Typ-Filter aus dem Hauptbalken wird mit angewandt - Info-Banner zeigt die kombinierte Suchformel zurueck Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -852,8 +852,32 @@
|
||||
<option value="pinned">📌 Nur Pinned</option>
|
||||
<option value="cold">Nur Cold</option>
|
||||
</select>
|
||||
<button class="btn secondary" onclick="toggleAdvancedSearch()" id="btn-advanced-search" style="padding:4px 8px;font-size:11px;color:#8888AA;" title="Erweiterte Suche mit AND/OR-Verknuepfungen">⌃ Erweitert</button>
|
||||
<button class="btn secondary" onclick="resetBrainFilters();loadBrainMemoryList()" style="padding:4px 8px;font-size:11px;color:#8888AA;" title="Suche + Filter zurücksetzen">✕</button>
|
||||
</div>
|
||||
<div id="brain-advanced-panel" style="display:none;margin-top:10px;padding:10px;background:#080810;border:1px solid #1E1E2E;border-radius:6px;">
|
||||
<div style="color:#8888AA;font-size:11px;margin-bottom:6px;">
|
||||
Mehrere Begriffe mit AND/OR verknuepfen — Volltext-Substring, case-insensitive, link-nach-rechts ausgewertet.
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 60px 1fr 60px 1fr;gap:6px;align-items:center;">
|
||||
<input type="text" id="adv-term-1" placeholder="z.B. flugzeug" style="background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;font-size:12px;">
|
||||
<select id="adv-op-1" style="background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;font-size:11px;">
|
||||
<option value="AND" selected>UND</option>
|
||||
<option value="OR">ODER</option>
|
||||
</select>
|
||||
<input type="text" id="adv-term-2" placeholder="z.B. cessna" style="background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;font-size:12px;">
|
||||
<select id="adv-op-2" style="background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;font-size:11px;">
|
||||
<option value="AND" selected>UND</option>
|
||||
<option value="OR">ODER</option>
|
||||
</select>
|
||||
<input type="text" id="adv-term-3" placeholder="(optional)" style="background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;font-size:12px;">
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;margin-top:8px;align-items:center;">
|
||||
<button class="btn" onclick="runAdvancedSearch()" style="padding:4px 12px;font-size:11px;">Suchen</button>
|
||||
<button class="btn secondary" onclick="clearAdvancedSearch()" style="padding:4px 10px;font-size:11px;color:#8888AA;">Felder leeren</button>
|
||||
<span style="color:#555570;font-size:10px;margin-left:auto;">Tipp: Felder mit Inhalt werden zusammengeführt — leere Felder werden ignoriert</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="brain-search-info" style="margin-top:6px;font-size:10px;color:#8888AA;display:none;"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
@@ -3451,6 +3475,98 @@
|
||||
const p = document.getElementById('brain-filter-pinned'); if (p) p.value = 'all';
|
||||
const info = document.getElementById('brain-search-info'); if (info) info.style.display = 'none';
|
||||
brainSearchIds = null;
|
||||
clearAdvancedSearch();
|
||||
}
|
||||
|
||||
function toggleAdvancedSearch() {
|
||||
const panel = document.getElementById('brain-advanced-panel');
|
||||
const btn = document.getElementById('btn-advanced-search');
|
||||
if (!panel) return;
|
||||
const open = panel.style.display !== 'none';
|
||||
panel.style.display = open ? 'none' : 'block';
|
||||
if (btn) btn.textContent = open ? '⌃ Erweitert' : '⌄ Einklappen';
|
||||
}
|
||||
|
||||
function clearAdvancedSearch() {
|
||||
['adv-term-1','adv-term-2','adv-term-3'].forEach(id => {
|
||||
const el = document.getElementById(id); if (el) el.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
/** Mehrere Volltext-Suchen + Boolean-Kombination (links nach rechts).
|
||||
* Backend bleibt simpel — wir machen N parallele search-text-Calls
|
||||
* und kombinieren die ID-Mengen client-seitig per AND/OR. */
|
||||
async function runAdvancedSearch() {
|
||||
const terms = [
|
||||
document.getElementById('adv-term-1')?.value?.trim() || '',
|
||||
document.getElementById('adv-term-2')?.value?.trim() || '',
|
||||
document.getElementById('adv-term-3')?.value?.trim() || '',
|
||||
];
|
||||
const ops = [
|
||||
document.getElementById('adv-op-1')?.value || 'AND',
|
||||
document.getElementById('adv-op-2')?.value || 'AND',
|
||||
];
|
||||
const info = document.getElementById('brain-search-info');
|
||||
// Felder mit Inhalt zusammen mit dem ops-Op DAVOR sammeln
|
||||
const active = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (terms[i]) active.push({ term: terms[i], op: i === 0 ? null : ops[i - 1] });
|
||||
}
|
||||
if (active.length === 0) {
|
||||
if (info) info.style.display = 'none';
|
||||
loadBrainMemoryList();
|
||||
return;
|
||||
}
|
||||
|
||||
const typeFilter = document.getElementById('brain-filter-type').value;
|
||||
const baseParams = { k: '500', include_pinned: 'true' };
|
||||
if (typeFilter) baseParams.type = typeFilter;
|
||||
|
||||
try {
|
||||
// Pro Begriff einmal Backend fragen, dann Map<id, memory> + Set<id>
|
||||
const sets = [];
|
||||
for (const a of active) {
|
||||
const params = new URLSearchParams({ ...baseParams, q: a.term });
|
||||
const r = await fetch('/api/brain/memory/search-text?' + params.toString());
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
const hits = await r.json();
|
||||
hits.forEach(m => { brainMemoryCache[m.id] = m; });
|
||||
sets.push(new Set(hits.map(m => m.id)));
|
||||
}
|
||||
|
||||
// Links-nach-rechts kombinieren mit den Operatoren
|
||||
let combined = sets[0];
|
||||
for (let i = 1; i < sets.length; i++) {
|
||||
const op = active[i].op;
|
||||
if (op === 'AND') {
|
||||
combined = new Set([...combined].filter(id => sets[i].has(id)));
|
||||
} else {
|
||||
combined = new Set([...combined, ...sets[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
const hits = Array.from(combined).map(id => brainMemoryCache[id]).filter(Boolean);
|
||||
brainSearchIds = hits.map(m => m.id);
|
||||
const desc = active.map((a, i) => i === 0 ? `"${a.term}"` : ` ${a.op} "${a.term}"`).join('');
|
||||
if (info) {
|
||||
info.style.display = 'block';
|
||||
if (hits.length === 0) {
|
||||
info.innerHTML = `🔍 Keine Treffer fuer ${escapeHtml(desc)}` +
|
||||
(typeFilter ? ` · Typ=${escapeHtml(typeFilter)}` : '') +
|
||||
` · 📝 wortlich, Boolean-Kombi`;
|
||||
} else {
|
||||
info.innerHTML = `🔍 ${hits.length} Treffer fuer ${escapeHtml(desc)}` +
|
||||
(typeFilter ? ` · Typ=${escapeHtml(typeFilter)}` : '') +
|
||||
` · 📝 wortlich, Boolean-Kombi`;
|
||||
}
|
||||
}
|
||||
renderBrainList(hits, true);
|
||||
} catch (e) {
|
||||
if (info) {
|
||||
info.style.display = 'block';
|
||||
info.innerHTML = `🔴 Erweiterte Suche fehlgeschlagen: ${escapeHtml(e.message)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runBrainSearch() {
|
||||
|
||||
Reference in New Issue
Block a user