Skip to content

Commit

Permalink
fix: gizmo mode is now the detaul mode
Browse files Browse the repository at this point in the history
Fix conversation choice
  • Loading branch information
pionxzh committed Dec 21, 2023
1 parent efb1e20 commit 7d83072
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 207 deletions.
299 changes: 105 additions & 194 deletions src/exporter/image.ts
@@ -1,6 +1,6 @@
import html2canvas from 'html2canvas'
import i18n from '../i18n'
import { checkIfConversationStarted, conversationChoiceSelector, getChatIdFromUrl, isGizmoMode } from '../page'
import { checkIfConversationStarted, getChatIdFromUrl } from '../page'
import { downloadUrl, getFileNameWithFormat } from '../utils/download'
import { Effect } from '../utils/effect'
import { sleep } from '../utils/utils'
Expand All @@ -18,76 +18,66 @@ export async function exportToPng(fileNameFormat: string) {

const effect = new Effect()

let thread: HTMLElement | null = null

const isGizmo = isGizmoMode()
if (isGizmo) {
thread = document.querySelector('main [class^=\'react-scroll-to-bottom\'] > div > div')
if (!thread || thread.children.length === 0 || thread.scrollHeight < 50) {
alert(i18n.t('Failed to export to PNG. Failed to find the element node.'))
return false
}

// gizmo style caused too much weird stuff... transparent background, weird spacing, etc.
effect.add(() => {
document.documentElement.classList.remove('gizmo')
return () => document.documentElement.classList.add('gizmo')
})

effect.add(() => {
document.documentElement.style.setProperty('font-size', '12px')
return () => document.documentElement.style.removeProperty('font-size')
})

// pre-calculated rem to px for tailwindcss
// effect.add(() => {
// const stylesheet = new CSSStyleSheet()
// stylesheet.replaceSync(`
// .text-xs {
// font-size: 12px; // 0.75rem
// line-height: 16px; // 1rem
// }

// .text-sm {
// font-size: 14px; // 0.875rem
// line-height: 20px; // 1.25rem
// }

// .text-base {
// font-size: 16px; // 1rem
// line-height: 24px; // 1.5rem
// }

// .text-xl {
// font-size: 24px; // 1.5rem
// line-height: 32px; // 2rem
// }

// .px-4 {
// padding-left: 16px; // 1rem
// padding-right: 16px; // 1rem
// }

// .py-2 {
// padding-top: 8px; // 0.5rem
// padding-bottom: 8px; // 0.5rem
// }

// .font-semibold {
// line-height: 20px;
// }
// `)
// const oldAdoptedStyleSheets = document.adoptedStyleSheets
// document.adoptedStyleSheets = [...oldAdoptedStyleSheets, stylesheet]
// return () => {
// stylesheet.replaceSync('')
// document.adoptedStyleSheets = oldAdoptedStyleSheets
// }
// })
const thread = document.querySelector('main [class^=\'react-scroll-to-bottom\'] > div > div')
if (!thread || thread.children.length === 0 || thread.scrollHeight < 50) {
alert(i18n.t('Failed to export to PNG. Failed to find the element node.'))
return false
}

effect.add(() => {
const style = document.createElement('style')
style.textContent = `
effect.add(() => {
document.documentElement.style.setProperty('font-size', '12px')
return () => document.documentElement.style.removeProperty('font-size')
})

// pre-calculated rem to px for tailwindcss
// effect.add(() => {
// const stylesheet = new CSSStyleSheet()
// stylesheet.replaceSync(`
// .text-xs {
// font-size: 12px; // 0.75rem
// line-height: 16px; // 1rem
// }

// .text-sm {
// font-size: 14px; // 0.875rem
// line-height: 20px; // 1.25rem
// }

// .text-base {
// font-size: 16px; // 1rem
// line-height: 24px; // 1.5rem
// }

// .text-xl {
// font-size: 24px; // 1.5rem
// line-height: 32px; // 2rem
// }

// .px-4 {
// padding-left: 16px; // 1rem
// padding-right: 16px; // 1rem
// }

// .py-2 {
// padding-top: 8px; // 0.5rem
// padding-bottom: 8px; // 0.5rem
// }

// .font-semibold {
// line-height: 20px;
// }
// `)
// const oldAdoptedStyleSheets = document.adoptedStyleSheets
// document.adoptedStyleSheets = [...oldAdoptedStyleSheets, stylesheet]
// return () => {
// stylesheet.replaceSync('')
// document.adoptedStyleSheets = oldAdoptedStyleSheets
// }
// })

effect.add(() => {
const style = document.createElement('style')
style.textContent = `
pre {
margin-top: 8px !important;
}
Expand All @@ -97,145 +87,66 @@ export async function exportToPng(fileNameFormat: string) {
padding-bottom: 2px;
}
`
thread!.appendChild(style)
return () => style.remove()
})

const conversationNodes = document.querySelectorAll<HTMLDivElement>('[data-testid^="conversation-turn-"]')
conversationNodes.forEach((node) => {
effect.add(() => {
node.style.height = `${node.clientHeight}px`
return () => node.style.removeProperty('height')
})
})

// hide top header
const topHeader = thread.querySelector('.sticky.top-0')
if (topHeader) {
effect.add(() => {
topHeader.classList.add('hidden')
return () => topHeader.classList.remove('hidden')
})
}
thread!.appendChild(style)
return () => style.remove()
})

// hide buttons
const buttonWrappers = document.querySelectorAll<HTMLDivElement>('main .flex.justify-between')
buttonWrappers.forEach((wrapper) => {
if (!wrapper.querySelector('button')) return
// ignore codeblock
if (wrapper.closest('pre')) return

effect.add(() => {
wrapper.style.display = 'none'
return () => wrapper.style.display = ''
})
})

// hide code block copy button
const copyButtons = thread.querySelectorAll('pre button')
copyButtons.forEach((button) => {
effect.add(() => {
button.classList.add('hidden')
return () => button.classList.remove('hidden')
})
})

// hide back to top button
const backToTop = thread.querySelectorAll('button.absolute')
backToTop.forEach((button) => {
effect.add(() => {
button.classList.add('hidden')
return () => button.classList.remove('hidden')
})
const conversationNodes = document.querySelectorAll<HTMLDivElement>('[data-testid^="conversation-turn-"]')
conversationNodes.forEach((node) => {
effect.add(() => {
node.style.height = `${node.clientHeight}px`
return () => node.style.removeProperty('height')
})
})

// hide weird cover on avatar
const shadowStrokes = thread.querySelectorAll('.gizmo-shadow-stroke')
shadowStrokes.forEach((stroke) => {
effect.add(() => {
stroke.classList.remove('gizmo-shadow-stroke')
return () => stroke.classList.add('gizmo-shadow-stroke')
})
// hide top header
const topHeader = thread.querySelector('.sticky.top-0')
if (topHeader) {
effect.add(() => {
topHeader.classList.add('hidden')
return () => topHeader.classList.remove('hidden')
})
}
else {
thread = document.querySelector('main .group')?.parentElement ?? null
if (!thread || thread.children.length === 0 || thread.scrollHeight < 50) {
alert(i18n.t('Failed to export to PNG. Failed to find the element node.'))
return false
}

const threadEl = thread as HTMLElement

// hide model bar
const modelBar = threadEl.firstElementChild
if (modelBar?.textContent?.startsWith('Model:')) {
effect.add(() => {
modelBar.classList.add('hidden')
return () => modelBar.classList.remove('hidden')
})
}
// hide buttons
const buttonWrappers = document.querySelectorAll<HTMLDivElement>('main .flex.justify-between')
buttonWrappers.forEach((wrapper) => {
if (!wrapper.querySelector('button')) return
// ignore codeblock
if (wrapper.closest('pre')) return

// hide bottom bar
effect.add(() => {
const bottomBar = threadEl.children[threadEl.children.length - 1]
bottomBar.classList.add('hidden')
return () => bottomBar.classList.remove('hidden')
})

// hide buttons
const buttonWrappers = document.querySelectorAll<HTMLDivElement>('main .flex.justify-between')
buttonWrappers.forEach((wrapper) => {
if (!wrapper.querySelector('button')) return
// ignore codeblock
if (wrapper.closest('pre')) return

effect.add(() => {
wrapper.style.display = 'none'
return () => wrapper.style.display = ''
})
wrapper.style.display = 'none'
return () => wrapper.style.display = ''
})
})

// hide conversation choices. eg. <1 / 6>
const conversationChoices = document.querySelectorAll(conversationChoiceSelector)
conversationChoices.forEach((choice) => {
effect.add(() => {
const parent = choice.parentElement
if (!parent) return
parent.classList.add('hidden')
return () => parent.classList.remove('hidden')
})
// hide code block copy button
const copyButtons = thread.querySelectorAll('pre button')
copyButtons.forEach((button) => {
effect.add(() => {
button.classList.add('hidden')
return () => button.classList.remove('hidden')
})
})

// disabled the avatar srcset
// fix https://github.com/pionxzh/chatgpt-exporter/issues/53
// seems related to https://github.com/niklasvh/html2canvas/issues/2218
const avatarEls = Array.from(document.querySelectorAll('img[alt]:not([aria-hidden])'))
avatarEls.forEach((el) => {
const srcset = el.getAttribute('srcset')
if (srcset) {
effect.add(() => {
el.setAttribute('data-srcset', srcset)
el.removeAttribute('srcset')
return () => {
el.setAttribute('srcset', srcset)
el.removeAttribute('data-srcset')
}
})
}
// hide back to top button
const backToTop = thread.querySelectorAll('button.absolute')
backToTop.forEach((button) => {
effect.add(() => {
button.classList.add('hidden')
return () => button.classList.remove('hidden')
})
})

// add `break-words` to all message elements
// html2canvas cannot handle the spacing correctly on Firefox with MacOS
// fix https://github.com/pionxzh/chatgpt-exporter/issues/78
const messageEls = Array.from(threadEl.querySelectorAll('.group .whitespace-pre-wrap'))
messageEls.forEach((el) => {
effect.add(() => {
el.classList.add('break-words')
return () => el.classList.remove('break-words')
})
// hide weird cover on avatar
const shadowStrokes = thread.querySelectorAll('.gizmo-shadow-stroke')
shadowStrokes.forEach((stroke) => {
effect.add(() => {
stroke.classList.remove('gizmo-shadow-stroke')
return () => stroke.classList.add('gizmo-shadow-stroke')
})
}
})

const threadEl = thread as HTMLElement

Expand Down
18 changes: 5 additions & 13 deletions src/page.ts
Expand Up @@ -75,23 +75,15 @@ export function getConversationFromSharePage() {
return null
}

export const conversationChoiceSelector = '.flex.justify-center span.flex-grow'
export const conversationChoiceSelectorGizmo = '.flex-grow.flex-shrink-0.tabular-nums'

export function isGizmoMode() {
return document.documentElement.classList.contains('gizmo')
}
const conversationChoiceSelector = '.flex-grow.flex-shrink-0.tabular-nums'

export function getConversationChoice() {
// parse x from `< x / y >` to get the index of the selected response
const nodes = isGizmoMode()
? Array.from(document.querySelectorAll('[data-testid^="conversation-turn-"]'))
.map(turn => turn.querySelector(conversationChoiceSelectorGizmo))
: Array.from(document.querySelectorAll('main .group'))
.map(group => group.querySelector(conversationChoiceSelector))
const nodes = Array.from(document.querySelectorAll('[data-testid^="conversation-turn-"]'))
.map(turn => turn.querySelector(conversationChoiceSelector))
const conversationChoices: Array<number | null> = nodes
// non-existing element will produce null here, which will point to the last child
// just in case the selector changed
// non-existing element will produce null here, which will point to the last child
// just in case the selector changed
.map(span => Number.parseInt(span?.textContent?.trim().split(' / ')[0] ?? '0') - 1)
.map(x => x === -1 ? null : x)

Expand Down

0 comments on commit 7d83072

Please sign in to comment.