import { createReducer, isAnyOf } from '@reduxjs/toolkit'; import { ReducerWithInitialState } from '@reduxjs/toolkit/dist/createReducer'; import { WritableDraft } from 'immer/dist/internal'; import { Tab, updateWindowsAsync, WindowsState } from '../main/features/windows/windowsSlice'; import { activated, attached, created, detached, highlighted, moved, removed, replaced, updated, } from '../main/features/windows/windowsSlice'; import { rootInitialState, RootState } from './rootState'; import { AutoTag, db } from '../db/db'; export const autotagReducer: ReducerWithInitialState = createReducer( rootInitialState, (builder) => { builder.addCase(updateWindowsAsync.fulfilled, (state, action) => { updateAllOpenerLinks(state, action.payload); }); builder.addMatcher(isAnyOf(activated, created, updated), (state, action) => { const tab = action.payload.tab; updateOpenerLinks(state, tab); return state; }); builder.addMatcher( isAnyOf( // activated, created, // highlighted, updated ), (state, action) => { const url = action.payload.tab.url; const page = url ? state.pages.pages[url] : undefined; page && autotagPropagate(state, page); return state; } ); builder.addMatcher( isAnyOf( // activated, attached, detached, moved, removed ), (state, action) => { const url = state.windows.tabs[action.payload.tabId]?.url; const page = url ? state.pages.pages[url] : undefined; page && autotagPropagate(state, page); return state; } ); builder.addMatcher(isAnyOf(replaced), (state, action) => { const url = state.windows.tabs[action.payload.addedTabId]?.url; const page = url ? state.pages.pages[url] : undefined; page && autotagPropagate(state, page); return state; }); builder.addMatcher(isAnyOf(addTag, removeTag, setTags), (state, action) => { const urls = action.payload.urls; for (const url of urls.filter(Boolean)) { const page = url ? state.pages.pages[url] : undefined; page && autotagPropagate(state, page); } return state; }); } ); async function updateAllOpenerLinks(state: WindowsState, windows: chrome.windows.Window[]) { for (const window of windows) { // fire and forget a bunch of async tasks // window?.tabs?.forEach((tab) => updateOpenerLinks(state, tab)); // No, do them in order for (const tab of window?.tabs || []) { await updateOpenerLinks(state, tab); } } } async function updateOpenerLinks(state: WindowsState, chromeTab: chrome.tabs.Tab) { if (tab && tab.url && tab.openerTabId) { const page = await db.pages.get(tab.url); // state.pages.pages[tab.url]; const openerUrl = state.tabs[tab.openerTabId]?.url; if (openerUrl) { const openerPage = await db.pages.get(openerUrl); if (page && openerUrl && !page.openerUrls.includes(openerUrl)) { page.openerUrls.push(openerUrl); await db.pages.put(page); } if (openerPage && !openerPage.openedUrls.includes(tab.url)) { openerPage.openedUrls.push(tab.url); await db.pages.put(openerPage); } } } } export async function autotagPropagate(state: WindowsState, url: String) { const page = await db.pages.get(url); if (!page) { return; } const neighbors = [ page.url, ...page.openerUrls, ...page.openedUrls, ...windowNeighbors(state, page.url), ]; for (const url of neighbors) { await autotagOne(state, url); } } function windowNeighbors(state: WindowsState, url: string) { const focalTabs = Object.values(state.tabs).filter((tab) => tab.url === url); const focalWindowIds = focalTabs.map((tab) => tab.windowId).filter(Boolean) as number[]; const windowNeighborUrls = focalWindowIds .map((windowId) => state.windows[windowId]) .flatMap((window) => window?.tabIds) .map((tabId) => state.tabs[tabId]) .map((tab) => tab?.url) .filter(Boolean) as string[]; return new Set(windowNeighborUrls); } export async function autotagOne(state: WindowsState, url: string) { const page = await db.pages.get(url); if (!page || page.disableTagPropagation) { return; } const autotags: AutoTag[] = []; const counts: { [key: string]: number } = {}; let total = 0; const neighbors = [...page.openerUrls, ...page.openedUrls, ...windowNeighbors(state, page.url)]; for (const neighbor of neighbors) { for (const tag of (await db.pages.get(neighbor))?.humanTags || []) { counts[tag] = counts[tag] ? counts[tag] + 1 : 1; total++; } // could penalize humanRejectedTags here } Object.entries(counts) .sort((x) => -x[1]) .forEach(([tag, count]) => { const strength = count / total; console.log(`${page.url} ${tag} ${count} ${total} ${strength}`); if (strength > 0.1) { if (!page.humanTags.includes(tag) && !page.humanRejectedTags.includes(tag)) { autotags.push({ name: tag, inferenceStrength: strength }); } } }); await db.pages.update(page.url, { autoTags: autotags }); }