mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 240c1e2c8a |
@@ -0,0 +1,314 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, Button, Modal, ModalClose, ModalDialog, Typography } from '@mui/joy';
|
||||
import WarningRoundedIcon from '@mui/icons-material/WarningRounded';
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||
|
||||
import { DConversation, DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { useChatStore } from '~/common/stores/chat/store-chats';
|
||||
|
||||
import { parseTypingMindExport } from './typingmind.parser';
|
||||
import { convertTypingMindChatToConversation } from './typingmind.transformer';
|
||||
import type { ImportedOutcome } from '../../trade/ImportOutcomeModal';
|
||||
|
||||
interface ImportWarning {
|
||||
type: 'warning' | 'error';
|
||||
message: string;
|
||||
chatId?: string;
|
||||
}
|
||||
|
||||
interface ParsedData {
|
||||
totalChats: number;
|
||||
validChats: number;
|
||||
warnings: ImportWarning[];
|
||||
conversations: DConversation[];
|
||||
}
|
||||
|
||||
type ImportStep = 'confirm' | 'importing' | 'complete';
|
||||
|
||||
export interface TypingMindImportModalProps {
|
||||
open: boolean;
|
||||
jsonContent: string;
|
||||
fileName: string;
|
||||
onClose: () => void;
|
||||
onConversationActivate: (conversationId: DConversationId) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi-step modal for importing TypingMind exports
|
||||
* Steps: Parse → Validate → Confirm → Import → Results
|
||||
*/
|
||||
export function TypingMindImportModal(props: TypingMindImportModalProps) {
|
||||
const [step, setStep] = React.useState<ImportStep>('confirm');
|
||||
const [parsedData, setParsedData] = React.useState<ParsedData | null>(null);
|
||||
const [importOutcome, setImportOutcome] = React.useState<ImportedOutcome | null>(null);
|
||||
|
||||
// Parse and validate on mount
|
||||
React.useEffect(() => {
|
||||
if (!props.open || parsedData) return;
|
||||
|
||||
try {
|
||||
// Parse the export
|
||||
const exportData = parseTypingMindExport(props.jsonContent);
|
||||
const warnings: ImportWarning[] = [];
|
||||
const conversations: DConversation[] = [];
|
||||
|
||||
// Check for unsupported features
|
||||
if (exportData.data.folders && Array.isArray(exportData.data.folders) && exportData.data.folders.length > 0) {
|
||||
warnings.push({
|
||||
type: 'warning',
|
||||
message: `${exportData.data.folders.length} folder(s) will not be imported - Big-AGI uses a different organization system`,
|
||||
});
|
||||
}
|
||||
|
||||
if (exportData.data.userPrompts && Array.isArray(exportData.data.userPrompts) && exportData.data.userPrompts.length > 0) {
|
||||
warnings.push({
|
||||
type: 'warning',
|
||||
message: `${exportData.data.userPrompts.length} custom prompt(s) will not be imported - please recreate in Big-AGI if needed`,
|
||||
});
|
||||
}
|
||||
|
||||
if (exportData.data.userCharacters && Array.isArray(exportData.data.userCharacters) && exportData.data.userCharacters.length > 0) {
|
||||
warnings.push({
|
||||
type: 'warning',
|
||||
message: `${exportData.data.userCharacters.length} AI character(s) will not be imported - please recreate in Big-AGI's persona system`,
|
||||
});
|
||||
}
|
||||
|
||||
// Convert each chat
|
||||
let validChats = 0;
|
||||
for (const chat of exportData.data.chats) {
|
||||
try {
|
||||
const conversation = convertTypingMindChatToConversation(chat);
|
||||
if (conversation.messages.length === 0) {
|
||||
warnings.push({
|
||||
type: 'warning',
|
||||
message: `Chat "${chat.chatTitle || chat.chatID}" has no messages and will be skipped`,
|
||||
chatId: chat.chatID,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for ID conflicts
|
||||
const existingConversation = useChatStore.getState().conversations.find(c => c.id === conversation.id);
|
||||
if (existingConversation) {
|
||||
warnings.push({
|
||||
type: 'warning',
|
||||
message: `Chat "${conversation.autoTitle || conversation.id}" already exists and will be overwritten`,
|
||||
chatId: conversation.id,
|
||||
});
|
||||
}
|
||||
|
||||
conversations.push(conversation);
|
||||
validChats++;
|
||||
} catch (error: any) {
|
||||
warnings.push({
|
||||
type: 'error',
|
||||
message: `Failed to convert chat "${chat.chatTitle || chat.chatID}": ${error?.message || 'Unknown error'}`,
|
||||
chatId: chat.chatID,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setParsedData({
|
||||
totalChats: exportData.data.chats.length,
|
||||
validChats,
|
||||
warnings,
|
||||
conversations,
|
||||
});
|
||||
} catch (error: any) {
|
||||
setParsedData({
|
||||
totalChats: 0,
|
||||
validChats: 0,
|
||||
warnings: [{
|
||||
type: 'error',
|
||||
message: `Failed to parse TypingMind export: ${error?.message || 'Invalid format'}`,
|
||||
}],
|
||||
conversations: [],
|
||||
});
|
||||
}
|
||||
}, [props.open, props.jsonContent, parsedData]);
|
||||
|
||||
const handleConfirmImport = () => {
|
||||
if (!parsedData || parsedData.conversations.length === 0) return;
|
||||
|
||||
setStep('importing');
|
||||
|
||||
// Import conversations
|
||||
const outcome: ImportedOutcome = {
|
||||
conversations: [],
|
||||
activateConversationId: null,
|
||||
};
|
||||
|
||||
try {
|
||||
for (const conversation of parsedData.conversations) {
|
||||
try {
|
||||
useChatStore.getState().importConversation(conversation, false);
|
||||
outcome.conversations.push({
|
||||
success: true,
|
||||
fileName: conversation.autoTitle || conversation.id,
|
||||
conversation,
|
||||
});
|
||||
} catch (error: any) {
|
||||
outcome.conversations.push({
|
||||
success: false,
|
||||
fileName: conversation.autoTitle || conversation.id,
|
||||
error: error?.message || 'Import failed',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Activate the last imported conversation
|
||||
const lastSuccess = outcome.conversations.findLast(c => c.success);
|
||||
if (lastSuccess && 'conversation' in lastSuccess) {
|
||||
outcome.activateConversationId = lastSuccess.conversation.id;
|
||||
props.onConversationActivate(lastSuccess.conversation.id);
|
||||
}
|
||||
|
||||
setImportOutcome(outcome);
|
||||
setStep('complete');
|
||||
} catch (error: any) {
|
||||
outcome.conversations.push({
|
||||
success: false,
|
||||
fileName: props.fileName,
|
||||
error: error?.message || 'Unknown error',
|
||||
});
|
||||
setImportOutcome(outcome);
|
||||
setStep('complete');
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setParsedData(null);
|
||||
setImportOutcome(null);
|
||||
setStep('confirm');
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
if (!parsedData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasErrors = parsedData.warnings.some(w => w.type === 'error');
|
||||
const canImport = parsedData.validChats > 0 && !hasErrors;
|
||||
|
||||
return (
|
||||
<Modal open={props.open} onClose={handleClose}>
|
||||
<ModalDialog sx={{ minWidth: 500, maxWidth: 700 }}>
|
||||
<ModalClose />
|
||||
|
||||
{/* Confirmation Step */}
|
||||
{step === 'confirm' && (
|
||||
<>
|
||||
<Typography level='h4'>
|
||||
Import from TypingMind
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 2 }}>
|
||||
<Typography level='body-md'>
|
||||
<strong>File:</strong> {props.fileName}
|
||||
</Typography>
|
||||
|
||||
<Typography level='body-md'>
|
||||
<strong>Chats found:</strong> {parsedData.totalChats}
|
||||
{parsedData.validChats < parsedData.totalChats && (
|
||||
<> ({parsedData.validChats} valid)</>
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
{parsedData.warnings.length > 0 && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography level='body-sm' fontWeight='bold'>
|
||||
{hasErrors ? 'Errors:' : 'Warnings:'}
|
||||
</Typography>
|
||||
{parsedData.warnings.map((warning, idx) => (
|
||||
<Box key={idx} sx={{ display: 'flex', gap: 1, alignItems: 'flex-start' }}>
|
||||
{warning.type === 'error' ? (
|
||||
<ErrorOutlineIcon color='error' sx={{ fontSize: 20, mt: 0.25 }} />
|
||||
) : (
|
||||
<WarningRoundedIcon color='warning' sx={{ fontSize: 20, mt: 0.25 }} />
|
||||
)}
|
||||
<Typography level='body-sm' color={warning.type === 'error' ? 'danger' : 'warning'}>
|
||||
{warning.message}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Typography level='body-sm' color='neutral'>
|
||||
Note: Folders, custom prompts, AI characters, plugins, and attachments will not be imported.
|
||||
See documentation for details.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, mt: 3 }}>
|
||||
<Button variant='soft' onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color='primary'
|
||||
disabled={!canImport}
|
||||
onClick={handleConfirmImport}
|
||||
sx={{ ml: 'auto' }}
|
||||
>
|
||||
Import {parsedData.validChats} Chat{parsedData.validChats !== 1 ? 's' : ''}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Importing Step */}
|
||||
{step === 'importing' && (
|
||||
<>
|
||||
<Typography level='h4'>
|
||||
Importing...
|
||||
</Typography>
|
||||
<Typography level='body-md' sx={{ mt: 2 }}>
|
||||
Please wait while we import your chats.
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Complete Step */}
|
||||
{step === 'complete' && importOutcome && (
|
||||
<>
|
||||
<Typography level='h4'>
|
||||
Import Complete
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 2 }}>
|
||||
<Typography level='body-md'>
|
||||
<strong>Successful:</strong> {importOutcome.conversations.filter(c => c.success).length}
|
||||
</Typography>
|
||||
<Typography level='body-md'>
|
||||
<strong>Failed:</strong> {importOutcome.conversations.filter(c => !c.success).length}
|
||||
</Typography>
|
||||
|
||||
{importOutcome.conversations.some(c => !c.success) && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography level='body-sm' fontWeight='bold'>
|
||||
Failures:
|
||||
</Typography>
|
||||
{importOutcome.conversations
|
||||
.filter(c => !c.success)
|
||||
.map((conv, idx) => (
|
||||
<Typography key={idx} level='body-sm' color='danger'>
|
||||
• {conv.fileName}: {conv.error}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, mt: 3 }}>
|
||||
<Button color='primary' onClick={handleClose} sx={{ ml: 'auto' }}>
|
||||
Close
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</ModalDialog>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { typingMindExportSchema, TypingMindExport } from './typingmind.schema';
|
||||
|
||||
/**
|
||||
* Parse and validate a TypingMind export JSON string
|
||||
*/
|
||||
export function parseTypingMindExport(jsonString: string): TypingMindExport {
|
||||
// Parse JSON
|
||||
let jsonObject: unknown;
|
||||
try {
|
||||
jsonObject = JSON.parse(jsonString);
|
||||
} catch (error: any) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: `Invalid JSON: ${error?.message || 'Parse error'}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Validate with Zod schema
|
||||
const result = typingMindExportSchema.safeParse(jsonObject);
|
||||
if (!result.success) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: `Invalid TypingMind export format: ${result.error.message}`,
|
||||
});
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract text content from TypingMind message content
|
||||
* Handles both string and array formats
|
||||
*/
|
||||
export function extractMessageText(content: string | Array<{ type?: string; text?: string }>): string {
|
||||
if (typeof content === 'string') {
|
||||
return content;
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
.map(block => block.text || '')
|
||||
.filter(text => text.length > 0)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
/**
|
||||
* TypingMind Export Schema
|
||||
*
|
||||
* Uses looseObject() to allow optional/unknown fields for forward compatibility.
|
||||
* The schema requires minimal fields and parses as much as possible.
|
||||
*/
|
||||
|
||||
// Message content can be either string or array of content blocks
|
||||
const typingMindMessageContentBlockSchema = z.looseObject({
|
||||
type: z.string().optional(),
|
||||
text: z.string().optional(),
|
||||
}).passthrough();
|
||||
|
||||
const typingMindMessageContentSchema = z.union([
|
||||
z.string(), // Simple string content
|
||||
z.array(typingMindMessageContentBlockSchema), // Array of content blocks
|
||||
]);
|
||||
|
||||
// Individual message
|
||||
export const typingMindMessageSchema = z.looseObject({
|
||||
uuid: z.string(),
|
||||
role: z.enum(['user', 'assistant', 'system']),
|
||||
content: typingMindMessageContentSchema,
|
||||
createdAt: z.string().optional(), // ISO timestamp
|
||||
}).passthrough();
|
||||
|
||||
// Chat/Conversation
|
||||
export const typingMindChatSchema = z.looseObject({
|
||||
id: z.string().optional(),
|
||||
chatID: z.string(),
|
||||
chatTitle: z.string().optional(),
|
||||
preview: z.string().optional(),
|
||||
createdAt: z.string().optional(), // ISO timestamp
|
||||
updatedAt: z.string().optional(), // ISO timestamp
|
||||
messages: z.array(typingMindMessageSchema),
|
||||
// Other fields we don't use but allow
|
||||
folderID: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
modelInfo: z.unknown().optional(),
|
||||
linkedPlugins: z.unknown().optional(),
|
||||
chatParams: z.unknown().optional(),
|
||||
selectedMultimodelIDs: z.unknown().optional(),
|
||||
tokenUsage: z.unknown().optional(),
|
||||
}).passthrough();
|
||||
|
||||
// Top-level export structure
|
||||
export const typingMindExportSchema = z.looseObject({
|
||||
data: z.looseObject({
|
||||
chats: z.array(typingMindChatSchema),
|
||||
// Other top-level arrays we don't import
|
||||
folders: z.unknown().optional(),
|
||||
userPrompts: z.unknown().optional(),
|
||||
userCharacters: z.unknown().optional(),
|
||||
installedPlugins: z.unknown().optional(),
|
||||
promptSettings: z.unknown().optional(),
|
||||
characterSettings: z.unknown().optional(),
|
||||
}).passthrough(),
|
||||
}).passthrough();
|
||||
|
||||
export type TypingMindExport = z.infer<typeof typingMindExportSchema>;
|
||||
export type TypingMindChat = z.infer<typeof typingMindChatSchema>;
|
||||
export type TypingMindMessage = z.infer<typeof typingMindMessageSchema>;
|
||||
@@ -0,0 +1,76 @@
|
||||
import { createDConversation, DConversation } from '~/common/stores/chat/chat.conversation';
|
||||
import { createDMessageTextContent, DMessage } from '~/common/stores/chat/chat.message';
|
||||
|
||||
import { TypingMindChat, TypingMindMessage } from './typingmind.schema';
|
||||
import { extractMessageText } from './typingmind.parser';
|
||||
|
||||
/**
|
||||
* Convert TypingMind chat to DConversation
|
||||
*/
|
||||
export function convertTypingMindChatToConversation(chat: TypingMindChat): DConversation {
|
||||
const conversation = createDConversation();
|
||||
|
||||
// Use the original chat ID
|
||||
conversation.id = chat.chatID;
|
||||
|
||||
// Set title
|
||||
if (chat.chatTitle) {
|
||||
conversation.autoTitle = chat.chatTitle;
|
||||
} else if (chat.preview) {
|
||||
conversation.autoTitle = chat.preview;
|
||||
}
|
||||
|
||||
// Set timestamps - convert ISO strings to Unix milliseconds
|
||||
if (chat.createdAt) {
|
||||
const createdDate = new Date(chat.createdAt);
|
||||
if (!isNaN(createdDate.getTime())) {
|
||||
conversation.created = createdDate.getTime();
|
||||
}
|
||||
}
|
||||
|
||||
if (chat.updatedAt) {
|
||||
const updatedDate = new Date(chat.updatedAt);
|
||||
if (!isNaN(updatedDate.getTime())) {
|
||||
conversation.updated = updatedDate.getTime();
|
||||
}
|
||||
}
|
||||
|
||||
// Convert messages
|
||||
conversation.messages = chat.messages
|
||||
.map(msg => convertTypingMindMessageToDMessage(msg))
|
||||
.filter((msg): msg is DMessage => msg !== null);
|
||||
|
||||
return conversation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert TypingMind message to DMessage
|
||||
*/
|
||||
function convertTypingMindMessageToDMessage(message: TypingMindMessage): DMessage | null {
|
||||
// Extract text content
|
||||
const text = extractMessageText(message.content);
|
||||
if (!text || text.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only support user and assistant roles
|
||||
if (message.role !== 'user' && message.role !== 'assistant') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create message
|
||||
const dMessage = createDMessageTextContent(message.role, text);
|
||||
|
||||
// Use original UUID
|
||||
dMessage.id = message.uuid;
|
||||
|
||||
// Set timestamp
|
||||
if (message.createdAt) {
|
||||
const createdDate = new Date(message.createdAt);
|
||||
if (!isNaN(createdDate.getTime())) {
|
||||
dMessage.created = createdDate.getTime();
|
||||
}
|
||||
}
|
||||
|
||||
return dMessage;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
# TypingMind Import - Unsupported Features
|
||||
|
||||
This document explains what TypingMind features are not imported and why.
|
||||
|
||||
## Folders
|
||||
|
||||
**TypingMind Feature:** Chats can be organized into folders with hierarchical structure.
|
||||
|
||||
**Why Not Imported:** Big-AGI currently uses a different organization system. All chats are imported to the root level. The folder structure from TypingMind is not preserved.
|
||||
|
||||
**Future:** If Big-AGI implements folder/tagging functionality, this could be revisited.
|
||||
|
||||
## User Prompts
|
||||
|
||||
**TypingMind Feature:** Custom prompt library with variables, tags, and favorites.
|
||||
|
||||
**Why Not Imported:** Big-AGI has a different prompt/persona system. TypingMind prompts use a different variable syntax (`{field}`, `{{action}}`) and organization model.
|
||||
|
||||
**Alternative:** Users can manually recreate important prompts in Big-AGI's persona system.
|
||||
|
||||
## User Characters / AI Personas
|
||||
|
||||
**TypingMind Feature:** Custom AI characters with system instructions, conversation starters, default models, training data, and voice settings.
|
||||
|
||||
**Why Not Imported:** Big-AGI has its own persona system with different capabilities and structure. TypingMind characters include features like:
|
||||
- Enforced model selection
|
||||
- Speech synthesis settings (ElevenLabs, OpenAI TTS)
|
||||
- Plugin assignments
|
||||
- Training file attachments
|
||||
|
||||
These don't have direct equivalents in Big-AGI's current architecture.
|
||||
|
||||
**Alternative:** Users can manually recreate character behaviors using Big-AGI's persona system.
|
||||
|
||||
## Installed Plugins / Tools
|
||||
|
||||
**TypingMind Feature:** Custom plugins with JavaScript code, HTTP actions, and OpenAI function specs (web search, image generation, research tools, etc.).
|
||||
|
||||
**Why Not Imported:** Plugin architectures are fundamentally incompatible:
|
||||
- TypingMind uses JavaScript execution and HTTP actions
|
||||
- Big-AGI has its own tool/integration system
|
||||
- Security model differences
|
||||
|
||||
**Result:** Only text messages are imported. Any tool/plugin invocations or results in the chat history are preserved as text content.
|
||||
|
||||
## Attachments and Images
|
||||
|
||||
**TypingMind Feature:** File attachments and images uploaded to chats, with blob storage.
|
||||
|
||||
**Why Not Imported:** TypingMind's export feature has a known bug where attachments are not included in the export, despite being referenced in the JSON. The JSON contains public URLs to images, but:
|
||||
- URLs may expire or become inaccessible
|
||||
- Downloading from external URLs introduces privacy/security concerns
|
||||
- No guarantee the files still exist
|
||||
|
||||
**Current Behavior:** Image and file references appear as text mentions in the imported messages (e.g., `[Image: url]` or `[File: filename]`).
|
||||
|
||||
**Future:** If TypingMind fixes their export to include attachments, this feature could be implemented with:
|
||||
- User consent for downloading external resources
|
||||
- Rate limiting to respect server constraints
|
||||
- Local storage of downloaded assets
|
||||
|
||||
## Token Usage and Cost Tracking
|
||||
|
||||
**TypingMind Feature:** Detailed token usage and cost tracking per chat, including cached tokens and reasoning tokens.
|
||||
|
||||
**Why Not Imported:** Big-AGI has its own token counting and cost tracking system. Historical cost data from TypingMind is not carried over.
|
||||
|
||||
**Result:** Imported chats will have token counts recalculated by Big-AGI's system.
|
||||
|
||||
## Model Parameters and Settings
|
||||
|
||||
**TypingMind Feature:** Per-chat model parameters (context limit, streaming, output settings, system message templates with variables).
|
||||
|
||||
**Why Not Imported:** These are session-specific settings that don't transfer meaningfully:
|
||||
- Big-AGI users select models interactively
|
||||
- System messages use a different variable system
|
||||
- Output tone/language/style/format settings are TypingMind-specific
|
||||
|
||||
**Result:** Imported chats use Big-AGI's default model and settings.
|
||||
@@ -20,6 +20,7 @@ import { importConversationsFromFilesAtRest, openConversationsAtRestPicker } fro
|
||||
|
||||
import { FlashRestore } from './BackupRestore';
|
||||
import { ImportedOutcome, ImportOutcomeModal } from './ImportOutcomeModal';
|
||||
import { TypingMindImportModal } from '../data/typingmind/TypingMindImportModal';
|
||||
|
||||
|
||||
export type ImportConfig = { dir: 'import' };
|
||||
@@ -43,6 +44,8 @@ export function ImportChats(props: { onConversationActivate: (conversationId: DC
|
||||
const [chatGptSource, setChatGptSource] = React.useState('');
|
||||
const [importJson, setImportJson] = React.useState<string | null>(null);
|
||||
const [importOutcome, setImportOutcome] = React.useState<ImportedOutcome | null>(null);
|
||||
const [typingMindModalOpen, setTypingMindModalOpen] = React.useState(false);
|
||||
const [typingMindData, setTypingMindData] = React.useState<{ content: string; fileName: string } | null>(null);
|
||||
|
||||
// derived state
|
||||
const isUrl = importMedia === 'link';
|
||||
@@ -122,6 +125,32 @@ export function ImportChats(props: { onConversationActivate: (conversationId: DC
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
const handleTypingMindImport = () => {
|
||||
// Create a file input element
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json,application/json';
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const content = await file.text();
|
||||
setTypingMindData({ content, fileName: file.name });
|
||||
setTypingMindModalOpen(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to read file:', error);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
|
||||
const handleTypingMindModalClose = () => {
|
||||
setTypingMindModalOpen(false);
|
||||
setTypingMindData(null);
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
|
||||
return <>
|
||||
|
||||
@@ -150,6 +179,15 @@ export function ImportChats(props: { onConversationActivate: (conversationId: DC
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!chatGptEdit && (
|
||||
<Button
|
||||
variant='soft' endDecorator={<FileUploadIcon />} sx={{ minWidth: 240, justifyContent: 'space-between' }}
|
||||
onClick={handleTypingMindImport}
|
||||
>
|
||||
TypingMind · Export
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Insert to Restore a Flash */}
|
||||
<FlashRestore unlockRestore={true} />
|
||||
|
||||
@@ -193,5 +231,16 @@ export function ImportChats(props: { onConversationActivate: (conversationId: DC
|
||||
{/* import outcome */}
|
||||
{!!importOutcome && <ImportOutcomeModal outcome={importOutcome} rawJson={importJson} onClose={handleImportOutcomeClosed} />}
|
||||
|
||||
{/* TypingMind import modal */}
|
||||
{typingMindModalOpen && typingMindData && (
|
||||
<TypingMindImportModal
|
||||
open={typingMindModalOpen}
|
||||
jsonContent={typingMindData.content}
|
||||
fileName={typingMindData.fileName}
|
||||
onClose={handleTypingMindModalClose}
|
||||
onConversationActivate={props.onConversationActivate}
|
||||
/>
|
||||
)}
|
||||
|
||||
</>;
|
||||
}
|
||||
Reference in New Issue
Block a user