feat(chat): add conversation import from JSON (M8.8)
Import AIUI JSON exports and Claude.ai export format via file picker. Merges imported conversations into existing data without overwriting. Shows import summary with count and format detected. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
6182adb01c
commit
37e8aec920
@@ -191,6 +191,12 @@
|
||||
Plain text (.txt)
|
||||
</button>
|
||||
<div class="border-t border-white/10 mt-1 pt-1">
|
||||
<button
|
||||
class="w-full text-left px-3 py-2 rounded-lg text-xs text-white/60 hover:text-white hover:bg-white/10 transition-all"
|
||||
@click="triggerImport"
|
||||
>
|
||||
Import conversations
|
||||
</button>
|
||||
<button
|
||||
class="w-full text-left px-3 py-2 rounded-lg text-xs text-red-400/80 hover:text-red-400 hover:bg-white/10 transition-all"
|
||||
@click="handleDelete"
|
||||
@@ -201,6 +207,23 @@
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
|
||||
<input
|
||||
ref="importInputRef"
|
||||
type="file"
|
||||
accept=".json"
|
||||
class="hidden"
|
||||
@change="handleImportFile"
|
||||
/>
|
||||
|
||||
<!-- Import status -->
|
||||
<div
|
||||
v-if="importStatus"
|
||||
class="absolute top-full left-3 right-3 mt-1 z-[100] glass px-3 py-2 rounded-lg text-xs animate-fade-up-fast"
|
||||
:class="importStatus.startsWith('Error') ? 'text-red-400' : 'text-accent'"
|
||||
>
|
||||
{{ importStatus }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -210,6 +233,7 @@ import { useChatStore } from '@/stores/chat'
|
||||
import { useAI } from '@/composables/useAI'
|
||||
import { useContentPanel } from '@/composables/useContentPanel'
|
||||
import { downloadConversation, type ExportFormat } from '@/utils/conversation-export'
|
||||
import { parseImportFile } from '@/utils/conversation-import'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
@@ -330,6 +354,44 @@ function handleDelete() {
|
||||
chatStore.deleteConversation(id)
|
||||
showMenu.value = false
|
||||
}
|
||||
|
||||
const importInputRef = ref<HTMLInputElement | null>(null)
|
||||
const importStatus = ref<string | null>(null)
|
||||
|
||||
function triggerImport() {
|
||||
showMenu.value = false
|
||||
importInputRef.value?.click()
|
||||
}
|
||||
|
||||
async function handleImportFile(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
const text = await file.text()
|
||||
const result = parseImportFile(text)
|
||||
|
||||
if (result.error || result.conversations.length === 0) {
|
||||
importStatus.value = `Error: ${result.error ?? 'No conversations found'}`
|
||||
} else {
|
||||
for (const conv of result.conversations) {
|
||||
chatStore.conversations.set(conv.id, conv)
|
||||
}
|
||||
const count = result.conversations.length
|
||||
importStatus.value = `Imported ${count} conversation${count > 1 ? 's' : ''} (${result.format})`
|
||||
// Switch to first imported conversation
|
||||
chatStore.setActiveConversation(result.conversations[0].id)
|
||||
}
|
||||
} catch {
|
||||
importStatus.value = 'Error: Failed to read file'
|
||||
}
|
||||
|
||||
// Clear file input for re-use
|
||||
input.value = ''
|
||||
// Auto-hide status after 3 seconds
|
||||
setTimeout(() => { importStatus.value = null }, 3000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { Conversation, Message } from '@aiui/core/types/message'
|
||||
|
||||
function generateId(): string {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0
|
||||
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
|
||||
})
|
||||
}
|
||||
|
||||
interface ImportResult {
|
||||
conversations: Conversation[]
|
||||
format: 'aiui' | 'claude' | 'unknown'
|
||||
error?: string
|
||||
}
|
||||
|
||||
/** Try to parse as AIUI JSON export (single conversation) */
|
||||
function parseAIUIFormat(data: unknown): Conversation | null {
|
||||
if (!data || typeof data !== 'object') return null
|
||||
const obj = data as Record<string, unknown>
|
||||
|
||||
if (typeof obj.id === 'string' && typeof obj.title === 'string' && Array.isArray(obj.messages)) {
|
||||
return obj as unknown as Conversation
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** Try to parse Claude.ai export format */
|
||||
function parseClaudeFormat(data: unknown): Conversation[] {
|
||||
if (!Array.isArray(data)) return []
|
||||
|
||||
const conversations: Conversation[] = []
|
||||
for (const item of data) {
|
||||
if (!item || typeof item !== 'object') continue
|
||||
const obj = item as Record<string, unknown>
|
||||
|
||||
// Claude.ai exports have { uuid, name, chat_messages: [...] }
|
||||
if (typeof obj.uuid === 'string' && typeof obj.name === 'string' && Array.isArray(obj.chat_messages)) {
|
||||
const messages: Message[] = []
|
||||
for (const cm of obj.chat_messages as Record<string, unknown>[]) {
|
||||
if (!cm || typeof cm !== 'object') continue
|
||||
const role = cm.sender === 'human' ? 'user' as const : 'assistant' as const
|
||||
const content = typeof cm.text === 'string' ? cm.text : ''
|
||||
messages.push({
|
||||
id: generateId(),
|
||||
role,
|
||||
content,
|
||||
timestamp: typeof cm.created_at === 'string' ? new Date(cm.created_at as string).getTime() : Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
conversations.push({
|
||||
id: generateId(),
|
||||
title: obj.name as string,
|
||||
messages,
|
||||
createdAt: typeof obj.created_at === 'string' ? new Date(obj.created_at as string).getTime() : Date.now(),
|
||||
updatedAt: typeof obj.updated_at === 'string' ? new Date(obj.updated_at as string).getTime() : Date.now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
return conversations
|
||||
}
|
||||
|
||||
export function parseImportFile(jsonString: string): ImportResult {
|
||||
try {
|
||||
const data = JSON.parse(jsonString)
|
||||
|
||||
// Try AIUI single conversation
|
||||
const aiui = parseAIUIFormat(data)
|
||||
if (aiui) {
|
||||
return { conversations: [aiui], format: 'aiui' }
|
||||
}
|
||||
|
||||
// Try Claude.ai export (array of conversations)
|
||||
const claude = parseClaudeFormat(data)
|
||||
if (claude.length > 0) {
|
||||
return { conversations: claude, format: 'claude' }
|
||||
}
|
||||
|
||||
// Try AIUI array format
|
||||
if (Array.isArray(data)) {
|
||||
const aiuiConvs: Conversation[] = []
|
||||
for (const item of data) {
|
||||
const c = parseAIUIFormat(item)
|
||||
if (c) aiuiConvs.push(c)
|
||||
}
|
||||
if (aiuiConvs.length > 0) {
|
||||
return { conversations: aiuiConvs, format: 'aiui' }
|
||||
}
|
||||
}
|
||||
|
||||
return { conversations: [], format: 'unknown', error: 'Unrecognized format' }
|
||||
} catch {
|
||||
return { conversations: [], format: 'unknown', error: 'Invalid JSON' }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user