Add NextGen library: index DB, thumbnails, AI culling, and CLIP search
Builds the "indexed library" foundation and first intelligent features on top of the organizer (sql.js index, non-destructive in-place indexing). Phase 0 — Library index: - sql.js (WASM SQLite) index DB; contentHash-keyed assets, resumable indexing (skip by path+mtime), batch persistence (chosen over native better-sqlite3 which fails to build on Node 24 / Python 3.12) - Library folders (in place, non-destructive) + background indexer w/ progress - Thumbnails generated in the AI worker (canvas->webp), cached in userData; served via photoai-media://thumb by hash; thumbnail grid w/ pagination Phase 1 — AI quality assessment & culling: - Focus (Laplacian variance), exposure (histogram), eyes-open (face-api EAR) computed in one analyze pass alongside the thumbnail - Culling filters (candidate/rejected) + quality badges - Adjustable thresholds (live SQL re-classification from stored raw scores, no re-analysis) + manual star rating (0-5) and color labels (usermeta) Phase 2 — CLIP natural-language / similarity search: - @huggingface/transformers (WASM/WebGPU, no native build) - CLIP image/text embeddings (lazy-loaded); Korean queries auto-translated via opus-mt-ko-en into the English CLIP - Embeddings stored as SQLite BLOBs; "build search index" batch w/ progress; brute-force cosine search; new Search tab - Note: models download from HF Hub on first use; fully-offline ORT-wasm packaging and KO search-accuracy tuning are follow-ups Tabs added (Organize / Library / Search). All typecheck/tests(12)/build green; boot smoke verified across phases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+59
-18
@@ -8,10 +8,15 @@ import { RunControl } from './components/RunControl'
|
||||
import { ProgressView } from './components/ProgressView'
|
||||
import { FileList } from './components/FileList'
|
||||
import { ReportView } from './components/ReportView'
|
||||
import { LibraryView } from './components/LibraryView'
|
||||
import { SearchView } from './components/SearchView'
|
||||
import type { AppView } from './store'
|
||||
|
||||
export default function App(): JSX.Element {
|
||||
const t = useT()
|
||||
const phase = useStore((s) => s.phase)
|
||||
const view = useStore((s) => s.view)
|
||||
const setView = useStore((s) => s.setView)
|
||||
const onboarded = useStore((s) => s.onboarded)
|
||||
const refreshProfiles = useStore((s) => s.refreshProfiles)
|
||||
const initSettings = useStore((s) => s.initSettings)
|
||||
@@ -28,29 +33,65 @@ export default function App(): JSX.Element {
|
||||
if (!ready) return <div className="h-full" />
|
||||
if (!onboarded) return <Onboarding />
|
||||
|
||||
const tabs: { id: AppView; label: string }[] = [
|
||||
{ id: 'organize', label: t('nav.organize') },
|
||||
{ id: 'library', label: t('nav.library') },
|
||||
{ id: 'search', label: t('nav.search') }
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<header className="px-6 py-4 bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 shadow-sm shrink-0">
|
||||
<h1 className="text-xl font-bold text-brand-dark dark:text-brand">{t('app.title')}</h1>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">{t('app.subtitle')}</p>
|
||||
<header className="px-6 pt-4 bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 shadow-sm shrink-0">
|
||||
<div className="flex items-end justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-brand-dark dark:text-brand">{t('app.title')}</h1>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">{t('app.subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* 탭 네비 */}
|
||||
<nav className="flex gap-1 mt-3 -mb-px">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setView(tab.id)}
|
||||
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||
view === tab.id
|
||||
? 'border-brand text-brand'
|
||||
: 'border-transparent text-slate-500 dark:text-slate-400 hover:text-brand'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 min-h-0 grid grid-cols-12 gap-4 p-6">
|
||||
{/* 좌측: 설정 패널 (자체 스크롤) */}
|
||||
<section className="col-span-5 min-h-0 flex flex-col gap-4 overflow-y-auto pr-2">
|
||||
<ProfileManager />
|
||||
<FolderPicker />
|
||||
<RunControl />
|
||||
</section>
|
||||
{view === 'organize' ? (
|
||||
<main className="flex-1 min-h-0 grid grid-cols-12 gap-4 p-6">
|
||||
{/* 좌측: 설정 패널 (자체 스크롤) */}
|
||||
<section className="col-span-5 min-h-0 flex flex-col gap-4 overflow-y-auto pr-2">
|
||||
<ProfileManager />
|
||||
<FolderPicker />
|
||||
<RunControl />
|
||||
</section>
|
||||
|
||||
{/* 우측: 진행/결과 — FileList만 내부 스크롤 */}
|
||||
<section className="col-span-7 min-h-0 flex flex-col gap-4">
|
||||
<div className="shrink-0">
|
||||
{phase === 'done' ? <ReportView /> : <ProgressView />}
|
||||
</div>
|
||||
<FileList />
|
||||
</section>
|
||||
</main>
|
||||
{/* 우측: 진행/결과 — FileList만 내부 스크롤 */}
|
||||
<section className="col-span-7 min-h-0 flex flex-col gap-4">
|
||||
<div className="shrink-0">
|
||||
{phase === 'done' ? <ReportView /> : <ProgressView />}
|
||||
</div>
|
||||
<FileList />
|
||||
</section>
|
||||
</main>
|
||||
) : view === 'library' ? (
|
||||
<main className="flex-1 min-h-0 overflow-y-auto p-6">
|
||||
<LibraryView />
|
||||
</main>
|
||||
) : (
|
||||
<main className="flex-1 min-h-0 overflow-y-auto p-6">
|
||||
<SearchView />
|
||||
</main>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user