Skip to content

Commit 5b32db9

Browse files
committed
fix(frontend): filter invalid/dedup PPT pages in search + pagination + domain filter
- Search SQL now filters ppt_pages by ocr_status='done' so invalid/dedup pages don't appear as "OCR命中" but then vanish in the PPT viewer - Pagination: 50 results per page with "加载更多" load-more button - Search domain checkboxes (摘要/标题/转录/OCR) in course filter dropdown let users narrow which fields to search
1 parent 898a831 commit 5b32db9

3 files changed

Lines changed: 122 additions & 30 deletions

File tree

frontend/index.html

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,33 @@ <h4 class="font-medium text-gray-800 text-sm truncate" x-text="lec.sub_title ||
430430
<input x-model="searchCourseFilterQuery" type="text"
431431
placeholder="搜索课程名…"
432432
class="w-full border-b px-3 py-2 text-sm sticky top-0 bg-white focus:outline-none">
433+
<div class="border-b px-3 py-2 space-y-1">
434+
<p class="text-xs text-gray-400 mb-1">搜索范围</p>
435+
<label class="flex items-center gap-2 text-sm cursor-pointer">
436+
<input type="checkbox" :checked="searchDomains.summary"
437+
@change="toggleSearchDomain('summary')"
438+
class="rounded border-gray-300 text-blue-600">
439+
<span>摘要</span>
440+
</label>
441+
<label class="flex items-center gap-2 text-sm cursor-pointer">
442+
<input type="checkbox" :checked="searchDomains.sub_title"
443+
@change="toggleSearchDomain('sub_title')"
444+
class="rounded border-gray-300 text-blue-600">
445+
<span>标题</span>
446+
</label>
447+
<label class="flex items-center gap-2 text-sm cursor-pointer">
448+
<input type="checkbox" :checked="searchDomains.transcript"
449+
@change="toggleSearchDomain('transcript')"
450+
class="rounded border-gray-300 text-blue-600">
451+
<span>转录</span>
452+
</label>
453+
<label class="flex items-center gap-2 text-sm cursor-pointer">
454+
<input type="checkbox" :checked="searchDomains.ocr"
455+
@change="toggleSearchDomain('ocr')"
456+
class="rounded border-gray-300 text-blue-600">
457+
<span>OCR</span>
458+
</label>
459+
</div>
433460
<template x-for="c in searchCourseOptionsFiltered" :key="c.course_id">
434461
<label class="flex items-center gap-2 px-3 py-1.5 hover:bg-gray-50 text-sm cursor-pointer">
435462
<input type="checkbox" :checked="searchSelectedCourseIds.includes(String(c.course_id))"
@@ -467,6 +494,12 @@ <h4 class="font-medium text-gray-800 text-sm mt-0.5" x-text="r.sub_title || 'Unt
467494
</button>
468495
</template>
469496
</div>
497+
<div x-show="searchHasMore" class="text-center mt-2">
498+
<button @click="loadMore()"
499+
class="px-4 py-2 text-sm text-blue-600 border border-blue-200 rounded-lg hover:bg-blue-50">
500+
加载更多
501+
</button>
502+
</div>
470503
</div>
471504

472505
<!-- ─── SUBSCRIPTIONS (three-column editor) ─── -->

