diff --git a/sitecompanion/content.js b/sitecompanion/content.js index ef3371d..4fccf0f 100644 --- a/sitecompanion/content.js +++ b/sitecompanion/content.js @@ -596,6 +596,7 @@ async function refreshToolbar() { let refreshTimer = null; +let contentChangeTimer = null; function scheduleToolbarRefresh() { if (refreshTimer) return; refreshTimer = window.setTimeout(() => { @@ -610,9 +611,22 @@ function scheduleToolbarRefresh() { }, 200); } +function scheduleContentChangeNotice() { + if (contentChangeTimer) return; + contentChangeTimer = window.setTimeout(() => { + contentChangeTimer = null; + chrome.runtime.sendMessage({ type: "SITE_CONTENT_CHANGED" }, () => { + if (chrome.runtime.lastError) { + return; + } + }); + }, 250); +} + const observer = new MutationObserver(() => { if (suppressObserver) return; scheduleToolbarRefresh(); + scheduleContentChangeNotice(); }); observer.observe(document.documentElement, { childList: true, subtree: true }); diff --git a/sitecompanion/popup.html b/sitecompanion/popup.html index 4ff041c..3518f7c 100644 --- a/sitecompanion/popup.html +++ b/sitecompanion/popup.html @@ -77,7 +77,7 @@
Site Text: 0 chars - Task: 0 chars + Total: 0 chars Idle
diff --git a/sitecompanion/popup.js b/sitecompanion/popup.js index f114042..58ac7f5 100644 --- a/sitecompanion/popup.js +++ b/sitecompanion/popup.js @@ -57,7 +57,9 @@ const state = { selectedTaskId: "", selectedEnvId: "", selectedProfileId: "", - alwaysShowOutput: false + alwaysShowOutput: false, + activeTabId: null, + pendingConfigRefresh: false }; async function switchState(stateName) { @@ -118,6 +120,7 @@ function applyPopupDraft(draft) { } else if (typeof draft.siteTextSelector === "string") { state.siteTextTarget = { kind: "css", selector: draft.siteTextSelector }; } + updateCounts(); } function matchUrl(url, pattern) { @@ -677,6 +680,10 @@ function setAnalyzing(isAnalyzing) { updateTaskSelectState(); updateEnvSelectState(); updateProfileSelectState(); + if (!isAnalyzing && state.pendingConfigRefresh) { + state.pendingConfigRefresh = false; + scheduleConfigRefresh(); + } } function updateOutputVisibility() { @@ -686,12 +693,93 @@ function updateOutputVisibility() { outputSection.classList.toggle("hidden", shouldHide); } +function getSelectedTask() { + if (state.forcedTask) return state.forcedTask; + const selectedId = taskSelect?.value || state.selectedTaskId; + return state.tasks.find((item) => item.id === selectedId) || state.tasks[0] || null; +} + +function getSelectedProfile() { + const selectedId = profileSelect?.value || state.selectedProfileId; + return ( + state.profiles.find((item) => item.id === selectedId) || + state.profiles[0] || + null + ); +} + +function getSelectedEnv() { + const selectedId = envSelect?.value || state.selectedEnvId; + return state.envs.find((item) => item.id === selectedId) || state.envs[0] || null; +} + +function buildTotalPromptText() { + const task = getSelectedTask(); + const profile = getSelectedProfile(); + const env = getSelectedEnv(); + const systemPrompt = env?.systemPrompt || ""; + const userPrompt = buildUserMessage( + profile?.text || "", + task?.text || "", + state.siteText || "" + ); + return systemPrompt ? `${systemPrompt}\n\n${userPrompt}` : userPrompt; +} + function updateSiteTextCount() { - postingCountEl.textContent = `Site Text: ${state.siteText.length} chars`; + const length = (state.siteText || "").length; + postingCountEl.textContent = `Site Text: ${length} chars`; } function updatePromptCount(count) { - promptCountEl.textContent = `Task: ${count} chars`; + const total = + typeof count === "number" ? count : buildTotalPromptText().length; + promptCountEl.textContent = `Total: ${total} chars`; +} + +function updateCounts() { + updateSiteTextCount(); + updatePromptCount(); +} + +let siteContentRefreshTimer = null; +function scheduleSiteContentRefresh() { + if (siteContentRefreshTimer) return; + siteContentRefreshTimer = window.setTimeout(() => { + siteContentRefreshTimer = null; + void refreshSiteContentCounts(); + }, 250); +} + +let configRefreshTimer = null; +function scheduleConfigRefresh() { + if (state.isAnalyzing) { + state.pendingConfigRefresh = true; + return; + } + if (configRefreshTimer) return; + configRefreshTimer = window.setTimeout(() => { + configRefreshTimer = null; + void loadConfig(); + }, 250); +} + +async function refreshSiteContentCounts() { + if (state.isAnalyzing) return; + if (state.currentPopupState !== "normal") return; + if (!state.siteTextTarget) return; + try { + const response = await sendToActiveTab({ + type: "EXTRACT_BY_SELECTOR", + target: state.siteTextTarget + }); + if (!response?.ok) return; + state.siteText = response.extracted || ""; + state.siteTextTarget = response.target || state.siteTextTarget; + updateCounts(); + } catch { + // Ignore refresh failures; counts will update on next explicit extract. + } } function renderTasks(tasks) { @@ -792,6 +880,7 @@ function setEnvironmentSelection(envId) { envSelect.value = target; } state.selectedEnvId = target; + updatePromptCount(); } function setProfileSelection(profileId) { @@ -803,6 +892,7 @@ function setProfileSelection(profileId) { profileSelect.value = target; } state.selectedProfileId = target; + updatePromptCount(); } function selectTask(taskId, { resetEnv } = { resetEnv: false }) { @@ -814,6 +904,7 @@ function selectTask(taskId, { resetEnv } = { resetEnv: false }) { setEnvironmentSelection(getTaskDefaultEnvId(task)); setProfileSelection(getTaskDefaultProfileId(task)); } + updatePromptCount(); } async function persistSelections() { @@ -900,6 +991,7 @@ function ensurePort() { async function loadConfig() { const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); const currentUrl = tabs[0]?.url || ""; + state.activeTabId = tabs[0]?.id || null; const { lastPopupState, [POPUP_DRAFT_KEY]: popupDraft } = await getStorage([ "lastPopupState", @@ -968,6 +1060,9 @@ async function loadConfig() { state.currentWorkspace = activeWorkspace; currentWorkspaceName.textContent = activeWorkspace.name || "Global"; } + if (state.currentSite && !state.siteTextTarget) { + state.siteTextTarget = normalizeStoredExtractTarget(state.currentSite); + } if (stored.theme) { state.globalTheme = stored.theme; } @@ -1005,6 +1100,7 @@ async function loadConfig() { state.selectedTaskId = ""; setEnvironmentSelection(effectiveEnvs[0]?.id || ""); setProfileSelection(effectiveProfiles[0]?.id || ""); + updateCounts(); return; } @@ -1040,6 +1136,10 @@ async function loadConfig() { await persistSelections(); } + updateCounts(); + if (state.currentSite) { + await refreshSiteContentCounts(); + } maybeRunDefaultTask(); } @@ -1068,8 +1168,7 @@ async function handleExtract() { state.siteText = response.extracted || ""; state.siteTextTarget = response.target || target; - updateSiteTextCount(); - updatePromptCount(0); + updateCounts(); setStatus("Text extracted."); return true; } catch (error) { @@ -1199,7 +1298,7 @@ async function handleAnalyze() { task.text || "", state.siteText ); - updatePromptCount(promptText.length); + updatePromptCount(); state.outputRaw = ""; renderOutput(); @@ -1298,6 +1397,7 @@ async function runMinimalExtraction(text, minLength = 5) { state.siteText = response.extracted; state.siteTextTarget = response.target || { kind: "textScope", text: trimmed }; extractedPreview.textContent = state.siteText; + updateCounts(); await fillSiteDefaultsFromTab(); switchState("review"); await persistPopupDraft(); @@ -1336,6 +1436,7 @@ extractFullBtn.addEventListener("click", async () => { state.siteText = response.extracted; state.siteTextTarget = target; extractedPreview.textContent = state.siteText; + updateCounts(); await fillSiteDefaultsFromTab(); switchState("review"); await persistPopupDraft(); @@ -1372,6 +1473,7 @@ retryExtractBtn.addEventListener("click", () => { if (workspaceSelect) workspaceSelect.value = "global"; state.siteText = ""; state.siteTextTarget = null; + updateCounts(); setMinimalStatus(""); void clearPopupDraft(); setStatus("Ready."); @@ -1427,7 +1529,7 @@ confirmSiteBtn.addEventListener("click", async () => { currentWorkspaceName.textContent = state.currentWorkspace.name || "Global"; await loadConfig(); await switchState("normal"); - updateSiteTextCount(); + updateCounts(); setStatus("Site saved."); }); @@ -1450,8 +1552,7 @@ profileSelect.addEventListener("change", () => { void persistSelections(); }); -updateSiteTextCount(); -updatePromptCount(0); +updateCounts(); renderOutput(); setAnalyzing(false); void loadTheme(); @@ -1597,4 +1698,27 @@ chrome.storage.onChanged.addListener((changes) => { state.alwaysShowOutput = Boolean(changes.alwaysShowOutput.newValue); updateOutputVisibility(); } + + const configKeys = [ + "tasks", + "envConfigs", + "profiles", + "shortcuts", + "workspaces", + "sites", + "theme", + "alwaysShowOutput" + ]; + if (configKeys.some((key) => changes[key])) { + scheduleConfigRefresh(); + } +}); + +chrome.runtime.onMessage.addListener((message, sender) => { + if (message?.type !== "SITE_CONTENT_CHANGED") return; + const senderTabId = sender?.tab?.id || null; + if (state.activeTabId && senderTabId && senderTabId !== state.activeTabId) { + return; + } + scheduleSiteContentRefresh(); });