Skip to content

Commit 7fb5785

Browse files
authored
feat: Add copy-to-clipboard fix prompt to Mermaid error notice (#2)
## Summary - add a "Copy fix prompt" action to Mermaid error notices so users can quickly ask ChatGPT for corrections - generate a context-aware prompt including the parse error and original Mermaid source - implement clipboard helpers with graceful fallback and expose the prompt for manual copy ## Testing - pnpm build ------ https://chatgpt.com/codex/tasks/task_b_68d6dada1980832b8978aa84ebc4b833
1 parent ca460d5 commit 7fb5785

File tree

1 file changed

+119
-4
lines changed

1 file changed

+119
-4
lines changed

src/contentScript/index.ts

Lines changed: 119 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ async function renderBlock(block: MermaidBlock) {
298298
}
299299

300300
diagramHost.innerHTML = ''
301-
diagramHost.append(createErrorNotice(diagramHost.ownerDocument, err))
301+
diagramHost.append(createErrorNotice(diagramHost.ownerDocument, err, source))
302302
container.dataset[BLOCK_DATA_STATUS] = 'error'
303303
registry.lastSvg = null
304304
registry.lastRenderId = undefined
@@ -652,7 +652,7 @@ function loadImage(url: string): Promise<HTMLImageElement> {
652652
})
653653
}
654654

