feat(chat): add conversation export & three-dot menu (M8.7)
Export conversations as Markdown, JSON, or plain text via three-dot menu in chat header. Uses File System Access API with <a download> fallback. Also adds delete conversation option. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
87cbd8c4ea
commit
6182adb01c
@@ -45,6 +45,17 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
ref="menuTriggerRef"
|
||||
class="w-9 h-9 rounded-xl path-glass-icon flex items-center justify-center transition-colors text-white/70 hover:text-white"
|
||||
aria-label="Conversation menu"
|
||||
@click="showMenu = !showMenu"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="w-9 h-9 rounded-xl path-glass-icon flex items-center justify-center transition-colors text-white/70 hover:text-white"
|
||||
aria-label="New conversation"
|
||||
@@ -149,6 +160,47 @@
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
|
||||
<!-- Conversation menu (export, delete) -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showMenu" class="fixed inset-0 z-[9998]" aria-hidden="true" @click="showMenu = false" />
|
||||
<Transition name="picker">
|
||||
<div
|
||||
v-if="showMenu"
|
||||
class="fixed z-[9999] path-glass-card p-2 animate-fade-up-fast shadow-2xl min-w-[160px]"
|
||||
:style="menuDropdownStyle"
|
||||
@click.stop
|
||||
>
|
||||
<p class="text-[10px] font-semibold uppercase tracking-wider mb-1.5 px-2 text-white/40">Export</p>
|
||||
<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="handleExport('markdown')"
|
||||
>
|
||||
Markdown (.md)
|
||||
</button>
|
||||
<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="handleExport('json')"
|
||||
>
|
||||
JSON
|
||||
</button>
|
||||
<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="handleExport('text')"
|
||||
>
|
||||
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-red-400/80 hover:text-red-400 hover:bg-white/10 transition-all"
|
||||
@click="handleDelete"
|
||||
>
|
||||
Delete conversation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -157,6 +209,7 @@ import { ref, computed, watch, nextTick } from 'vue'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { useAI } from '@/composables/useAI'
|
||||
import { useContentPanel } from '@/composables/useContentPanel'
|
||||
import { downloadConversation, type ExportFormat } from '@/utils/conversation-export'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
@@ -176,6 +229,9 @@ const chatStore = useChatStore()
|
||||
const webSearchEnabled = computed(() => chatStore.webSearchEnabled)
|
||||
const showModelPicker = ref(false)
|
||||
const showChatList = ref(false)
|
||||
const showMenu = ref(false)
|
||||
const menuTriggerRef = ref<HTMLElement | null>(null)
|
||||
const menuDropdownStyle = ref<Record<string, string>>({})
|
||||
const headerRef = ref<HTMLElement | null>(null)
|
||||
const chatListTriggerRef = ref<HTMLElement | null>(null)
|
||||
const modelPickerTriggerRef = ref<HTMLElement | null>(null)
|
||||
@@ -211,8 +267,22 @@ function updateModelPickerPosition() {
|
||||
})
|
||||
}
|
||||
|
||||
function updateMenuPosition() {
|
||||
nextTick(() => {
|
||||
const el = menuTriggerRef.value
|
||||
if (el) {
|
||||
const r = el.getBoundingClientRect()
|
||||
menuDropdownStyle.value = {
|
||||
top: `${r.bottom + 4}px`,
|
||||
right: `${window.innerWidth - r.right}px`,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
watch(showChatList, (v) => { if (v) updateChatListPosition() })
|
||||
watch(showModelPicker, (v) => { if (v) updateModelPickerPosition() })
|
||||
watch(showMenu, (v) => { if (v) updateMenuPosition() })
|
||||
|
||||
const conversationList = computed(() => chatStore.conversationList)
|
||||
const activeConversationId = computed(() => chatStore.activeConversationId)
|
||||
@@ -246,6 +316,20 @@ function openDesignSystem() {
|
||||
enterDesignSystemMode()
|
||||
showModelPicker.value = false
|
||||
}
|
||||
|
||||
async function handleExport(format: ExportFormat) {
|
||||
const conv = chatStore.activeConversation
|
||||
if (!conv) return
|
||||
await downloadConversation(conv, format)
|
||||
showMenu.value = false
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
const id = chatStore.activeConversationId
|
||||
if (!id) return
|
||||
chatStore.deleteConversation(id)
|
||||
showMenu.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user