frontend/js/app.js

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,8 @@ document.addEventListener("alpine:init", () => {
254254
searchQuery: "", searchResults: [],
255255
searchCourseFilterQuery: "", searchSelectedCourseIds: [],
256256
searchCourseOpen: false,
257+
searchPage: 1, searchHasMore: false,
258+
searchDomains: { summary: true, sub_title: true, transcript: true, ocr: true },
257259
commitSha: null,
258260
setup: { token: "", stuid: "", uispsw: "" },
259261
setupError: "", setupTesting: false,
@@ -582,11 +584,36 @@ document.addEventListener("alpine:init", () => {
582584
doSearch() {
583585
clearTimeout(this._searchTimeout);
584586
this._searchTimeout = setTimeout(() => {
585-
this.searchResults = this.searchQuery.trim()
586-
? ICS.db.searchSummaries(this.searchQuery, this.searchSelectedCourseIds)
587-
: [];
587+
this._loadSearchResults(1);
588588
}, 300);
589589
},
590+
_loadSearchResults(page) {
591+
if (!this.searchQuery.trim()) {
592+
this.searchResults = [];
593+
this.searchHasMore = false;
594+
this.searchPage = 1;
595+
return;
596+
}
597+
var result = ICS.db.searchSummaries(
598+
this.searchQuery, this.searchSelectedCourseIds,
599+
page, 50, this.searchDomains,
600+
);
601+
if (page <= 1) {
602+
this.searchResults = result.results;
603+
} else {
604+
[].push.apply(this.searchResults, result.results);
605+
}
606+
this.searchPage = result.page;
607+
this.searchHasMore = result.hasMore;
608+
},
609+
loadMore() {
610+
this._loadSearchResults(this.searchPage + 1);
611+
},
612+
toggleSearchDomain(domain) {
613+
this.searchDomains = Object.assign({}, this.searchDomains);
614+
this.searchDomains[domain] = !this.searchDomains[domain];
615+
this.doSearch();
616+
},
590617

591618
async refresh() {
592619
const c = _loadCreds();

frontend/js/db.js

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -164,43 +164,75 @@ function _getPptPages(subId) {
164164
`, [subId]);
165165
}
166166

167-
function _searchSummaries(query, courseIds) {
168-
if (!query?.trim()) return [];
169-
// Match against summary, transcript, and sub_title. Transcript is the
170-
// most useful for "I remember the teacher said X" lookups; summary
171-
// covers "I read it in the notes"; sub_title covers session names.
172-
// We mark which field hit so the UI can show the right snippet.
167+
function _searchSummaries(query, courseIds, page, pageSize, domains) {
168+
if (!query?.trim()) return { results: [], page: 1, hasMore: false };
169+
page = page || 1;
170+
pageSize = pageSize || 50;
171+
const offset = (page - 1) * pageSize;
173172
const q = query;
174-
var textMatchClause = `
175-
l.summary LIKE '%' || ? || '%'
176-
OR l.sub_title LIKE '%' || ? || '%'
177-
OR l.transcript LIKE '%' || ? || '%'
178-
OR EXISTS(SELECT 1 FROM ppt_pages pp3 WHERE pp3.sub_id = l.sub_id AND pp3.text LIKE '%' || ? || '%')
179-
`;
180-
var whereClauses = ["(" + textMatchClause + ")"];
181-
// Params order matches the SQL placeholder order:
182-
// 1 (ppt_text) + 4 (CASE hit_field) + 4 (WHERE text match)
183-
var params = [q, q, q, q, q, q, q, q, q];
173+
174+
// Domain flags — default all enabled
175+
const d = domains || {};
176+
const matchSummary = d.summary !== false;
177+
const matchSubtitle = d.sub_title !== false;
178+
const matchTranscript = d.transcript !== false;
179+
const matchOcr = d.ocr !== false;
180+
181+
// Build WHERE parts per active domain
182+
var textParts = [];
183+
var textParams = [];
184+
function addText(cond) { textParts.push(cond); textParams.push(q); }
185+
if (matchSummary) addText("l.summary LIKE '%' || ? || '%'");
186+
if (matchSubtitle) addText("l.sub_title LIKE '%' || ? || '%'");
187+
if (matchTranscript) addText("l.transcript LIKE '%' || ? || '%'");
188+
if (matchOcr) addText("EXISTS(SELECT 1 FROM ppt_pages pp3 WHERE pp3.sub_id = l.sub_id AND pp3.ocr_status = 'done' AND pp3.text LIKE '%' || ? || '%')");
189+
190+
if (!textParts.length) return { results: [], page: 1, hasMore: false };
191+
192+
// Build hit_field CASE for active domains
193+
var caseParts = [];
194+
var caseParams = [];
195+
function addCase(when, then) { caseParts.push("WHEN " + when + " THEN '" + then + "'"); caseParams.push(q); }
196+
if (matchSummary) addCase("l.summary LIKE '%' || ? || '%'", "summary");
197+
if (matchSubtitle) addCase("l.sub_title LIKE '%' || ? || '%'", "sub_title");
198+
if (matchTranscript) addCase("l.transcript LIKE '%' || ? || '%'", "transcript");
199+
if (matchOcr) addCase("EXISTS(SELECT 1 FROM ppt_pages pp2 WHERE pp2.sub_id = l.sub_id AND pp2.ocr_status = 'done' AND pp2.text LIKE '%' || ? || '%')", "ocr");
200+
201+
// ppt_text subquery (for OCR snippet, only when OCR domain active)
202+
var pptSql = matchOcr
203+
? "(SELECT pp.text FROM ppt_pages pp WHERE pp.sub_id = l.sub_id AND pp.ocr_status = 'done' AND pp.text LIKE '%' || ? || '%' LIMIT 1)"
204+
: "NULL";
205+
var pptParams = matchOcr ? [q] : [];
206+
207+
// Full params: ppt_text + CASE + WHERE
208+
var params = pptParams.concat(caseParams, textParams);
209+
210+
// WHERE clause with optional course filter
211+
var whereClauses = ["(" + textParts.join("\n OR ") + ")"];
184212
if (courseIds && courseIds.length) {
185213
var placeholders = courseIds.map(function () { return "?"; }).join(",");
186214
whereClauses.push("l.course_id IN (" + placeholders + ")");
187215
courseIds.forEach(function (id) { params.push(String(id)); });
188216
}
189-
return _queryAll(`
217+
218+
var caseSql = caseParts.length
219+
? "CASE\n " + caseParts.join("\n ") + "\n ELSE 'other'\n END"
220+
: "'other'";
221+
222+
// Fetch pageSize+1 rows to detect whether a next page exists
223+
var rows = _queryAll(`
190224
SELECT l.sub_id, l.sub_title, l.summary, l.transcript,
191-
(SELECT pp.text FROM ppt_pages pp WHERE pp.sub_id = l.sub_id AND pp.text LIKE '%' || ? || '%' LIMIT 1) AS ppt_text,
225+
${pptSql} AS ppt_text,
192226
l.course_id, c.title AS course_title,
193-
CASE
194-
WHEN l.summary LIKE '%' || ? || '%' THEN 'summary'
195-
WHEN l.sub_title LIKE '%' || ? || '%' THEN 'sub_title'
196-
WHEN l.transcript LIKE '%' || ? || '%' THEN 'transcript'
197-
WHEN EXISTS(SELECT 1 FROM ppt_pages pp2 WHERE pp2.sub_id = l.sub_id AND pp2.text LIKE '%' || ? || '%') THEN 'ocr'
198-
ELSE 'other'
199-
END AS hit_field
227+
${caseSql} AS hit_field
200228
FROM lectures l JOIN courses c ON l.course_id = c.course_id
201229
WHERE ` + whereClauses.join(" AND ") + `
202-
ORDER BY l.processed_at DESC LIMIT 50
203-
`, params);
230+
ORDER BY l.processed_at DESC LIMIT ? OFFSET ?
231+
`, params.concat([pageSize + 1, offset]));
232+
233+
var hasMore = rows.length > pageSize;
234+
if (hasMore) rows.pop();
235+
return { results: rows, page: page, hasMore: hasMore };
204236
}
205237

206238
function _getAllCourses(term) {

0 commit comments

Comments
 (0)