Skip to content

Commit 9ddd11f

Browse files
yyogopionxzh
authored andcommitted
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.
1 parent d3b73cb commit 9ddd11f

File tree

2 files changed

+94
-12
lines changed

2 files changed

+94
-12
lines changed

src/api.ts

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,21 @@ export interface ApiConversations {
202202
items: ApiConversationItem[]
203203
limit: number
204204
offset: number
205-
total: number
205+
total: number | null
206+
}
207+
208+
/// "Gizmos" are what OpenAI calls "projects" or other GPTs in the UI
209+
export interface ApiGizmo {
210+
// weird nesting but ok
211+
gizmo: { gizmo: ApiProjectInfo }
212+
conversations: { itmes: ApiConversationItem[] }
213+
}
214+
215+
export interface ApiProjectInfo {
216+
id: string
217+
organization_id: string
218+
display: { name: string; description: string }
219+
// todo: support exporting project context
206220
}
207221

208222
interface ApiAccountsCheckAccountDetail {
@@ -286,6 +300,8 @@ const sessionApi = urlcat(baseUrl, '/api/auth/session')
286300
const conversationApi = (id: string) => urlcat(apiUrl, '/conversation/:id', { id })
287301
const conversationsApi = (offset: number, limit: number) => urlcat(apiUrl, '/conversations', { offset, limit })
288302
const fileDownloadApi = (id: string) => urlcat(apiUrl, '/files/:id/download', { id })
303+
const projectsApi = () => urlcat(apiUrl, '/gizmos/snorlax/sidebar', { conversations_per_gizmo: 0 })
304+
const projectConversationsApi = (gizmo: string, offset: number, limit: number) => urlcat(apiUrl, '/gizmos/:gizmo/conversations', { gizmo, cursor: offset, limit })
289305
const accountsCheckApi = urlcat(apiUrl, '/accounts/check/v4-2023-04-27')
290306

291307
export async function getCurrentChatId(): Promise<string> {
@@ -390,19 +406,41 @@ export async function fetchConversation(chatId: string, shouldReplaceAssets: boo
390406
}
391407
}
392408

393-
async function fetchConversations(offset = 0, limit = 20): Promise<ApiConversations> {
409+
export async function fetchProjects(): Promise<ApiProjectInfo[]> {
410+
const url = projectsApi()
411+
const { items } = await fetchApi<{ items: ApiGizmo[] }>(url)
412+
return items.map(gizmo => (gizmo.gizmo.gizmo))
413+
}
414+
415+
async function fetchConversations(offset = 0, limit = 20, project: string | null = null): Promise<ApiConversations> {
416+
if (project) {
417+
return fetchProjectConversations(project, offset, limit)
418+
}
394419
const url = conversationsApi(offset, limit)
395420
return fetchApi(url)
396421
}
397422

398-
export async function fetchAllConversations(): Promise<ApiConversationItem[]> {
423+
async function fetchProjectConversations(project: string, offset = 0, limit = 20): Promise<ApiConversations> {
424+
const url = projectConversationsApi(project, offset, limit)
425+
const { items } = await fetchApi< { items: ApiConversationItem[]; cursor: number | null }>(url)
426+
return {
427+
has_missing_conversations: false,
428+
items,
429+
limit,
430+
offset,
431+
total: null,
432+
}
433+
}
434+
435+
export async function fetchAllConversations(project: string | null = null): Promise<ApiConversationItem[]> {
399436
const conversations: ApiConversationItem[] = []
400-
const limit = 100
437+
const limit = project === null ? 100 : 50 // gizmos api uses a smaller limit
401438
let offset = 0
402439
while (true) {
403-
const result = await fetchConversations(offset, limit)
440+
const result = await fetchConversations(offset, limit, project)
404441
conversations.push(...result.items)
405-
if (offset + limit >= result.total) break
442+
if (result.total !== null && offset + limit >= result.total) break
443+
if (result.items.length === 0) break
406444
if (offset + limit >= 1000) break
407445
offset += limit
408446
}
@@ -506,6 +544,8 @@ export interface ConversationResult {
506544
createTime: number
507545
updateTime: number
508546
conversationNodes: ConversationNode[]
547+
projectName?: string
548+
projectId?: string
509549
}
510550

511551
const ModelMapping: { [key in ModelSlug]: string } & { [key: string]: string } = {

src/ui/ExportDialog.tsx

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,50 @@
11
import * as Dialog from '@radix-ui/react-dialog'
22
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks'
33
import { useTranslation } from 'react-i18next'
4-
import { archiveConversation, deleteConversation, fetchAllConversations, fetchConversation } from '../api'
4+
import { archiveConversation, deleteConversation, fetchAllConversations, fetchConversation, fetchProjects } from '../api'
55
import { exportAllToHtml } from '../exporter/html'
66
import { exportAllToJson, exportAllToOfficialJson } from '../exporter/json'
77
import { exportAllToMarkdown } from '../exporter/markdown'
88
import { RequestQueue } from '../utils/queue'
99
import { CheckBox } from './CheckBox'
1010
import { IconCross, IconUpload } from './Icons'
1111
import { useSettingContext } from './SettingContext'
12-
import type { ApiConversationItem, ApiConversationWithId } from '../api'
12+
import type { ApiConversationItem, ApiConversationWithId, ApiProjectInfo } from '../api'
1313
import type { FC } from '../type'
1414
import type { ChangeEvent } from 'preact/compat'
1515

16+
interface ProjectSelectProps {
17+
projects: ApiProjectInfo[]
18+
selected: ApiProjectInfo | null
19+
setSelected: (selected: ApiProjectInfo | null) => void
20+
disabled: boolean
21+
}
22+
23+
const ProjectSelect: FC<ProjectSelectProps> = ({ projects, selected, setSelected, disabled }) => {
24+
const { t } = useTranslation()
25+
26+
return (
27+
<div className="flex items-center text-gray-600 dark:text-gray-300 flex justify-between mb-3">
28+
{t('Select Project')}
29+
<select
30+
disabled={disabled}
31+
className="Select"
32+
value={selected?.id || ''}
33+
onChange={(e) => {
34+
const projectId = e.currentTarget.value
35+
const project = projects.find(p => p.id === projectId)
36+
setSelected(project || null)
37+
}}
38+
>
39+
<option value="">{t('(no project)')}</option>
40+
{projects.map(project => (
41+
<option key={project.id} value={project.id}>{project.display.name}</option>
42+
))}
43+
</select>
44+
</div>
45+
)
46+
}
47+
1648
interface ConversationSelectProps {
1749
conversations: ApiConversationItem[]
1850
selected: ApiConversationItem[]
@@ -47,7 +79,8 @@ const ConversationSelect: FC<ConversationSelectProps> = ({
4779
<ul className="SelectList">
4880
{loading && <li className="SelectItem">{t('Loading')}...</li>}
4981
{error && <li className="SelectItem">{t('Error')}: {error}</li>}
50-
{conversations.map(c => (
82+
{!loading && !error
83+
&& conversations.map(c => (
5184
<li className="SelectItem" key={c.id}>
5285
<CheckBox
5386
label={c.title}
@@ -90,9 +123,11 @@ const DialogContent: FC<DialogContentProps> = ({ format }) => {
90123
const [apiConversations, setApiConversations] = useState<ApiConversationItem[]>([])
91124
const [localConversations, setLocalConversations] = useState<ApiConversationWithId[]>([])
92125
const conversations = exportSource === 'API' ? apiConversations : localConversations
126+
const [projects, setProjects] = useState<ApiProjectInfo[]>([])
93127
const [loading, setLoading] = useState(false)
94128
const [error, setError] = useState('')
95129
const [processing, setProcessing] = useState(false)
130+
const [selectedProject, setSelectedProject] = useState<ApiProjectInfo | null>(null)
96131

97132
const [selected, setSelected] = useState<ApiConversationItem[]>([])
98133
const [exportType, setExportType] = useState(exportAllOptions[0].label)
@@ -245,13 +280,19 @@ const DialogContent: FC<DialogContentProps> = ({ format }) => {
245280
archiveQueue.start()
246281
}, [disabled, selected, archiveQueue, t])
247282

283+
useEffect(() => {
284+
fetchProjects()
285+
.then(setProjects)
286+
.catch(err => setError(err.toString()))
287+
}, [])
288+
248289
useEffect(() => {
249290
setLoading(true)
250-
fetchAllConversations()
291+
fetchAllConversations(selectedProject?.id)
251292
.then(setApiConversations)
252-
.catch(setError)
293+
.catch(err => setError(err.toString()))
253294
.finally(() => setLoading(false))
254-
}, [])
295+
}, [selectedProject])
255296

256297
return (
257298
<>
@@ -276,6 +317,7 @@ const DialogContent: FC<DialogContentProps> = ({ format }) => {
276317
{t('Export from API')}
277318
</div>
278319
)}
320+
<ProjectSelect projects={projects} selected={selectedProject} setSelected={setSelectedProject} disabled={processing || loading} />
279321
<ConversationSelect
280322
conversations={conversations}
281323
selected={selected}

0 commit comments

Comments
 (0)