import React, { useState, useEffect, useCallback } from 'react'; import { useLiveQuery } from 'dexie-react-hooks'; import { CssBaseline, CssVarsProvider, Sheet, Typography, Stack, Divider, } from '@mui/joy'; import { db } from '../db/db'; import { addTag, removeTag } from '../db/pages'; import { fetchTagNames } from '../db/tags'; import { normalizeUrl } from '../chrome/normalizeUrl'; import { shouldIgnore } from '../db/urlNormalizationPrefs'; import { loadPrefsFromDB } from '../db/prefsLoader'; import { CustomTagSelector } from '../main/tagSelector/CustomTagSelector'; import { TagAttachment } from '../main/tagSelector/tag_utils'; import { SystemTags } from '../db/constants'; import '@fontsource/inter'; import './Sidebar.less'; // Initialize prefs so that shouldIgnore works correctly loadPrefsFromDB(); const Sidebar = () => { const [currentUrl, setCurrentUrl] = useState(null); const [tabTitle, setTabTitle] = useState(undefined); const [tabFavIcon, setTabFavIcon] = useState(undefined); const updateCurrentTab = useCallback(() => { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { const tab = tabs[0]; if (tab?.url) { try { const nurl = normalizeUrl(tab.url); if (!shouldIgnore(nurl.normalized)) { setCurrentUrl(nurl.normalized); setTabTitle(tab.title); setTabFavIcon(tab.favIconUrl ?? undefined); return; } } catch { // ignore un-normalizable URLs } } setCurrentUrl(null); setTabTitle(undefined); setTabFavIcon(undefined); }); }, []); useEffect(() => { updateCurrentTab(); const handleActivated = () => updateCurrentTab(); const handleUpdated = ( _tabId: number, changeInfo: chrome.tabs.TabChangeInfo ) => { if (changeInfo.url !== undefined || changeInfo.title !== undefined) { updateCurrentTab(); } }; chrome.tabs.onActivated.addListener(handleActivated); chrome.tabs.onUpdated.addListener(handleUpdated); return () => { chrome.tabs.onActivated.removeListener(handleActivated); chrome.tabs.onUpdated.removeListener(handleUpdated); }; }, [updateCurrentTab]); // Fetch the tracked page from IndexedDB const page = useLiveQuery( () => (currentUrl ? db.pages.get(currentUrl) : undefined), [currentUrl] ); // All tag names for the add-tag autocomplete list const tagOptions = (useLiveQuery(fetchTagNames) ?? []) as string[]; // Applied human tags (excluding system tags like #stash, #flag, etc.) const appliedTags: TagAttachment[] = (page?.humanTags ?? []) .filter((t) => !SystemTags.includes(t)) .map((t) => ({ name: t, humanEdit: true, inferenceStrength: 1, proportion: 1 })); // Suggested tags: aggregate tags from URL-graph adjacent pages that aren't already applied const suggestedTags = useLiveQuery(async () => { if (!page) return []; const appliedSet = new Set(page.humanTags ?? []); const adjacentUrls = [ ...(page.openerUrls ?? []), ...(page.openedUrls ?? []), ...(page.timeAdjacentUrls ?? []), ]; if (adjacentUrls.length === 0) return []; const adjacentPages = await db.pages.bulkGet(adjacentUrls); const tagCounts: Record = {}; for (const p of adjacentPages) { if (!p) continue; for (const tag of p.humanTags ?? []) { if (!appliedSet.has(tag) && !SystemTags.includes(tag)) { tagCounts[tag] = (tagCounts[tag] || 0) + 1; } } } return Object.entries(tagCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([name, count]) => ({ name, humanEdit: true, inferenceStrength: 1, proportion: Math.min(1, count / adjacentUrls.length), })); }, [page]); const handleAdd = (tag: string) => { if (tag && currentUrl) addTag([currentUrl], tag); }; const handleRemove = (tag: string) => { if (tag && currentUrl) removeTag([currentUrl], tag); }; const noop = () => {}; const displayTitle = tabTitle ?? page?.title ?? currentUrl ?? ''; return ( {/* Header */} Tabvana {currentUrl ? ( {/* Page info */} {tabFavIcon && ( )} {displayTitle} {/* Applied tags */} APPLIED TAGS {/* Suggested tags (from URL-graph neighbors) */} {(suggestedTags?.length ?? 0) > 0 && ( <> SUGGESTED TAGS )} ) : ( No trackable page is active. )} ); }; export default Sidebar;