Expand Doris Kitchen import, failures, and tag curation

This commit is contained in:
Fizzlepoof
2026-05-17 04:07:05 +00:00
parent e2470ff7c9
commit 165e772946
18 changed files with 1268 additions and 80 deletions

View File

@@ -51,6 +51,41 @@ function renderSuggestionCards(target, items) {
}).join('');
}
function renderFailureCards(target, items) {
if (!target) return;
if (!items.length) {
target.textContent = 'No failed searches logged.';
target.classList.add('muted');
return;
}
target.classList.remove('muted');
target.innerHTML = items.map((item) => {
const status = String(item.status || 'open');
const resolved = status === 'resolved-with-recipes';
const titles = (item.sample_titles || []).map((entry) => '<li>' + escapeHtml(entry) + '</li>').join('');
const candidateQueries = (item.candidate_queries || []).map((entry) => '<span>' + escapeHtml(entry) + '</span>').join('');
return `
<article class="failure-card" data-failure-id="${escapeHtml(item.id)}">
<div class="suggestion-top">
<div>
<p class="section-label">Logged query</p>
<h3>${escapeHtml(item.query)}</h3>
</div>
<p class="pill ${resolved ? 'status-resolved' : 'status-open'}">${escapeHtml(status)}</p>
</div>
<p class="muted">Logged: ${escapeHtml(item.created_at || '')}</p>
<div class="chip-list">${candidateQueries || '<span>No candidate queries captured</span>'}</div>
<p class="muted failure-count">Current matching recipes: ${escapeHtml(item.result_count || 0)}</p>
<ul class="reason-list failure-title-list">${titles || '<li>No recovered recipe titles yet.</li>'}</ul>
<div class="action-row">
<button type="button" class="ghost" data-failure-open="${escapeHtml(item.id)}">View recipes</button>
</div>
<div class="failure-results stack-form muted" data-failure-results="${escapeHtml(item.id)}">Click <strong>View recipes</strong> to load the current recipe matches for this search.</div>
</article>
`;
}).join('');
}
async function loadDashboard() {
const data = await fetchJson('/api/dashboard/summary');
const counts = data.counts || {};
@@ -64,6 +99,7 @@ async function loadDashboard() {
setText('count-saved', counts.saved || 0);
setText('count-rejected', counts.rejected || 0);
setText('count-plans', counts.plans || 0);
setText('count-failed-searches', counts.failed_searches || 0);
setText('backend-note', 'Storage backend: ' + data.backend);
const blacklist = document.getElementById('blacklist-items');
@@ -85,6 +121,7 @@ async function loadDashboard() {
if (enabled) enabled.checked = Boolean(profile.auto_import_enabled);
renderSuggestionCards(document.getElementById('suggestion-list'), data.recent_suggestions || []);
renderFailureCards(document.getElementById('failed-search-preview'), data.recent_failed_searches || []);
}
async function handleDecision(event) {
@@ -94,17 +131,39 @@ async function handleDecision(event) {
const suggestionId = card && card.dataset ? card.dataset.suggestionId : '';
const decision = button.dataset.decision;
if (!suggestionId || !decision) return;
const failurePanel = card ? card.closest('[data-failure-results]') : null;
button.disabled = true;
try {
await fetchJson('/api/suggestions/' + encodeURIComponent(suggestionId) + '/decision', {
const data = await fetchJson('/api/suggestions/' + encodeURIComponent(suggestionId) + '/decision', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({decision, import_to_kitchenowl: decision === 'approve'}),
});
await loadDashboard();
if (failurePanel && card) {
card.remove();
const remaining = failurePanel.querySelectorAll('[data-suggestion-id]');
if (!remaining.length) {
failurePanel.innerHTML = '<p class="muted">No actionable recipes left in this review set.</p>';
failurePanel.classList.remove('muted');
}
const failureCard = failurePanel.closest('[data-failure-id]');
if (failureCard) {
const count = failureCard.querySelector('.failure-count');
if (count) {
const currentText = count.textContent || '';
const match = currentText.match(/(\d+)/);
if (match) {
const nextCount = Math.max(0, Number(match[1]) - 1);
count.textContent = 'Current matching recipes: ' + String(nextCount);
}
}
}
}
if (document.getElementById('search-results')) {
await loadSuggestionsInto(document.getElementById('search-results'));
}
return data;
} catch (error) {
alert(error.message);
} finally {
@@ -117,12 +176,70 @@ async function loadSuggestionsInto(target) {
renderSuggestionCards(target, data.items || []);
}
async function loadFailuresInto(target) {
const data = await fetchJson('/api/failed-searches');
renderFailureCards(target, data.items || []);
}
async function loadFailureResults(failureId, target, button) {
if (!target || !failureId) return;
if (button) {
button.disabled = true;
button.textContent = 'Loading…';
}
target.textContent = 'Loading recipes for this search…';
target.classList.add('muted');
try {
const data = await fetchJson('/api/failed-searches/' + encodeURIComponent(failureId) + '/results');
const results = data.results || [];
if (!results.length) {
target.innerHTML = '<p class="muted">Still no usable recipes for this search.</p>';
target.classList.remove('muted');
} else {
renderSuggestionCards(target, results);
}
const card = target.closest('[data-failure-id]');
if (card && data.item) {
const badge = card.querySelector('.pill');
const count = card.querySelector('.failure-count');
const list = card.querySelector('.failure-title-list');
if (badge) {
const resolved = String(data.item.status || '') === 'resolved-with-recipes';
badge.textContent = String(data.item.status || 'open');
badge.className = 'pill ' + (resolved ? 'status-resolved' : 'status-open');
}
if (count) {
count.textContent = 'Current matching recipes: ' + String(data.item.result_count || 0);
}
if (list) {
const titles = (data.item.sample_titles || []).map((entry) => '<li>' + escapeHtml(entry) + '</li>').join('');
list.innerHTML = titles || '<li>No recovered recipe titles yet.</li>';
}
}
} catch (error) {
target.textContent = error.message;
} finally {
if (button) {
button.disabled = false;
button.textContent = 'View recipes';
}
}
}
function wireDashboard() {
document.addEventListener('click', async (event) => {
if (event.target.matches('button[data-decision]')) {
await handleDecision(event);
return;
}
if (event.target.matches('button[data-failure-open]')) {
const button = event.target;
const failureId = button.dataset.failureOpen;
const card = button.closest('[data-failure-id]');
const target = card ? card.querySelector('[data-failure-results]') : null;
await loadFailureResults(failureId, target, button);
return;
}
if (event.target.matches('button[data-blacklist-name]')) {
const name = event.target.dataset.blacklistName;
const enabled = event.target.dataset.blacklistEnabled !== '1';
@@ -188,22 +305,84 @@ function wireDashboard() {
function wireSearch() {
const form = document.getElementById('search-form');
if (!form) return;
form.addEventListener('submit', async (event) => {
event.preventDefault();
const query = document.getElementById('search-query').value.trim();
if (!query) return;
const urlForm = document.getElementById('url-form');
const queryInput = document.getElementById('search-query');
const submitSearch = async (query) => {
const target = document.getElementById('search-results');
const status = document.getElementById('search-status');
if (status) status.textContent = '';
target.textContent = 'Searching and normalizing recipes…';
target.classList.add('muted');
const data = await fetchJson('/api/search', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({query, limit: 10}),
});
if (status && data.needs_attention) {
status.textContent = 'No recipes came back for that search. Doris logged it as a search failure for troubleshooting.';
} else if (status) {
status.textContent = '';
}
renderSuggestionCards(target, data.results || []);
};
const submitUrl = async (url) => {
const target = document.getElementById('search-results');
const status = document.getElementById('search-status');
if (status) status.textContent = '';
target.textContent = 'Fetching and formatting that recipe link…';
target.classList.add('muted');
const data = await fetchJson('/api/search/url', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({url}),
});
if (status) {
status.textContent = 'Recipe link ingested and queued for review.';
}
renderSuggestionCards(target, data.suggestion ? [data.suggestion] : []);
};
document.querySelectorAll('.example-query').forEach((button) => {
button.addEventListener('click', async () => {
const query = button.dataset.query || '';
if (queryInput) {
queryInput.value = query;
}
try {
await submitSearch(query);
} catch (error) {
const target = document.getElementById('search-results');
const status = document.getElementById('search-status');
if (status) status.textContent = 'Search failed.';
target.textContent = error.message;
}
});
});
if (form) {
form.addEventListener('submit', async (event) => {
event.preventDefault();
const query = queryInput ? queryInput.value.trim() : '';
if (!query) return;
try {
await submitSearch(query);
} catch (error) {
const target = document.getElementById('search-results');
const status = document.getElementById('search-status');
if (status) status.textContent = 'Search failed.';
target.textContent = error.message;
}
});
}
if (!urlForm) return;
urlForm.addEventListener('submit', async (event) => {
event.preventDefault();
const url = document.getElementById('recipe-url').value.trim();
if (!url) return;
try {
const data = await fetchJson('/api/search', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({query, limit: 6}),
});
renderSuggestionCards(target, data.results || []);
await submitUrl(url);
} catch (error) {
const target = document.getElementById('search-results');
const status = document.getElementById('search-status');
if (status) status.textContent = 'Recipe link import failed.';
target.textContent = error.message;
}
});
@@ -256,10 +435,34 @@ function wirePlanner() {
});
}
function wireFailures() {
const target = document.getElementById('failures-list');
if (target) {
loadFailuresInto(target).catch(() => {
target.textContent = 'Could not load failed searches.';
});
}
const refresh = document.getElementById('refresh-failures');
if (refresh && target) {
refresh.addEventListener('click', async () => {
refresh.disabled = true;
refresh.textContent = 'Refreshing…';
try {
const data = await fetchJson('/api/failed-searches/refresh', {method: 'POST'});
renderFailureCards(target, data.items || []);
} finally {
refresh.disabled = false;
refresh.textContent = 'Refresh progress';
}
});
}
}
loadDashboard().catch(() => {});
wireDashboard();
wireSearch();
wirePlanner();
wireFailures();
if (document.getElementById('search-results')) {
loadSuggestionsInto(document.getElementById('search-results')).catch(() => {});
}