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:
Dorian
2026-03-03 23:14:11 +00:00
co-authored by Claude Opus 4.6
parent 6182adb01c
commit 37e8aec920
2 changed files with 160 additions and 0 deletions
@@ -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>