fix(extension): sync robuste content/sidepanel, diagnostic panel, preview propre v0.4.4
- content.js : écrit la sélection dans chrome.storage.session/local en plus du message runtime - sidepanel.js : lit le storage en fallback, loggue chaque étape, affiche un diagnostic panel - sidepanel.js : avertissement explicite si le content script n'est pas détecté - sidepanel.js : sépare le rendu Markdown du résumé et l'affichage texte brut de l'extrait - sidepanel.css : agrandit la zone de prévisualisation - i18n : nouvelles clés contentScriptMissing, diagnosticsTitle, copyDiagnostics - locales : correction FR (Retour, Enregistrement, Note, etc.) + version 0.4.4 partout - extract-article.ts : nettoie textContent des métadonnées (dates, temps de lecture, sources) - analyze/route.ts : excerpt propre via makeExcerpt - builds Chrome Store et Firefox regénérés en 0.4.4
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
const ALLOW_INSTANCE_CONFIG = false
|
||||
const DEFAULT_BASE = 'https://memento-note.com'
|
||||
const STORAGE_KEYS = { baseUrl: 'memento_clipper_base_url', notebookId: 'memento_clipper_notebook_id' }
|
||||
const SESSION_KEY = 'memento_clipper_session'
|
||||
|
||||
let state = 'idle'
|
||||
let notebooks = []
|
||||
@@ -25,6 +26,9 @@ let pendingClipType = 'page'
|
||||
let analyzeResult = null
|
||||
let editableTitle = ''
|
||||
let connected = false
|
||||
let contentScriptReady = false
|
||||
const diagLogs = []
|
||||
let diagOpen = false
|
||||
|
||||
const els = {
|
||||
screen: document.getElementById('screen'),
|
||||
@@ -36,6 +40,10 @@ const els = {
|
||||
settingsStatus: document.getElementById('settingsStatus'),
|
||||
applyInstanceBtn: document.getElementById('applyInstanceBtn'),
|
||||
openLoginBtn: document.getElementById('openLoginBtn'),
|
||||
diagPanel: document.getElementById('diagPanel'),
|
||||
diagToggle: document.getElementById('diagToggle'),
|
||||
diagBody: document.getElementById('diagBody'),
|
||||
copyDiagBtn: document.getElementById('copyDiagBtn'),
|
||||
}
|
||||
|
||||
const ICON_SELECT =
|
||||
@@ -71,6 +79,10 @@ function escapeHtml(s) {
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function escapePlainText(s) {
|
||||
return escapeHtml(s).replace(/\n/g, '<br/>')
|
||||
}
|
||||
|
||||
/**
|
||||
* Mini-renderer Markdown sûr pour le side panel.
|
||||
* - Échappe d'abord le texte (anti-XSS)
|
||||
@@ -136,6 +148,38 @@ function rtlAttrs(text) {
|
||||
return ` class="text-rtl" dir="rtl"${lang}`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Diagnostic logging
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function logDiag(label, detail = '') {
|
||||
const line = `[${new Date().toLocaleTimeString()}] ${label}${detail ? ` — ${detail}` : ''}`
|
||||
diagLogs.push(line)
|
||||
if (diagLogs.length > 80) diagLogs.shift()
|
||||
renderDiag()
|
||||
console.debug('[memento-diag]', label, detail)
|
||||
}
|
||||
|
||||
function renderDiag() {
|
||||
if (!els.diagBody) return
|
||||
els.diagBody.innerHTML = diagLogs.map((l) => `<div class="diag-line">${escapeHtml(l)}</div>`).join('')
|
||||
els.diagBody.scrollTop = els.diagBody.scrollHeight
|
||||
}
|
||||
|
||||
async function copyDiagnostics() {
|
||||
try {
|
||||
const text = diagLogs.join('\n')
|
||||
await navigator.clipboard.writeText(text)
|
||||
logDiag('diagnostics copied')
|
||||
} catch (e) {
|
||||
logDiag('copy failed', String(e))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Notebooks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function sortNotebooksHierarchy(list) {
|
||||
const byParent = new Map()
|
||||
for (const n of list) {
|
||||
@@ -189,29 +233,60 @@ async function getActiveTab() {
|
||||
return tab
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Content script synchronization (v0.4.4)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function storageArea() {
|
||||
return chrome.storage.session || chrome.storage.local
|
||||
}
|
||||
|
||||
async function ensureContentScript(tabId) {
|
||||
logDiag('ensureContentScript start', `tab=${tabId}`)
|
||||
try {
|
||||
const resp = await chrome.tabs.sendMessage(tabId, { type: 'PING' })
|
||||
if (resp?.ok) return true
|
||||
} catch {
|
||||
/* inject */
|
||||
if (resp?.ok) {
|
||||
contentScriptReady = true
|
||||
logDiag('PING ok', `pickMode=${resp.pickMode} sel=${resp.selection?.text?.length || 0}`)
|
||||
if (resp.selection) applySelectionFromMessage(resp.selection, { fromPing: true })
|
||||
return true
|
||||
}
|
||||
} catch (err) {
|
||||
logDiag('PING failed', String(err?.message || err))
|
||||
}
|
||||
try {
|
||||
await chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'] })
|
||||
return true
|
||||
} catch {
|
||||
logDiag('content script injected')
|
||||
// attendre un court instant que le script s'enregistre
|
||||
await new Promise((r) => setTimeout(r, 120))
|
||||
const resp = await chrome.tabs.sendMessage(tabId, { type: 'PING' })
|
||||
contentScriptReady = resp?.ok || false
|
||||
logDiag('PING after inject', `ok=${contentScriptReady}`)
|
||||
return contentScriptReady
|
||||
} catch (err) {
|
||||
contentScriptReady = false
|
||||
logDiag('content script inject failed', String(err?.message || err))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function setPickModeOnTab(enabled) {
|
||||
if (!activeTabId || pageRestricted) return
|
||||
if (!activeTabId || pageRestricted) {
|
||||
logDiag('setPickMode skipped', `tab=${activeTabId} restricted=${pageRestricted}`)
|
||||
return false
|
||||
}
|
||||
const ok = await ensureContentScript(activeTabId)
|
||||
if (!ok) return
|
||||
if (!ok) return false
|
||||
try {
|
||||
await chrome.tabs.sendMessage(activeTabId, { type: 'SET_PICK_MODE', enabled })
|
||||
} catch {
|
||||
/* ignore */
|
||||
const resp = await chrome.tabs.sendMessage(activeTabId, { type: 'SET_PICK_MODE', enabled })
|
||||
contentScriptReady = resp?.ok || false
|
||||
logDiag('SET_PICK_MODE ok', `enabled=${enabled} pickMode=${resp?.pickMode}`)
|
||||
if (resp?.selection) applySelectionFromMessage(resp.selection, { fromSetPickMode: true })
|
||||
return contentScriptReady
|
||||
} catch (err) {
|
||||
contentScriptReady = false
|
||||
logDiag('SET_PICK_MODE failed', String(err?.message || err))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +294,158 @@ async function syncPickMode() {
|
||||
await setPickModeOnTab(state === 'idle' && !pageRestricted)
|
||||
}
|
||||
|
||||
async function readSelectionFromStorage() {
|
||||
try {
|
||||
const data = await storageArea().get(SESSION_KEY)
|
||||
const session = data?.[SESSION_KEY]
|
||||
if (!session) return null
|
||||
logDiag('storage read', `url=${session.url?.slice(0, 60)} sel=${session.text?.length || 0}`)
|
||||
return session
|
||||
} catch (err) {
|
||||
logDiag('storage read failed', String(err?.message || err))
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function applySelectionFromMessage(msg, { fromStorage = false, fromPing = false, fromSetPickMode = false } = {}) {
|
||||
if (!msg) return
|
||||
const normalizeUrl = (u) => {
|
||||
if (!u) return ''
|
||||
try { return new URL(u).origin + new URL(u).pathname.replace(/\/+$/, '') }
|
||||
catch { return u }
|
||||
}
|
||||
const msgUrl = msg.url || ''
|
||||
const curUrl = pageUrl || ''
|
||||
|
||||
if (!curUrl) {
|
||||
if (msgUrl) {
|
||||
pageUrl = msgUrl
|
||||
pageTitle = msg.title || pageTitle || ''
|
||||
try {
|
||||
const u = new URL(msgUrl)
|
||||
pageDomain = u.hostname
|
||||
pageFavicon = `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32`
|
||||
} catch { /* ignore */ }
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else if (normalizeUrl(msgUrl) !== normalizeUrl(curUrl)) {
|
||||
return
|
||||
}
|
||||
|
||||
selectionText = msg.text || ''
|
||||
if (msg.dir?.toLowerCase() === 'rtl') pageDir = 'rtl'
|
||||
if (msg.lang) pageLang = msg.lang
|
||||
logDiag('selection applied', `len=${selectionText.length} source=${fromStorage ? 'storage' : fromPing ? 'ping' : fromSetPickMode ? 'setPickMode' : 'message'}`)
|
||||
if (state === 'idle') {
|
||||
if (!curUrl) render()
|
||||
else updateSelectionUI()
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshPageContext() {
|
||||
logDiag('refreshPageContext start')
|
||||
const tab = await getActiveTab()
|
||||
activeTabId = tab?.id ?? null
|
||||
pageRestricted = tab?.url ? isRestrictedUrl(tab.url) : false
|
||||
logDiag('active tab', `id=${activeTabId} url=${tab?.url?.slice(0, 80)} restricted=${pageRestricted}`)
|
||||
|
||||
if (!tab?.id || pageRestricted) {
|
||||
pageUrl = tab?.url || ''
|
||||
pageTitle = tab?.title || t('pageNotAccessible')
|
||||
selectionText = ''
|
||||
contentScriptReady = false
|
||||
return
|
||||
}
|
||||
|
||||
pageUrl = tab.url
|
||||
pageTitle = tab.title || ''
|
||||
try {
|
||||
const u = new URL(pageUrl)
|
||||
pageDomain = u.hostname
|
||||
pageFavicon = `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32`
|
||||
} catch {
|
||||
pageDomain = pageUrl
|
||||
pageFavicon = 'https://www.google.com/s2/favicons?domain=google.com&sz=32'
|
||||
}
|
||||
|
||||
const ok = await ensureContentScript(tab.id)
|
||||
if (!ok) {
|
||||
// En dernier recours, lire depuis le storage session/local.
|
||||
const session = await readSelectionFromStorage()
|
||||
if (session) applySelectionFromMessage(session, { fromStorage: true })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const ctx = await chrome.tabs.sendMessage(tab.id, { type: 'GET_CONTEXT' })
|
||||
pageHtml = ctx?.html || ''
|
||||
selectionText = ctx?.text || ''
|
||||
pageDir = ctx?.dir?.toLowerCase() === 'rtl' ? 'rtl' : 'ltr'
|
||||
pageLang = ctx?.lang || ''
|
||||
logDiag('GET_CONTEXT ok', `html=${pageHtml.length} sel=${selectionText.length}`)
|
||||
} catch (err) {
|
||||
logDiag('GET_CONTEXT failed', String(err?.message || err))
|
||||
// Fallback storage
|
||||
const session = await readSelectionFromStorage()
|
||||
if (session) applySelectionFromMessage(session, { fromStorage: true })
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const stored = await chrome.storage.sync.get([STORAGE_KEYS.baseUrl, STORAGE_KEYS.notebookId])
|
||||
if (els.baseUrl) {
|
||||
els.baseUrl.value = ALLOW_INSTANCE_CONFIG
|
||||
? stored[STORAGE_KEYS.baseUrl] || DEFAULT_BASE
|
||||
: DEFAULT_BASE
|
||||
}
|
||||
await loadNotebooks(stored[STORAGE_KEYS.notebookId])
|
||||
}
|
||||
|
||||
async function loadNotebooks(preferredId) {
|
||||
try {
|
||||
await ensureApiPermission()
|
||||
const res = await fetch(`${apiBase()}/api/clip/notebooks`, { credentials: 'include' })
|
||||
if (!res.ok) {
|
||||
connected = false
|
||||
updateConnBadge()
|
||||
if (res.status === 401) {
|
||||
throw new Error(t('errLoginRequired'))
|
||||
}
|
||||
throw new Error(t('errLoadNotebooks'))
|
||||
}
|
||||
const data = await res.json()
|
||||
notebooks = data.notebooks || []
|
||||
selectedNotebookId =
|
||||
(preferredId && notebooks.some((n) => n.id === preferredId) ? preferredId : '') ||
|
||||
notebooks[0]?.id ||
|
||||
''
|
||||
connected = true
|
||||
updateConnBadge()
|
||||
errorMessage = ''
|
||||
setSettingsStatus(t('notebooksLoaded'), false)
|
||||
logDiag('notebooks loaded', `${notebooks.length}`)
|
||||
} catch (e) {
|
||||
notebooks = []
|
||||
connected = false
|
||||
updateConnBadge()
|
||||
errorMessage = e.message
|
||||
setSettingsStatus(e.message, true)
|
||||
logDiag('notebooks failed', e.message)
|
||||
}
|
||||
}
|
||||
|
||||
async function applyInstance() {
|
||||
const url = (els.baseUrl?.value || DEFAULT_BASE).replace(/\/$/, '')
|
||||
if (els.baseUrl) els.baseUrl.value = url
|
||||
await chrome.storage.sync.set({ [STORAGE_KEYS.baseUrl]: url })
|
||||
setSettingsStatus(t('connecting'), false)
|
||||
await loadNotebooks(selectedNotebookId)
|
||||
if (connected) {
|
||||
setSettingsStatus(t('connectedToUrl', url), false)
|
||||
}
|
||||
}
|
||||
|
||||
function updateConnBadge() {
|
||||
if (!els.connBadge) return
|
||||
els.connBadge.hidden = !connected
|
||||
@@ -256,7 +483,10 @@ function selectionBlockHtml() {
|
||||
|
||||
function actionsBlockHtml() {
|
||||
const hasSel = Boolean(selectionText)
|
||||
return `<div class="actions" id="actionsSlot">
|
||||
const contentScriptWarning = !contentScriptReady && !pageRestricted
|
||||
? `<div class="cs-warning">${escapeHtml(t('contentScriptMissing') || 'Content script non détecté — rechargez la page pour clipper.')}</div>`
|
||||
: ''
|
||||
return `${contentScriptWarning}<div class="actions" id="actionsSlot">
|
||||
${
|
||||
hasSel
|
||||
? `<button type="button" class="btn btn-sky" id="clipSelBtn">
|
||||
@@ -283,7 +513,6 @@ function bindIdleHandlers() {
|
||||
document.getElementById('clipPageBtn')?.addEventListener('click', () => void runAnalyze('page'))
|
||||
document.getElementById('clipLinkBtn')?.addEventListener('click', () => void runAnalyze('link'))
|
||||
|
||||
// Gérer l'erreur de chargement du favicon
|
||||
document.querySelector('.page-favicon')?.addEventListener('error', function() {
|
||||
const fallback = this.getAttribute('data-fallback')
|
||||
if (fallback && this.src !== fallback) {
|
||||
@@ -319,160 +548,6 @@ function updateSelectionUI() {
|
||||
bindIdleHandlers()
|
||||
}
|
||||
|
||||
function applySelectionFromMessage(msg) {
|
||||
if (!msg) return
|
||||
// Comparaison d'URL tolérante : ignorer trailing slash, hash, query.
|
||||
// Compare origin + pathname, pas l'URL complète.
|
||||
const normalizeUrl = (u) => {
|
||||
if (!u) return ''
|
||||
try { return new URL(u).origin + new URL(u).pathname.replace(/\/+$/, '') }
|
||||
catch { return u }
|
||||
}
|
||||
const msgUrl = msg.url || ''
|
||||
const curUrl = pageUrl || ''
|
||||
// Si pageUrl n'est pas encore set (race avec refreshPageContext),
|
||||
// on accepte le premier message qui arrive et on amorce les métadonnées
|
||||
// de la page depuis le message. Sans ce fallback, le bouton ne s'affiche
|
||||
// jamais si chrome.tabs.query rate ou revient avant refreshPageContext.
|
||||
if (!curUrl) {
|
||||
if (msgUrl) {
|
||||
pageUrl = msgUrl
|
||||
pageTitle = msg.title || pageTitle || ''
|
||||
try {
|
||||
const u = new URL(msgUrl)
|
||||
pageDomain = u.hostname
|
||||
pageFavicon = `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32`
|
||||
} catch { /* ignore */ }
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else if (normalizeUrl(msgUrl) !== normalizeUrl(curUrl)) {
|
||||
// URL vraiment différente (autre onglet) — ignorer.
|
||||
return
|
||||
}
|
||||
selectionText = msg.text || ''
|
||||
if (msg.dir?.toLowerCase() === 'rtl') pageDir = 'rtl'
|
||||
if (msg.lang) pageLang = msg.lang
|
||||
if (state === 'idle') {
|
||||
// Si la page card n'existait pas encore (pageUrl vide avant),
|
||||
// il faut un render() complet pour qu'elle apparaisse.
|
||||
if (!curUrl) {
|
||||
render()
|
||||
} else {
|
||||
updateSelectionUI()
|
||||
}
|
||||
} else {
|
||||
console.debug('[memento] selection updated while busy, deferring UI')
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshPageContext() {
|
||||
const tab = await getActiveTab()
|
||||
activeTabId = tab?.id ?? null
|
||||
// Ne marquer restricted QUE si on a vraiment une URL à analyser.
|
||||
// tab?.url undefined doit être traité comme "inconnu", pas restricted.
|
||||
pageRestricted = tab?.url ? isRestrictedUrl(tab.url) : false
|
||||
|
||||
if (!tab?.id || pageRestricted) {
|
||||
pageUrl = tab?.url || ''
|
||||
pageTitle = tab?.title || t('pageNotAccessible')
|
||||
selectionText = ''
|
||||
return
|
||||
}
|
||||
|
||||
pageUrl = tab.url
|
||||
pageTitle = tab.title || ''
|
||||
try {
|
||||
const u = new URL(pageUrl)
|
||||
pageDomain = u.hostname
|
||||
pageFavicon = `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32`
|
||||
} catch {
|
||||
pageDomain = pageUrl
|
||||
pageFavicon = 'https://www.google.com/s2/favicons?domain=google.com&sz=32'
|
||||
}
|
||||
|
||||
const ok = await ensureContentScript(tab.id)
|
||||
if (!ok) return
|
||||
|
||||
try {
|
||||
const ctx = await chrome.tabs.sendMessage(tab.id, { type: 'GET_CONTEXT' })
|
||||
pageHtml = ctx?.html || ''
|
||||
selectionText = ctx?.text || ''
|
||||
pageDir = ctx?.dir?.toLowerCase() === 'rtl' ? 'rtl' : 'ltr'
|
||||
pageLang = ctx?.lang || ''
|
||||
} catch {
|
||||
try {
|
||||
const [{ result }] = await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: () => ({
|
||||
html: document.documentElement.outerHTML,
|
||||
text: window.getSelection()?.toString().trim() || '',
|
||||
dir: document.documentElement.getAttribute('dir') || '',
|
||||
lang: (document.documentElement.getAttribute('lang') || '').split('-')[0],
|
||||
}),
|
||||
})
|
||||
pageHtml = result?.html || ''
|
||||
selectionText = result?.text || ''
|
||||
pageDir = result?.dir?.toLowerCase() === 'rtl' ? 'rtl' : 'ltr'
|
||||
pageLang = result?.lang || ''
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const stored = await chrome.storage.sync.get([STORAGE_KEYS.baseUrl, STORAGE_KEYS.notebookId])
|
||||
if (els.baseUrl) {
|
||||
els.baseUrl.value = ALLOW_INSTANCE_CONFIG
|
||||
? stored[STORAGE_KEYS.baseUrl] || DEFAULT_BASE
|
||||
: DEFAULT_BASE
|
||||
}
|
||||
await loadNotebooks(stored[STORAGE_KEYS.notebookId])
|
||||
}
|
||||
|
||||
async function loadNotebooks(preferredId) {
|
||||
try {
|
||||
await ensureApiPermission()
|
||||
const res = await fetch(`${apiBase()}/api/clip/notebooks`, { credentials: 'include' })
|
||||
if (!res.ok) {
|
||||
connected = false
|
||||
updateConnBadge()
|
||||
if (res.status === 401) {
|
||||
throw new Error(t('errLoginRequired'))
|
||||
}
|
||||
throw new Error(t('errLoadNotebooks'))
|
||||
}
|
||||
const data = await res.json()
|
||||
notebooks = data.notebooks || []
|
||||
selectedNotebookId =
|
||||
(preferredId && notebooks.some((n) => n.id === preferredId) ? preferredId : '') ||
|
||||
notebooks[0]?.id ||
|
||||
''
|
||||
connected = true
|
||||
updateConnBadge()
|
||||
errorMessage = ''
|
||||
setSettingsStatus(t('notebooksLoaded'), false)
|
||||
} catch (e) {
|
||||
notebooks = []
|
||||
connected = false
|
||||
updateConnBadge()
|
||||
errorMessage = e.message
|
||||
setSettingsStatus(e.message, true)
|
||||
}
|
||||
}
|
||||
|
||||
async function applyInstance() {
|
||||
const url = (els.baseUrl?.value || DEFAULT_BASE).replace(/\/$/, '')
|
||||
if (els.baseUrl) els.baseUrl.value = url
|
||||
await chrome.storage.sync.set({ [STORAGE_KEYS.baseUrl]: url })
|
||||
setSettingsStatus(t('connecting'), false)
|
||||
await loadNotebooks(selectedNotebookId)
|
||||
if (connected) {
|
||||
setSettingsStatus(t('connectedToUrl', url), false)
|
||||
}
|
||||
}
|
||||
|
||||
function renderIdle() {
|
||||
const restrictedBlock = pageRestricted
|
||||
? `<div class="restricted-note">${escapeHtml(t('restrictedPage'))}</div>`
|
||||
@@ -483,9 +558,14 @@ function renderIdle() {
|
||||
? `<div class="auth-hint">${escapeHtml(errorMessage)}</div>`
|
||||
: ''
|
||||
|
||||
const csWarning = !contentScriptReady && !pageRestricted
|
||||
? `<div class="cs-warning">${escapeHtml(t('contentScriptMissing') || 'Content script non détecté — rechargez la page pour clipper.')}</div>`
|
||||
: ''
|
||||
|
||||
els.screen.innerHTML = `
|
||||
${restrictedBlock}
|
||||
${authHint}
|
||||
${csWarning}
|
||||
|
||||
<div>
|
||||
<span class="label">${escapeHtml(t('destinationNotebook'))}</span>
|
||||
@@ -532,10 +612,10 @@ function renderConfirm() {
|
||||
const reading = formatReadingTime(analyzeResult?.readingTime)
|
||||
const tagsHtml = tags.map((t) => `<span class="tag-chip">${escapeHtml(t)}</span>`).join('')
|
||||
|
||||
// Le résumé et l'extrait peuvent contenir du Markdown produit par l'IA.
|
||||
// On rend le Markdown en HTML (limité et sûr) au lieu de l'afficher brut.
|
||||
// Le résumé peut contenir du Markdown produit par l'IA.
|
||||
// L'extrait est du texte brut : on l'affiche tel quel, sans interpréter de Markdown.
|
||||
const summaryHtml = summary ? renderMarkdownSafe(summary) : ''
|
||||
const excerptHtml = (excerpt && pendingClipType !== 'link') ? renderMarkdownSafe(excerpt) : ''
|
||||
const excerptHtml = (excerpt && pendingClipType !== 'link') ? escapePlainText(excerpt) : ''
|
||||
|
||||
els.screen.innerHTML = `
|
||||
<div class="confirm-panel">
|
||||
@@ -552,7 +632,7 @@ function renderConfirm() {
|
||||
</section>` : ''}
|
||||
${excerptHtml ? `<section class="preview-block excerpt-block"${rtlAttrs(excerpt)}>
|
||||
<header class="preview-block-head"><span class="dot dot-excerpt"></span>${escapeHtml(t('excerptLabel'))}</header>
|
||||
<div class="preview-block-body md md-excerpt">${excerptHtml}</div>
|
||||
<div class="preview-block-body md md-excerpt excerpt-plain">${excerptHtml}</div>
|
||||
</section>` : ''}
|
||||
</div>
|
||||
<div class="actions">
|
||||
@@ -670,6 +750,7 @@ async function runAnalyze(type) {
|
||||
analyzeBody = { url: pageUrl, html: pageHtml, title: pageTitle, mode: 'article' }
|
||||
}
|
||||
|
||||
logDiag('analyze request', `${type} url=${pageUrl?.slice(0, 60)}`)
|
||||
const analyzeRes = await fetch(`${apiBase()}/api/clip/analyze`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
@@ -682,10 +763,12 @@ async function runAnalyze(type) {
|
||||
analyzeResult = analysis
|
||||
editableTitle = analysis.title || pageTitle || pageDomain
|
||||
state = 'confirm'
|
||||
logDiag('analyze ok', `title=${analysis.title?.slice(0, 40)}`)
|
||||
render()
|
||||
} catch (e) {
|
||||
errorMessage = e.message || t('errNetwork')
|
||||
state = 'error'
|
||||
logDiag('analyze error', errorMessage)
|
||||
render()
|
||||
}
|
||||
}
|
||||
@@ -717,20 +800,34 @@ async function runSave() {
|
||||
lastNoteId = saved.noteId
|
||||
lastNoteUrl = saved.noteUrl
|
||||
state = 'success'
|
||||
logDiag('save ok', `noteId=${lastNoteId}`)
|
||||
render()
|
||||
} catch (e) {
|
||||
errorMessage = e.message || t('errNetwork')
|
||||
state = 'error'
|
||||
logDiag('save error', errorMessage)
|
||||
render()
|
||||
}
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg) => {
|
||||
if (msg?.type === 'SELECTION_CHANGED') applySelectionFromMessage(msg)
|
||||
if (msg?.type === 'SELECTION_CHANGED') {
|
||||
logDiag('SELECTION_CHANGED received', `len=${msg.text?.length || 0}`)
|
||||
applySelectionFromMessage(msg)
|
||||
}
|
||||
})
|
||||
|
||||
// Surveiller le storage session/local en cas de message perdu.
|
||||
storageArea().onChanged.addListener((changes) => {
|
||||
if (changes[SESSION_KEY]?.newValue) {
|
||||
logDiag('storage changed event')
|
||||
applySelectionFromMessage(changes[SESSION_KEY].newValue, { fromStorage: true })
|
||||
}
|
||||
})
|
||||
|
||||
chrome.tabs.onActivated.addListener(async () => {
|
||||
if (state !== 'idle') return
|
||||
logDiag('tabs.onActivated')
|
||||
await refreshPageContext()
|
||||
await syncPickMode()
|
||||
render()
|
||||
@@ -740,6 +837,7 @@ chrome.tabs.onUpdated.addListener(async (tabId, info) => {
|
||||
if (info.status !== 'complete' || state !== 'idle') return
|
||||
const tab = await getActiveTab()
|
||||
if (tab?.id === tabId) {
|
||||
logDiag('tabs.onUpdated complete')
|
||||
await refreshPageContext()
|
||||
await syncPickMode()
|
||||
render()
|
||||
@@ -751,6 +849,14 @@ els.settingsBtn?.addEventListener('click', () => {
|
||||
els.settingsPanel.hidden = !els.settingsPanel.hidden
|
||||
})
|
||||
|
||||
els.diagToggle?.addEventListener('click', () => {
|
||||
diagOpen = !diagOpen
|
||||
if (els.diagPanel) els.diagPanel.hidden = !diagOpen
|
||||
if (els.diagToggle) els.diagToggle.setAttribute('aria-expanded', String(diagOpen))
|
||||
})
|
||||
|
||||
els.copyDiagBtn?.addEventListener('click', () => void copyDiagnostics())
|
||||
|
||||
document.querySelectorAll('.preset-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const url = btn.getAttribute('data-url')
|
||||
@@ -766,12 +872,12 @@ els.openLoginBtn?.addEventListener('click', () => {
|
||||
document.addEventListener('visibilitychange', async () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
if (state === 'idle') {
|
||||
logDiag('visibility visible')
|
||||
await refreshPageContext()
|
||||
await syncPickMode()
|
||||
render()
|
||||
}
|
||||
} else if (document.visibilityState === 'hidden') {
|
||||
// Désactiver le pick mode quand le sidepanel est fermé
|
||||
await setPickModeOnTab(false)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user