655-
function createErrorNotice(doc: Document, err: unknown): HTMLElement {
655+
function createErrorNotice(doc: Document, err: unknown, source: string): HTMLElement {
656656
const wrapper = doc.createElement('div')
657657
wrapper.style.display = 'flex'
658658
wrapper.style.flexDirection = 'column'
@@ -665,7 +665,8 @@ function createErrorNotice(doc: Document, err: unknown): HTMLElement {
665665
title.style.color = isDarkMode() ? '#f87171' : '#b91c1c'
666666

667667
const details = doc.createElement('pre')
668-
details.textContent = err instanceof Error ? err.message : String(err)
668+
const errorMessage = err instanceof Error ? err.message : String(err)
669+
details.textContent = errorMessage
669670
details.style.margin = '0'
670671
details.style.whiteSpace = 'pre-wrap'
671672
details.style.fontSize = '0.75rem'
@@ -680,10 +681,124 @@ function createErrorNotice(doc: Document, err: unknown): HTMLElement {
680681
hint.style.fontSize = '0.75rem'
681682
hint.style.color = isDarkMode() ? 'rgba(226, 232, 240, 0.75)' : '#6b7280'
682683

683-
wrapper.append(title, details, hint)
684+
const promptSection = doc.createElement('div')
685+
promptSection.style.display = 'flex'
686+
promptSection.style.flexDirection = 'column'
687+
promptSection.style.gap = '0.35rem'
688+
689+
const promptButton = createActionButton(doc, 'Copy fix prompt')
690+
promptButton.style.alignSelf = 'flex-start'
691+
692+
const promptStatus = doc.createElement('span')
693+
promptStatus.style.margin = '0'
694+
promptStatus.style.fontSize = '0.7rem'
695+
promptStatus.style.display = 'none'
696+
697+
const promptText = buildMermaidFixPrompt(errorMessage, source)
698+
699+
const promptPreview = doc.createElement('textarea')
700+
promptPreview.value = promptText
701+
promptPreview.readOnly = true
702+
promptPreview.spellcheck = false
703+
promptPreview.rows = Math.min(12, Math.max(4, promptText.split('\n').length + 1))
704+
promptPreview.style.display = 'none'
705+
promptPreview.style.width = '100%'
706+
promptPreview.style.padding = '0.75rem'
707+
promptPreview.style.borderRadius = '0.5rem'
708+
promptPreview.style.border = getBorderColor()
709+
promptPreview.style.background = isDarkMode() ? 'rgba(30, 41, 59, 0.85)' : 'rgba(248, 250, 252, 0.9)'
710+
promptPreview.style.color = getPrimaryTextColor()
711+
promptPreview.style.fontSize = '0.75rem'
712+
promptPreview.style.lineHeight = '1.4'
713+
promptPreview.style.fontFamily =
714+
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
715+
promptPreview.style.resize = 'vertical'
716+
717+
const defaultLabel = promptButton.dataset['coderchartLabel'] || 'Copy fix prompt'
718+
719+
promptButton.addEventListener('click', async () => {
720+
promptButton.disabled = true
721+
promptButton.textContent = 'Copying...'
722+
promptPreview.style.display = 'block'
723+
724+
const copied = await copyTextToClipboard(doc, promptText)
725+
726+
promptStatus.style.display = 'block'
727+
if (copied) {
728+
promptStatus.textContent = 'Prompt copied to your clipboard. Paste it back into ChatGPT to request a fix.'
729+
promptStatus.style.color = isDarkMode() ? 'rgba(134, 239, 172, 0.9)' : '#166534'
730+
promptButton.textContent = 'Prompt copied!'
731+
} else {
732+
promptStatus.textContent = 'Clipboard access was blocked. Copy the prompt below manually.'
733+
promptStatus.style.color = isDarkMode() ? 'rgba(248, 113, 113, 0.9)' : '#b91c1c'
734+
promptButton.textContent = 'Copy fix prompt'
735+
promptPreview.focus()
736+
promptPreview.select()
737+
}
738+
739+
setTimeout(() => {
740+
promptButton.disabled = false
741+
promptButton.textContent = defaultLabel
742+
}, 2000)
743+
})
744+
745+
promptSection.append(promptButton, promptStatus, promptPreview)
746+
747+
wrapper.append(title, details, hint, promptSection)
684748
return wrapper
685749
}
686750

751+
function buildMermaidFixPrompt(errorMessage: string, source: string): string {
752+
const trimmedSource = source.trim()
753+
const formattedSource = trimmedSource ? `\n\n\`\`\`mermaid\n${trimmedSource}\n\`\`\`` : ''
754+
return (
755+
'The Mermaid diagram you generated could not be rendered by the CoderChart extension.' +
756+
`\nParse error: ${errorMessage}` +
757+
'\nPlease acknowledge that your earlier response included invalid Mermaid syntax and provide a corrected diagram.' +
758+
'\nRespond with only the Mermaid code block using valid Mermaid syntax.' +
759+
(formattedSource
760+
? `${formattedSource}\n`
761+
: '\nIf you need the original code, please restate it before sending the fix.\n')
762+
)
763+
}
764+
765+
async function copyTextToClipboard(doc: Document, text: string): Promise<boolean> {
766+
if (navigator.clipboard?.writeText) {
767+
try {
768+
await navigator.clipboard.writeText(text)
769+
return true
770+
} catch (err) {
771+
console.warn('Failed to copy prompt via navigator.clipboard', err)
772+
}
773+
}
774+
775+
if (!doc.body) {
776+
return false
777+
}
778+
779+
const textarea = doc.createElement('textarea')
780+
textarea.value = text
781+
textarea.setAttribute('readonly', '')
782+
textarea.style.position = 'fixed'
783+
textarea.style.opacity = '0'
784+
textarea.style.pointerEvents = 'none'
785+
textarea.style.top = '0'
786+
textarea.style.left = '0'
787+
doc.body.appendChild(textarea)
788+
textarea.focus()
789+
textarea.select()
790+
791+
let copied = false
792+
try {
793+
copied = doc.execCommand('copy')
794+
} catch (err) {
795+
console.warn('Failed to copy prompt via execCommand', err)
796+
}
797+
798+
textarea.remove()
799+
return copied
800+
}
801+
687802
function clearRenderedBlocks() {
688803
processedBlocks.forEach((entry) => {
689804
entry.container.remove()

0 commit comments

Comments
 (0)