Skip to content

Commit

Permalink
feat: basic support for response from plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
pionxzh committed Apr 23, 2023
1 parent a50d580 commit c45fdbe
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 76 deletions.
93 changes: 72 additions & 21 deletions packages/userscript/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,71 @@ interface ApiSession {
}
}

type ModelSlug = 'text-davinci-002-render-sha' | 'text-davinci-002-render-paid' | 'gpt-4'
type ModelSlug = 'text-davinci-002-render-sha' | 'text-davinci-002-render-paid' | 'text-davinci-002-browse' | 'gpt-4'

interface MessageMeta {
command: 'click' | 'search' | 'quote' | 'scroll' & (string & {})
finish_details?: {
stop: string
type: 'stop' | 'interrupted' & (string & {})
}
model_slug?: ModelSlug & (string & {})
timestamp_: 'absolute' & (string & {})
_cite_metadata?: {
citation_format: {
name: 'tether_og' & (string & {})
}
metadata_list: Array<{
title: string
url: string
text: string
}>
}
}

export type AuthorRole = 'system' | 'assistant' | 'user' | 'tool'

export interface ConversationNodeMessage {
author: {
role: AuthorRole
name?: 'browser' & (string & {})
metadata: unknown
}
content: {
// chat response
content_type: 'text'
parts: string[]
} | {
// plugin response
content_type: 'code'
language: 'unknown' & (string & {})
text: string
} | {
content_type: 'tether_quote'
domain?: string
text: string
title: string
url?: string
} | {
content_type: 'tether_browsing_code'
// unknown
} | {
content_type: 'tether_browsing_display'
result: string
summary?: string
}
create_time: number
end_turn: boolean
id: string
metadata?: MessageMeta
recipient: 'all' & 'browser' & (string & {})
weight: number
}

