Export chat from sidebar
Browse files
app/components/sidebar/HistoryItem.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import * as Dialog from '@radix-ui/react-dialog';
|
| 2 |
import { useEffect, useRef, useState } from 'react';
|
| 3 |
import { type ChatHistoryItem } from '~/lib/persistence';
|
|
|
|
| 4 |
|
| 5 |
interface HistoryItemProps {
|
| 6 |
item: ChatHistoryItem;
|
|
@@ -43,9 +44,17 @@ export function HistoryItem({ item, onDelete, onDuplicate }: HistoryItemProps) {
|
|
| 43 |
>
|
| 44 |
<a href={`/chat/${item.urlId}`} className="flex w-full relative truncate block">
|
| 45 |
{item.description}
|
| 46 |
-
<div className="absolute right-0 z-1 top-0 bottom-0 bg-gradient-to-l from-bolt-elements-background-depth-2 group-hover:from-bolt-elements-background-depth-3 to-transparent w-10 flex justify-end group-hover:w-15 group-hover:from-
|
| 47 |
{hovering && (
|
| 48 |
<div className="flex items-center p-1 text-bolt-elements-textSecondary">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
{onDuplicate && (
|
| 50 |
<button
|
| 51 |
className="i-ph:copy scale-110 mr-2"
|
|
|
|
| 1 |
import * as Dialog from '@radix-ui/react-dialog';
|
| 2 |
import { useEffect, useRef, useState } from 'react';
|
| 3 |
import { type ChatHistoryItem } from '~/lib/persistence';
|
| 4 |
+
import { exportChat } from '~/utils/chatExport';
|
| 5 |
|
| 6 |
interface HistoryItemProps {
|
| 7 |
item: ChatHistoryItem;
|
|
|
|
| 44 |
>
|
| 45 |
<a href={`/chat/${item.urlId}`} className="flex w-full relative truncate block">
|
| 46 |
{item.description}
|
| 47 |
+
<div className="absolute right-0 z-1 top-0 bottom-0 bg-gradient-to-l from-bolt-elements-background-depth-2 group-hover:from-bolt-elements-background-depth-3 box-content pl-3 to-transparent w-10 flex justify-end group-hover:w-15 group-hover:from-99%">
|
| 48 |
{hovering && (
|
| 49 |
<div className="flex items-center p-1 text-bolt-elements-textSecondary">
|
| 50 |
+
<button
|
| 51 |
+
className="i-ph:download-simple scale-110 mr-2"
|
| 52 |
+
onClick={(event) => {
|
| 53 |
+
event.preventDefault();
|
| 54 |
+
exportChat(item.messages, item.description);
|
| 55 |
+
}}
|
| 56 |
+
title="Export chat"
|
| 57 |
+
/>
|
| 58 |
{onDuplicate && (
|
| 59 |
<button
|
| 60 |
className="i-ph:copy scale-110 mr-2"
|
app/utils/chatExport.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Message } from 'ai';
|
| 2 |
+
import { toast } from 'react-toastify';
|
| 3 |
+
|
| 4 |
+
export interface ChatExportData {
|
| 5 |
+
messages: Message[];
|
| 6 |
+
description?: string;
|
| 7 |
+
exportDate: string;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export const exportChat = (messages: Message[], description?: string) => {
|
| 11 |
+
const chatData: ChatExportData = {
|
| 12 |
+
messages,
|
| 13 |
+
description,
|
| 14 |
+
exportDate: new Date().toISOString(),
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
const blob = new Blob([JSON.stringify(chatData, null, 2)], { type: 'application/json' });
|
| 18 |
+
const url = URL.createObjectURL(blob);
|
| 19 |
+
const a = document.createElement('a');
|
| 20 |
+
a.href = url;
|
| 21 |
+
a.download = `chat-${new Date().toISOString()}.json`;
|
| 22 |
+
document.body.appendChild(a);
|
| 23 |
+
a.click();
|
| 24 |
+
document.body.removeChild(a);
|
| 25 |
+
URL.revokeObjectURL(url);
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
export const importChat = async (file: File): Promise<ChatExportData> => {
|
| 29 |
+
return new Promise((resolve, reject) => {
|
| 30 |
+
const reader = new FileReader();
|
| 31 |
+
reader.onload = (e) => {
|
| 32 |
+
try {
|
| 33 |
+
const content = e.target?.result as string;
|
| 34 |
+
const data = JSON.parse(content);
|
| 35 |
+
if (!Array.isArray(data.messages)) {
|
| 36 |
+
throw new Error('Invalid chat file format');
|
| 37 |
+
}
|
| 38 |
+
resolve(data);
|
| 39 |
+
} catch (error) {
|
| 40 |
+
reject(new Error('Failed to parse chat file'));
|
| 41 |
+
}
|
| 42 |
+
};
|
| 43 |
+
reader.onerror = () => reject(new Error('Failed to read chat file'));
|
| 44 |
+
reader.readAsText(file);
|
| 45 |
+
});
|
| 46 |
+
};
|