Skip to content

Commit

Permalink
fix: port continuation merging back and remove conversation choice
Browse files Browse the repository at this point in the history
  • Loading branch information
pionxzh committed Dec 24, 2023
1 parent dc157f7 commit b1ff972
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 56 deletions.
85 changes: 59 additions & 26 deletions src/api.ts
Expand Up @@ -367,7 +367,7 @@ export interface ConversationResult {
conversationNodes: ConversationNode[]
}

const modelMapping: { [key in ModelSlug]: string } & { [key: string]: string } = {
const ModelMapping: { [key in ModelSlug]: string } & { [key: string]: string } = {
'text-davinci-002-render-sha': 'GTP-3.5',
'text-davinci-002-render-paid': 'GTP-3.5',
'text-davinci-002-browse': 'GTP-3.5',
Expand All @@ -378,60 +378,93 @@ const modelMapping: { [key in ModelSlug]: string } & { [key: string]: string } =
'text-davinci-002': 'GTP-3.5',
}

export function processConversation(conversation: ApiConversationWithId, _conversationChoices: Array<number | null> = []): ConversationResult {
export function processConversation(conversation: ApiConversationWithId): ConversationResult {
const title = conversation.title || 'ChatGPT Conversation'
const createTime = conversation.create_time
const updateTime = conversation.update_time
const modelSlug = Object.values(conversation.mapping).find(node => node.message?.metadata?.model_slug)?.message?.metadata?.model_slug || ''
const { model, modelSlug } = extractModel(conversation.mapping)

const startNodeId = conversation.current_node
|| Object.values(conversation.mapping).find(node => !node.children || node.children.length === 0)?.id
if (!startNodeId) throw new Error('Failed to find start node.')

const conversationNodes = extractConversationResult(conversation.mapping, startNodeId)
const mergedConversationNodes = mergeContinuationNodes(conversationNodes)

return {
id: conversation.id,
title,
model,
modelSlug,
createTime,
updateTime,
conversationNodes: mergedConversationNodes,
}
}

function extractModel(conversationMapping: Record<string, ConversationNode>) {
let model = ''
const modelSlug = Object.values(conversationMapping).find(node => node.message?.metadata?.model_slug)?.message?.metadata?.model_slug || ''
if (modelSlug) {
if (modelMapping[modelSlug]) {
model = modelMapping[modelSlug]
if (ModelMapping[modelSlug]) {
model = ModelMapping[modelSlug]
}
else {
Object.keys(modelMapping).forEach((key) => {
Object.keys(ModelMapping).forEach((key) => {
if (modelSlug.startsWith(key)) {
model = key
}
})
}
}

let result: ConversationNode[] = []

const bottomMostNodeId = conversation.current_node
if (bottomMostNodeId) {
result = extractConversationResult(conversation.mapping, bottomMostNodeId)
}

return {
id: conversation.id,
title,
modelSlug,
model,
createTime,
updateTime,
conversationNodes: result,
modelSlug,
}
}

function extractConversationResult(conversationData: Record<string, ConversationNode>, startNodeId: string): ConversationNode[] {
function extractConversationResult(conversationMapping: Record<string, ConversationNode>, startNodeId: string): ConversationNode[] {
const result: ConversationNode[] = []
let currentNodeId: string | undefined = startNodeId

while (currentNodeId) {
const node: ConversationNode = conversationData[currentNodeId]
const node: ConversationNode = conversationMapping[currentNodeId]
if (!node) {
break// Node not found
break // Node not found
}

if (node.message?.author.role === 'system') {
break// Stop at system message
break // Stop at system message
}

result.push(node)

result.unshift(node)
currentNodeId = node.parent
}

return result.reverse()// Reverse the result to start from the root
return result
}

/**
* Merge continuation nodes generated by official continuation
* to improve the readability of the conversation. (#146)
*/
function mergeContinuationNodes(nodes: ConversationNode[]): ConversationNode[] {
const result: ConversationNode[] = []
for (const node of nodes) {
const prevNode = result[result.length - 1]
if (
prevNode?.message?.author.role === 'assistant' && node.message?.author.role === 'assistant'
&& prevNode.message.recipient === 'all' && node.message.recipient === 'all'
&& prevNode.message.content.content_type === 'text' && node.message.content.content_type === 'text'
) {
// the last part of the previous node should directly concat to the first part of the current node
prevNode.message.content.parts[prevNode.message.content.parts.length - 1] += node.message.content.parts[0]
prevNode.message.content.parts.push(...node.message.content.parts.slice(1))
}
else {
result.push(node)
}
}
return result
}
5 changes: 2 additions & 3 deletions src/exporter/html.ts
Expand Up @@ -2,7 +2,7 @@ import JSZip from 'jszip'
import { fetchConversation, getCurrentChatId, processConversation } from '../api'
import { KEY_TIMESTAMP_24H, KEY_TIMESTAMP_ENABLED, KEY_TIMESTAMP_HTML, baseUrl } from '../constants'
import i18n from '../i18n'
import { checkIfConversationStarted, getConversationChoice, getUserAvatar } from '../page'
import { checkIfConversationStarted, getUserAvatar } from '../page'
import templateHtml from '../template.html?raw'
import { downloadFile, getFileNameWithFormat } from '../utils/download'
import { fromMarkdown, toHtml } from '../utils/markdown'
Expand All @@ -22,8 +22,7 @@ export async function exportToHtml(fileNameFormat: string, metaList: ExportMeta[

const chatId = await getCurrentChatId()
const rawConversation = await fetchConversation(chatId, true)
const conversationChoices = getConversationChoice()
const conversation = processConversation(rawConversation, conversationChoices)
const conversation = processConversation(rawConversation)
const html = conversationToHtml(conversation, userAvatar, metaList)

const fileName = getFileNameWithFormat(fileNameFormat, 'html', { title: conversation.title, chatId, createTime: conversation.createTime, updateTime: conversation.updateTime })
Expand Down
5 changes: 2 additions & 3 deletions src/exporter/json.ts
@@ -1,7 +1,7 @@
import JSZip from 'jszip'
import { fetchConversation, getCurrentChatId, processConversation } from '../api'
import i18n from '../i18n'
import { checkIfConversationStarted, getConversationChoice } from '../page'
import { checkIfConversationStarted } from '../page'
import { downloadFile, getFileNameWithFormat } from '../utils/download'
import type { ApiConversationWithId } from '../api'

Expand All @@ -13,8 +13,7 @@ export async function exportToJson(fileNameFormat: string, options: { officialFo

const chatId = await getCurrentChatId()
const rawConversation = await fetchConversation(chatId, false)
const conversationChoices = getConversationChoice()
const conversation = processConversation(rawConversation, conversationChoices)
const conversation = processConversation(rawConversation)

const fileName = getFileNameWithFormat(fileNameFormat, 'json', { title: conversation.title, chatId })
/**
Expand Down
5 changes: 2 additions & 3 deletions src/exporter/markdown.ts
Expand Up @@ -2,7 +2,7 @@ import JSZip from 'jszip'
import { fetchConversation, getCurrentChatId, processConversation } from '../api'
import { KEY_TIMESTAMP_24H, KEY_TIMESTAMP_ENABLED, KEY_TIMESTAMP_HTML, baseUrl } from '../constants'
import i18n from '../i18n'
import { checkIfConversationStarted, getConversationChoice } from '../page'
import { checkIfConversationStarted } from '../page'
import { downloadFile, getFileNameWithFormat } from '../utils/download'
import { fromMarkdown, toMarkdown } from '../utils/markdown'
import { ScriptStorage } from '../utils/storage'
Expand All @@ -19,8 +19,7 @@ export async function exportToMarkdown(fileNameFormat: string, metaList: ExportM

const chatId = await getCurrentChatId()
const rawConversation = await fetchConversation(chatId, true)
const conversationChoices = getConversationChoice()
const conversation = processConversation(rawConversation, conversationChoices)
const conversation = processConversation(rawConversation)
const markdown = conversationToMarkdown(conversation, metaList)

const fileName = getFileNameWithFormat(fileNameFormat, 'md', { title: conversation.title, chatId, createTime: conversation.createTime, updateTime: conversation.updateTime })
Expand Down
5 changes: 2 additions & 3 deletions src/exporter/text.ts
@@ -1,6 +1,6 @@
import { fetchConversation, getCurrentChatId, processConversation } from '../api'
import i18n from '../i18n'
import { checkIfConversationStarted, getConversationChoice } from '../page'
import { checkIfConversationStarted } from '../page'
import { copyToClipboard } from '../utils/clipboard'
import { flatMap, fromMarkdown, toMarkdown } from '../utils/markdown'
import { standardizeLineBreaks } from '../utils/text'
Expand All @@ -18,8 +18,7 @@ export async function exportToText() {
// So we don't need to waste time to download them
const rawConversation = await fetchConversation(chatId, false)

const conversationChoices = getConversationChoice()
const { conversationNodes } = processConversation(rawConversation, conversationChoices)
const { conversationNodes } = processConversation(rawConversation)
const text = conversationNodes
.map(({ message }) => transformMessage(message))
.filter(Boolean)
Expand Down
5 changes: 2 additions & 3 deletions src/main.tsx
@@ -1,7 +1,7 @@
import { render } from 'preact'
import sentinel from 'sentinel-js'
import { fetchConversation, processConversation } from './api'
import { getChatIdFromUrl, getConversationChoice, isSharePage } from './page'
import { getChatIdFromUrl, isSharePage } from './page'
import { Menu } from './ui/Menu'
import { onloadSafe } from './utils/utils'

Expand Down Expand Up @@ -50,8 +50,7 @@ function main() {
chatId = currentChatId

const rawConversation = await fetchConversation(chatId, false)
const conversationChoices = getConversationChoice()
const { conversationNodes } = processConversation(rawConversation, conversationChoices)
const { conversationNodes } = processConversation(rawConversation)

threadContents.forEach((thread, index) => {
const createTime = conversationNodes[index]?.message?.create_time
Expand Down
15 changes: 0 additions & 15 deletions src/page.ts
Expand Up @@ -75,21 +75,6 @@ export function getConversationFromSharePage() {
return null
}

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 = 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
.map(span => Number.parseInt(span?.textContent?.trim().split(' / ')[0] ?? '0') - 1)
.map(x => x === -1 ? null : x)

return conversationChoices
}

const defaultAvatar = 'data:image/svg+xml,%3Csvg%20stroke%3D%22currentColor%22%20fill%3D%22none%22%20stroke-width%3D%221.5%22%20viewBox%3D%22-6%20-6%2036%2036%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20style%3D%22color%3A%20white%3B%20background%3A%20%23ab68ff%3B%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M20%2021v-2a4%204%200%200%200-4-4H8a4%204%200%200%200-4%204v2%22%3E%3C%2Fpath%3E%3Ccircle%20cx%3D%2212%22%20cy%3D%227%22%20r%3D%224%22%3E%3C%2Fcircle%3E%3C%2Fsvg%3E'
export async function getUserAvatar(): Promise<string> {
try {
Expand Down

0 comments on commit b1ff972

Please sign in to comment.