interface ConversationNode {
export interface ConversationNode {
children: string[]
id: string
message?: {
author: {
role: 'system' | 'assistant' | 'user'
metadata: unknown
}
content: {
content_type: 'text' & (string & {})
parts: string[]
}
create_time: number
end_turn: boolean
id: string
metadata?: MessageMeta
recipient: 'all' & (string & {})
weight: number
}
message?: ConversationNodeMessage
parent?: string
}

Expand Down Expand Up @@ -184,17 +219,33 @@ export interface ConversationResult {
conversationNodes: ConversationNode[]
}

const modelMapping: { [key in ModelSlug]: 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',
'gpt-4': 'GPT-4',

// fuzzy matching
'text-davinci-002': 'GTP-3.5',
}

export function processConversation(conversation: ApiConversationWithId, conversationChoices: Array<number | null> = []): ConversationResult {
const title = conversation.title || 'ChatGPT Conversation'
const createTime = conversation.create_time
const modelSlug = Object.values(conversation.mapping).find(node => node.message?.metadata?.model_slug)?.message?.metadata?.model_slug || ''
const model = modelSlug ? (modelMapping[modelSlug] || '') : ''
let model = ''
if (modelSlug) {
if (modelMapping[modelSlug]) {
model = modelMapping[modelSlug]
}
else {
Object.keys(modelMapping).forEach((key) => {
if (modelSlug.startsWith(key)) {
model = key
}
})
}
}

const result: ConversationNode[] = []
const nodes = Object.values(conversation.mapping)
Expand All @@ -211,7 +262,7 @@ export function processConversation(conversation: ApiConversationWithId, convers
if (!node) throw new Error('No node found.')

const role = node.message?.author.role
if (role === 'assistant' || role === 'user') {
if (role === 'assistant' || role === 'user' || role === 'tool') {
result.push(node)
}

Expand Down
76 changes: 61 additions & 15 deletions packages/userscript/src/exporter/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { fromMarkdown, toHtml } from '../utils/markdown'
import { ScriptStorage } from '../utils/storage'
import { standardizeLineBreaks } from '../utils/text'
import { dateStr, getColorScheme, timestamp } from '../utils/utils'
import type { ApiConversationWithId, ConversationResult } from '../api'
import type { ApiConversationWithId, ConversationNodeMessage, ConversationResult } from '../api'
import type { ExportMeta } from '../ui/SettingContext'

export async function exportToHtml(fileNameFormat: string, metaList: ExportMeta[]) {
Expand Down Expand Up @@ -61,30 +61,76 @@ export async function exportAllToHtml(fileNameFormat: string, apiConversations:
return true
}

const transformAuthor = (author: ConversationNodeMessage['author']): string => {
switch (author.role) {
case 'assistant':
return 'ChatGPT'
case 'user':
return 'You'
case 'tool':
return `Plugin${author.name ? ` (${author.name})` : ''}`
default:
return author.role
}
}

/**
* Convert the content based on the type of message
*/
const transformContent = (
content: ConversationNodeMessage['content'],
metadata: ConversationNodeMessage['metadata'],
) => {
switch (content.content_type) {
case 'text':
return content.parts?.join('\n') || ''
case 'code':
return content.text || ''
case 'tether_quote':
return `> ${content.title || content.text || ''}`
case 'tether_browsing_code':
return '' // TODO: implement
case 'tether_browsing_display': {
const metadataList = metadata?._cite_metadata?.metadata_list
if (Array.isArray(metadataList) && metadataList.length > 0) {
return metadataList.map(({ title, url }) => {
return `> [${title}](${url})`
}).join('\n')
}
return ''
}
default:
return ''
}
}

function conversationToHtml(conversation: ConversationResult, avatar: string, metaList?: ExportMeta[]) {
const { id, title, model, modelSlug, conversationNodes } = conversation

const conversationHtml = conversationNodes.map((item) => {
const author = item.message?.author.role === 'assistant' ? 'ChatGPT' : 'You'
const model = item.message?.metadata?.model_slug === 'gpt-4' ? 'GPT-4' : 'GPT-3'
const authorType = author === 'ChatGPT' ? model : 'user'
const avatarEl = author === 'ChatGPT'
? '<svg width="41" height="41"><use xlink:href="#chatgpt" /></svg>'
: `<img alt="${author}" />`
const content = item.message?.content.parts?.join('\n') ?? ''
const conversationHtml = conversationNodes.map(({ message }) => {
if (!message || !message.content) return null

const isUser = message.author.role === 'user'
const author = transformAuthor(message.author)
const model = message?.metadata?.model_slug === 'gpt-4' ? 'GPT-4' : 'GPT-3'
const authorType = isUser ? 'user' : model
const avatarEl = isUser
? `<img alt="${author}" />`
: '<svg width="41" height="41"><use xlink:href="#chatgpt" /></svg>'
const content = transformContent(message.content, message.metadata)
let conversationContent = content

if (author === 'ChatGPT') {
const root = fromMarkdown(content)
conversationContent = toHtml(root)
if (isUser) {
conversationContent = `<p>${escapeHtml(content)}</p>`
}
else {
conversationContent = `<p>${escapeHtml(content)}</p>`
const root = fromMarkdown(content)
conversationContent = toHtml(root)
}

const enableTimestamp = ScriptStorage.get<boolean>(KEY_TIMESTAMP_ENABLED) ?? false
const timeStamp24H = ScriptStorage.get<boolean>(KEY_TIMESTAMP_24H) ?? false
const timestamp = item.message?.create_time ?? ''
const timestamp = message?.create_time ?? ''
const showTimestamp = enableTimestamp && timestamp
let conversationDate = ''
let conversationTime = ''
Expand Down Expand Up @@ -112,7 +158,7 @@ function conversationToHtml(conversation: ConversationResult, avatar: string, me
</div>
${showTimestamp ? `<div class="time" title="${conversationDate}">${conversationTime}</div>` : ''}
</div>`
}).join('\n\n')
}).filter(Boolean).join('\n\n')

const date = dateStr()
const time = new Date().toISOString()
Expand Down
63 changes: 54 additions & 9 deletions packages/userscript/src/exporter/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { downloadFile, getFileNameWithFormat } from '../utils/download'
import { fromMarkdown, toMarkdown } from '../utils/markdown'
import { standardizeLineBreaks } from '../utils/text'
import { dateStr, timestamp } from '../utils/utils'
import type { ApiConversationWithId, ConversationResult } from '../api'
import type { ApiConversationWithId, ConversationNodeMessage, ConversationResult } from '../api'
import type { ExportMeta } from '../ui/SettingContext'

export async function exportToMarkdown(fileNameFormat: string, metaList: ExportMeta[]) {
Expand Down Expand Up @@ -55,6 +55,49 @@ export async function exportAllToMarkdown(fileNameFormat: string, apiConversatio
return true
}

const transformAuthor = (author: ConversationNodeMessage['author']): string => {
switch (author.role) {
case 'assistant':
return 'ChatGPT'
case 'user':
return 'You'
case 'tool':
return `Plugin${author.name ? ` (${author.name})` : ''}`
default:
return author.role
}
}

/**
* Convert the content based on the type of message
*/
const transformContent = (
content: ConversationNodeMessage['content'],
metadata: ConversationNodeMessage['metadata'],
) => {
switch (content.content_type) {
case 'text':
return content.parts?.join('\n') || ''
case 'code':
return content.text || ''
case 'tether_quote':
return `> ${content.title || content.text || ''}`
case 'tether_browsing_code':
return '' // TODO: implement
case 'tether_browsing_display': {
const metadataList = metadata?._cite_metadata?.metadata_list
if (Array.isArray(metadataList) && metadataList.length > 0) {
return metadataList.map(({ title, url }) => {
return `> [${title}](${url})`
}).join('\n')
}
return ''
}
default:
return ''
}
}

function conversationToMarkdown(conversation: ConversationResult, metaList?: ExportMeta[]) {
const { id, title, model, modelSlug, conversationNodes } = conversation
const source = `${baseUrl}/c/${id}`
Expand All @@ -77,18 +120,20 @@ function conversationToMarkdown(conversation: ConversationResult, metaList?: Exp
? `---\n${_metaList.join('\n')}\n---\n\n`
: ''

const content = conversationNodes.map((item) => {
const author = item.message?.author.role === 'assistant' ? 'ChatGPT' : 'You'
const content = item.message?.content.parts?.join('\n') ?? ''
let message = content
const content = conversationNodes.map(({ message }) => {
if (!message || !message.content) return null

const isUser = message.author.role === 'user'
const author = transformAuthor(message.author)
let content = transformContent(message.content, message.metadata)

// User's message will not be reformatted
if (author === 'ChatGPT') {
if (!isUser && content) {
const root = fromMarkdown(content)
message = toMarkdown(root)
content = toMarkdown(root)
}
return `#### ${author}:\n${message}`
}).join('\n\n')
return `#### ${author}:\n${content}`
}).filter(Boolean).join('\n\n')

const markdown = `${frontMatter}# ${title}\n\n${content}`

Expand Down
Loading

0 comments on commit c45fdbe

Please sign in to comment.