mirror of
https://github.com/pionxzh/chatgpt-exporter.git
synced 2026-07-23 09:00:51 -05:00
feat: add project support
Add support for getting project list. OpenAI calls these 'gizmos' and they include custom GPTs and other apps. Only projects are supported for now. Add project selection box in Export All dialog.
This commit is contained in:
52
src/api.ts
52
src/api.ts
@@ -202,7 +202,21 @@ export interface ApiConversations {
|
||||
items: ApiConversationItem[]
|
||||
limit: number
|
||||
offset: number
|
||||
total: number
|
||||
total: number | null
|
||||
}
|
||||
|
||||
/// "Gizmos" are what OpenAI calls "projects" or other GPTs in the UI
|
||||
export interface ApiGizmo {
|
||||
// weird nesting but ok
|
||||
gizmo: { gizmo: ApiProjectInfo }
|
||||
conversations: { itmes: ApiConversationItem[] }
|
||||
}
|
||||
|
||||
export interface ApiProjectInfo {
|
||||
id: string
|
||||
organization_id: string
|
||||
display: { name: string; description: string }
|
||||
// todo: support exporting project context
|
||||
}
|
||||
|
||||
interface ApiAccountsCheckAccountDetail {
|
||||
@@ -286,6 +300,8 @@ const sessionApi = urlcat(baseUrl, '/api/auth/session')
|
||||
const conversationApi = (id: string) => urlcat(apiUrl, '/conversation/:id', { id })
|
||||
const conversationsApi = (offset: number, limit: number) => urlcat(apiUrl, '/conversations', { offset, limit })
|
||||
const fileDownloadApi = (id: string) => urlcat(apiUrl, '/files/:id/download', { id })
|
||||
const projectsApi = () => urlcat(apiUrl, '/gizmos/snorlax/sidebar', { conversations_per_gizmo: 0 })
|
||||
const projectConversationsApi = (gizmo: string, offset: number, limit: number) => urlcat(apiUrl, '/gizmos/:gizmo/conversations', { gizmo, cursor: offset, limit })
|
||||
const accountsCheckApi = urlcat(apiUrl, '/accounts/check/v4-2023-04-27')
|
||||
|
||||
export async function getCurrentChatId(): Promise<string> {
|
||||
@@ -390,19 +406,41 @@ export async function fetchConversation(chatId: string, shouldReplaceAssets: boo
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchConversations(offset = 0, limit = 20): Promise<ApiConversations> {
|
||||
export async function fetchProjects(): Promise<ApiProjectInfo[]> {
|
||||
const url = projectsApi()
|
||||
const { items } = await fetchApi<{ items: ApiGizmo[] }>(url)
|
||||
return items.map(gizmo => (gizmo.gizmo.gizmo))
|
||||
}
|
||||
|
||||
async function fetchConversations(offset = 0, limit = 20, project: string | null = null): Promise<ApiConversations> {
|
||||
if (project) {
|
||||
return fetchProjectConversations(project, offset, limit)
|
||||
}
|
||||
const url = conversationsApi(offset, limit)
|
||||
return fetchApi(url)
|
||||
}
|
||||
|
||||
export async function fetchAllConversations(): Promise<ApiConversationItem[]> {
|
||||
async function fetchProjectConversations(project: string, offset = 0, limit = 20): Promise<ApiConversations> {
|
||||
const url = projectConversationsApi(project, offset, limit)
|
||||
const { items } = await fetchApi< { items: ApiConversationItem[]; cursor: number | null }>(url)
|
||||
return {
|
||||
has_missing_conversations: false,
|
||||
items,
|
||||
limit,
|
||||
offset,
|
||||
total: null,
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAllConversations(project: string | null = null): Promise<ApiConversationItem[]> {
|
||||
const conversations: ApiConversationItem[] = []
|
||||
const limit = 100
|
||||
const limit = project === null ? 100 : 50 // gizmos api uses a smaller limit
|
||||
let offset = 0
|
||||
while (true) {
|
||||
const result = await fetchConversations(offset, limit)
|
||||
const result = await fetchConversations(offset, limit, project)
|
||||
conversations.push(...result.items)
|
||||
if (offset + limit >= result.total) break
|
||||
if (result.total !== null && offset + limit >= result.total) break
|
||||
if (result.items.length === 0) break
|
||||
if (offset + limit >= 1000) break
|
||||
offset += limit
|
||||
}
|
||||
@@ -506,6 +544,8 @@ export interface ConversationResult {
|
||||
createTime: number
|
||||
updateTime: number
|
||||
conversationNodes: ConversationNode[]
|
||||
projectName?: string
|
||||
projectId?: string
|
||||
}
|
||||
|
||||
const ModelMapping: { [key in ModelSlug]: string } & { [key: string]: string } = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { archiveConversation, deleteConversation, fetchAllConversations, fetchConversation } from '../api'
|
||||
import { archiveConversation, deleteConversation, fetchAllConversations, fetchConversation, fetchProjects } from '../api'
|
||||
import { exportAllToHtml } from '../exporter/html'
|
||||
import { exportAllToJson, exportAllToOfficialJson } from '../exporter/json'
|
||||
import { exportAllToMarkdown } from '../exporter/markdown'
|
||||
@@ -9,10 +9,42 @@ import { RequestQueue } from '../utils/queue'
|
||||
import { CheckBox } from './CheckBox'
|
||||
import { IconCross, IconUpload } from './Icons'
|
||||
import { useSettingContext } from './SettingContext'
|
||||
import type { ApiConversationItem, ApiConversationWithId } from '../api'
|
||||
import type { ApiConversationItem, ApiConversationWithId, ApiProjectInfo } from '../api'
|
||||
import type { FC } from '../type'
|
||||
import type { ChangeEvent } from 'preact/compat'
|
||||
|
||||
interface ProjectSelectProps {
|
||||
projects: ApiProjectInfo[]
|
||||
selected: ApiProjectInfo | null
|
||||
setSelected: (selected: ApiProjectInfo | null) => void
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
const ProjectSelect: FC<ProjectSelectProps> = ({ projects, selected, setSelected, disabled }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex items-center text-gray-600 dark:text-gray-300 flex justify-between mb-3">
|
||||
{t('Select Project')}
|
||||
<select
|
||||
disabled={disabled}
|
||||
className="Select"
|
||||
value={selected?.id || ''}
|
||||
onChange={(e) => {
|
||||
const projectId = e.currentTarget.value
|
||||
const project = projects.find(p => p.id === projectId)
|
||||
setSelected(project || null)
|
||||
}}
|
||||
>
|
||||
<option value="">{t('(no project)')}</option>
|
||||
{projects.map(project => (
|
||||
<option key={project.id} value={project.id}>{project.display.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ConversationSelectProps {
|
||||
conversations: ApiConversationItem[]
|
||||
selected: ApiConversationItem[]
|
||||
@@ -47,7 +79,8 @@ const ConversationSelect: FC<ConversationSelectProps> = ({
|
||||
<ul className="SelectList">
|
||||
{loading && <li className="SelectItem">{t('Loading')}...</li>}
|
||||
{error && <li className="SelectItem">{t('Error')}: {error}</li>}
|
||||
{conversations.map(c => (
|
||||
{!loading && !error
|
||||
&& conversations.map(c => (
|
||||
<li className="SelectItem" key={c.id}>
|
||||
<CheckBox
|
||||
label={c.title}
|
||||
@@ -90,9 +123,11 @@ const DialogContent: FC<DialogContentProps> = ({ format }) => {
|
||||
const [apiConversations, setApiConversations] = useState<ApiConversationItem[]>([])
|
||||
const [localConversations, setLocalConversations] = useState<ApiConversationWithId[]>([])
|
||||
const conversations = exportSource === 'API' ? apiConversations : localConversations
|
||||
const [projects, setProjects] = useState<ApiProjectInfo[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [processing, setProcessing] = useState(false)
|
||||
const [selectedProject, setSelectedProject] = useState<ApiProjectInfo | null>(null)
|
||||
|
||||
const [selected, setSelected] = useState<ApiConversationItem[]>([])
|
||||
const [exportType, setExportType] = useState(exportAllOptions[0].label)
|
||||
@@ -246,13 +281,19 @@ const DialogContent: FC<DialogContentProps> = ({ format }) => {
|
||||
}, [disabled, selected, archiveQueue, t])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
fetchAllConversations()
|
||||
.then(setApiConversations)
|
||||
.catch(setError)
|
||||
.finally(() => setLoading(false))
|
||||
fetchProjects()
|
||||
.then(setProjects)
|
||||
.catch(err => setError(err.toString()))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
fetchAllConversations(selectedProject?.id)
|
||||
.then(setApiConversations)
|
||||
.catch(err => setError(err.toString()))
|
||||
.finally(() => setLoading(false))
|
||||
}, [selectedProject])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog.Title className="DialogTitle">{t('Export Dialog Title')}</Dialog.Title>
|
||||
@@ -276,6 +317,7 @@ const DialogContent: FC<DialogContentProps> = ({ format }) => {
|
||||
{t('Export from API')}
|
||||
</div>
|
||||
)}
|
||||
<ProjectSelect projects={projects} selected={selectedProject} setSelected={setSelectedProject} disabled={processing || loading} />
|
||||
<ConversationSelect
|
||||
conversations={conversations}
|
||||
selected={selected}
|
||||
|
||||
Reference in New Issue
Block a user