Art vandelay: Import & Export (#5053)

Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Douwe Osinga
2025-10-07 21:07:40 -04:00
committed by GitHub
parent 00302f5c89
commit b179be4cff
7 changed files with 467 additions and 2 deletions
@@ -7,6 +7,8 @@ import {
Folder,
Edit2,
Trash2,
Download,
Upload,
} from 'lucide-react';
import { Card } from '../ui/card';
import { Button } from '../ui/button';
@@ -19,7 +21,14 @@ import { groupSessionsByDate, type DateGroup } from '../../utils/dateUtils';
import { Skeleton } from '../ui/skeleton';
import { toast } from 'react-toastify';
import { ConfirmationModal } from '../ui/ConfirmationModal';
import { deleteSession, listSessions, Session, updateSessionDescription } from '../../api';
import {
deleteSession,
exportSession,
importSession,
listSessions,
Session,
updateSessionDescription,
} from '../../api';
interface EditSessionModalProps {
session: Session | null;
@@ -210,6 +219,8 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
}
};
const fileInputRef = useRef<HTMLInputElement>(null);
const visibleDateGroups = useMemo(() => {
return dateGroups.slice(0, visibleGroupsCount);
}, [dateGroups, visibleGroupsCount]);
@@ -427,14 +438,66 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
setSessionToDelete(null);
}, []);
const handleExportSession = useCallback(async (session: Session, e: React.MouseEvent) => {
e.stopPropagation();
const response = await exportSession({
path: { session_id: session.id },
throwOnError: true,
});
const json = response.data;
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${session.description || session.id}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success('Session exported successfully');
}, []);
const handleImportClick = useCallback(() => {
fileInputRef.current?.click();
}, []);
const handleImportSession = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
try {
const json = await file.text();
await importSession({
body: { json },
throwOnError: true,
});
toast.success('Session imported successfully');
await loadSessions();
} catch (error) {
toast.error(`Failed to import session: ${error}`);
} finally {
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
},
[loadSessions]
);
const SessionItem = React.memo(function SessionItem({
session,
onEditClick,
onDeleteClick,
onExportClick,
}: {
session: Session;
onEditClick: (session: Session) => void;
onDeleteClick: (session: Session) => void;
onExportClick: (session: Session, e: React.MouseEvent) => void;
}) {
const handleEditClick = useCallback(
(e: React.MouseEvent) => {
@@ -456,6 +519,13 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
onSelectSession(session.id);
}, [session.id]);
const handleExportClick = useCallback(
(e: React.MouseEvent) => {
onExportClick(session, e);
},
[onExportClick, session]
);
return (
<Card
onClick={handleCardClick}
@@ -477,6 +547,13 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
>
<Trash2 className="w-3 h-3 text-red-500 hover:text-red-600" />
</button>
<button
onClick={handleExportClick}
className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer"
title="Export session"
>
<Download className="w-3 h-3 text-textSubtle hover:text-textStandard" />
</button>
</div>
<div className="flex-1">
@@ -598,6 +675,7 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
session={session}
onEditClick={handleEditSession}
onDeleteClick={handleDeleteSession}
onExportClick={handleExportSession}
/>
))}
</div>
@@ -624,6 +702,15 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
<div className="flex flex-col page-transition">
<div className="flex justify-between items-center mb-1">
<h1 className="text-4xl font-light">Chat history</h1>
<Button
onClick={handleImportClick}
variant="outline"
size="sm"
className="flex items-center gap-2"
>
<Upload className="w-4 h-4" />
Import Session
</Button>
</div>
<p className="text-sm text-text-muted mb-4">
View and search your past conversations with Goose.
@@ -701,6 +788,14 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
</div>
</MainPanelLayout>
<input
ref={fileInputRef}
type="file"
accept=".json"
onChange={handleImportSession}
className="hidden"
/>
<EditSessionModal
session={editingSession}
isOpen={showEditModal}