mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
feat(ai): polish chat interface and loading
This commit is contained in:
1 parent
d53a99359b
commit
349510edf6
7 files changed
+219
-171
No files matched your search
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
@@ -9,6 +9,7 @@ import ChatMessage from './chat/ChatMessage.vue'
|
||||
import AIChatInput from './input/AIChatInput.vue'
|
||||
import AIThinkingIndicator from './chat/AIThinkingIndicator.vue'
|
||||
import ChatStatusBar from './chat/ChatStatusBar.vue'
|
||||
import ChatHeaderActions from './chat/ChatHeaderActions.vue'
|
||||
import { useAIChat } from '@/composables/useAIChat'
|
||||
import { useAIService, useLLMService } from '@/services'
|
||||
import AssistantInlineBar from './assistant/AssistantInlineBar.vue'
|
||||
@@ -45,7 +46,12 @@ const props = defineProps<{
|
||||
chatType?: 'group' | 'private'
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'restore-loading-change': [loading: boolean]
|
||||
}>()
|
||||
|
||||
const initialAIChatId = typeof route.query.aiChatId === 'string' ? route.query.aiChatId : null
|
||||
if (initialAIChatId) emit('restore-loading-change', true)
|
||||
|
||||
// 使用 AI 对话 Composable
|
||||
const {
|
||||
@@ -92,10 +98,13 @@ async function syncAIChatIdToRoute(aiChatId: string | null): Promise<void> {
|
||||
}
|
||||
|
||||
void initialization.finally(() => {
|
||||
emit('restore-loading-change', false)
|
||||
isAIChatInitialized = true
|
||||
void syncAIChatIdToRoute(currentAIChatId.value)
|
||||
})
|
||||
|
||||
onUnmounted(() => emit('restore-loading-change', false))
|
||||
|
||||
watch(currentAIChatId, (aiChatId) => {
|
||||
if (isAIChatInitialized) void syncAIChatIdToRoute(aiChatId)
|
||||
})
|
||||
@@ -445,7 +454,7 @@ watch(
|
||||
<div class="relative flex min-w-[480px] flex-1 flex-col overflow-hidden">
|
||||
<!-- 顶部:有消息时显示助手切换按钮 -->
|
||||
<template v-if="messages.length > 0 || isAIThinking">
|
||||
<div class="flex items-center gap-1.5 px-3 py-1.5">
|
||||
<div class="flex items-center justify-between gap-2 px-3 py-1.5">
|
||||
<button
|
||||
class="flex items-center gap-1 rounded-md px-1.5 py-1 text-xs text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200"
|
||||
:disabled="isAIThinking || !assistantStore.selectedAssistant?.id"
|
||||
@@ -455,6 +464,12 @@ watch(
|
||||
<UIcon name="i-heroicons-sparkles" class="h-3.5 w-3.5" />
|
||||
<span>{{ assistantStore.selectedAssistant?.name || t('ai.assistant.fallbackName') }}</span>
|
||||
</button>
|
||||
|
||||
<ChatHeaderActions
|
||||
:current-ai-chat-id="currentAIChatId"
|
||||
:current-messages="messages"
|
||||
:fallback-title="sessionName"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -606,10 +621,10 @@ watch(
|
||||
</div>
|
||||
|
||||
<!-- 输入框区域 -->
|
||||
<div class="px-4 pb-2">
|
||||
<div class="px-4 pb-3">
|
||||
<div class="mx-auto max-w-3xl">
|
||||
<div
|
||||
class="overflow-visible rounded-2xl bg-white shadow-[0_2px_14px_rgba(0,0,0,0.04)] ring-1 ring-gray-200/60 transition-all focus-within:ring-primary-500/40 focus-within:shadow-[0_4px_20px_rgba(0,0,0,0.08)] dark:bg-page-dark dark:ring-white/5 dark:focus-within:ring-primary-500/40"
|
||||
class="relative overflow-visible rounded-2xl bg-white shadow-[0_2px_14px_rgba(0,0,0,0.04)] ring-1 ring-gray-200/60 transition-all focus-within:ring-primary-500/40 focus-within:shadow-[0_4px_20px_rgba(0,0,0,0.08)] dark:bg-page-dark dark:ring-white/5 dark:focus-within:ring-primary-500/40"
|
||||
>
|
||||
<AIChatInput
|
||||
ref="chatInputRef"
|
||||
@@ -625,12 +640,9 @@ watch(
|
||||
/>
|
||||
|
||||
<ChatStatusBar
|
||||
class="px-2 pb-1.5 pt-0.5"
|
||||
class="pb-1.5 pl-2 pr-[52px] pt-0.5"
|
||||
:session-token-usage="sessionTokenUsage"
|
||||
:agent-status="agentStatus"
|
||||
:current-ai-chat-id="currentAIChatId"
|
||||
:current-messages="messages"
|
||||
:fallback-title="sessionName"
|
||||
:estimated-context-tokens="estimatedContextTokens"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { ChatMessage } from '@/composables/useAIChat'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useAIService } from '@/services'
|
||||
import { useCacheService } from '@/services/cache/service'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
import {
|
||||
exportConversation,
|
||||
getExportableConversationMessages,
|
||||
hasExportableConversationMessages,
|
||||
type ConversationExportSourceMessage,
|
||||
type ExportFormat,
|
||||
} from '@/utils/conversationExport'
|
||||
|
||||
const props = defineProps<{
|
||||
currentAIChatId?: string | null
|
||||
currentMessages?: ChatMessage[]
|
||||
fallbackTitle?: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const promptStore = usePromptStore()
|
||||
const { aiGlobalSettings } = storeToRefs(promptStore)
|
||||
|
||||
const isExporting = ref(false)
|
||||
const isOpeningLog = ref(false)
|
||||
const visibleExportMessages = computed(() => getExportableConversationMessages(props.currentMessages ?? []))
|
||||
const canExportConversation = computed(() => {
|
||||
return Boolean(props.currentAIChatId) || hasExportableConversationMessages(props.currentMessages ?? [])
|
||||
})
|
||||
|
||||
function getExportLabels() {
|
||||
return {
|
||||
createdAt: t('ai.chat.conversation.export.createdAt'),
|
||||
user: t('ai.chat.conversation.export.user'),
|
||||
assistant: t('ai.chat.conversation.export.assistant'),
|
||||
}
|
||||
}
|
||||
|
||||
function toExportSourceMessages(messages: ConversationExportSourceMessage[]): ConversationExportSourceMessage[] {
|
||||
return messages.map((message) => ({
|
||||
...message,
|
||||
timestamp: message.timestamp * 1000,
|
||||
}))
|
||||
}
|
||||
|
||||
async function handleExportConversation() {
|
||||
if (isExporting.value || !canExportConversation.value) return
|
||||
|
||||
isExporting.value = true
|
||||
try {
|
||||
const format = (aiGlobalSettings.value.exportFormat || 'markdown') as ExportFormat
|
||||
const labels = getExportLabels()
|
||||
let title = props.fallbackTitle || t('ai.chat.conversation.newChat')
|
||||
let createdAt = visibleExportMessages.value[0]?.timestamp ?? Date.now()
|
||||
let messages = visibleExportMessages.value
|
||||
|
||||
if (props.currentAIChatId) {
|
||||
const [conversation, persistedMessages] = await Promise.all([
|
||||
useAIService().getAIChat(props.currentAIChatId),
|
||||
useAIService().getMessages(props.currentAIChatId),
|
||||
])
|
||||
|
||||
if (conversation) {
|
||||
title = conversation.title || title
|
||||
createdAt = conversation.createdAt * 1000
|
||||
}
|
||||
|
||||
const persistedExportMessages = getExportableConversationMessages(toExportSourceMessages(persistedMessages))
|
||||
if (persistedExportMessages.length > 0) {
|
||||
messages = persistedExportMessages
|
||||
}
|
||||
}
|
||||
|
||||
if (messages.length === 0) {
|
||||
toast.warn(t('ai.chat.conversation.export.noMessages'))
|
||||
return
|
||||
}
|
||||
|
||||
const result = await exportConversation(title, messages, createdAt, format, labels)
|
||||
|
||||
if (result.success && result.filePath) {
|
||||
const filename = result.filePath.split('/').pop() || result.filePath
|
||||
const exportedFilePath = result.filePath
|
||||
toast.add({
|
||||
title: t('common.exportSuccess'),
|
||||
description: filename,
|
||||
color: 'primary',
|
||||
actions: [
|
||||
{
|
||||
label: t('common.openFolder'),
|
||||
onClick: () => {
|
||||
useCacheService().showInFolder(exportedFilePath)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
} else {
|
||||
toast.fail(t('common.exportFailed'), { description: result.error })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to export AI conversation:', error)
|
||||
toast.fail(t('common.exportFailed'), { description: String(error) })
|
||||
} finally {
|
||||
isExporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openAiLogFile() {
|
||||
if (isOpeningLog.value) return
|
||||
|
||||
isOpeningLog.value = true
|
||||
try {
|
||||
const result = await useAIService().showAiLogFile()
|
||||
if (!result?.success) {
|
||||
toast.fail(t('ai.chat.statusBar.log.openFailed'), {
|
||||
description: result?.error || t('ai.chat.statusBar.log.openFailedDesc'),
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to open AI log file:', error)
|
||||
toast.fail(t('ai.chat.statusBar.log.openFailed'), { description: String(error) })
|
||||
} finally {
|
||||
isOpeningLog.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1 rounded-md px-1.5 py-1 text-xs text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 disabled:cursor-not-allowed disabled:opacity-60 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200"
|
||||
:title="t('ai.chat.statusBar.export.title')"
|
||||
:disabled="isExporting || !canExportConversation"
|
||||
@click="handleExportConversation"
|
||||
>
|
||||
<UIcon name="i-heroicons-arrow-down-tray" class="h-3.5 w-3.5" />
|
||||
<span>{{ t('ai.chat.statusBar.export.label') }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1 rounded-md px-1.5 py-1 text-xs text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 disabled:cursor-not-allowed disabled:opacity-60 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200"
|
||||
:title="t('ai.chat.statusBar.log.title')"
|
||||
:disabled="isOpeningLog"
|
||||
@click="openAiLogFile"
|
||||
>
|
||||
<UIcon name="i-heroicons-document-text" class="h-3.5 w-3.5" />
|
||||
<span>{{ t('ai.chat.statusBar.log.label') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,45 +2,27 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
import { useLLMStore } from '@/stores/llm'
|
||||
import {
|
||||
exportConversation,
|
||||
getExportableConversationMessages,
|
||||
hasExportableConversationMessages,
|
||||
type ConversationExportSourceMessage,
|
||||
type ExportFormat,
|
||||
} from '@/utils/conversationExport'
|
||||
import type { AgentRuntimeStatus } from '@electron/shared/types'
|
||||
import { useAIService } from '@/services'
|
||||
import { getSupportedThinkingLevels, type ThinkingLevel } from '@openchatlab/core'
|
||||
import { useCacheService } from '@/services/cache/service'
|
||||
import type { ChatMessage } from '@/composables/useAIChat'
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const layoutStore = useLayoutStore()
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
sessionTokenUsage: { totalTokens: number; cacheReadTokens: number; cacheWriteTokens: number }
|
||||
agentStatus?: AgentRuntimeStatus | null
|
||||
currentAIChatId?: string | null
|
||||
currentMessages?: ChatMessage[]
|
||||
fallbackTitle?: string
|
||||
estimatedContextTokens?: number
|
||||
}>()
|
||||
|
||||
// Store
|
||||
const promptStore = usePromptStore()
|
||||
const llmStore = useLLMStore()
|
||||
const { aiGlobalSettings } = storeToRefs(promptStore)
|
||||
const { defaultAssistantConfig, isLoading: isLoadingLLM } = storeToRefs(llmStore)
|
||||
|
||||
const isOpeningLog = ref(false)
|
||||
|
||||
const agentPhaseText = computed(() => {
|
||||
if (!props.agentStatus) return ''
|
||||
return t(`ai.chat.statusBar.agent.phase.${props.agentStatus.phase}`)
|
||||
@@ -105,11 +87,10 @@ const contextUsagePercent = computed(() => {
|
||||
return Math.min(100, Math.round((contextTokens.value / modelContextWindow.value) * 100))
|
||||
})
|
||||
|
||||
const contextBarColor = computed(() => {
|
||||
const pct = contextUsagePercent.value
|
||||
if (pct >= 80) return 'bg-red-500'
|
||||
if (pct >= 60) return 'bg-amber-500'
|
||||
return 'bg-emerald-500'
|
||||
const contextRingRadius = 5.5
|
||||
const contextRingCircumference = 2 * Math.PI * contextRingRadius
|
||||
const contextRingDasharray = computed(() => {
|
||||
return `${(contextRingCircumference * contextUsagePercent.value) / 100} ${contextRingCircumference}`
|
||||
})
|
||||
|
||||
const agentCompactTitle = computed(() => {
|
||||
@@ -125,109 +106,6 @@ function openModelSettings() {
|
||||
layoutStore.openSettings('ai', 'defaultModel')
|
||||
}
|
||||
|
||||
// 导出当前对话
|
||||
const isExporting = ref(false)
|
||||
const visibleExportMessages = computed(() => getExportableConversationMessages(props.currentMessages ?? []))
|
||||
const canExportConversation = computed(() => {
|
||||
return Boolean(props.currentAIChatId) || hasExportableConversationMessages(props.currentMessages ?? [])
|
||||
})
|
||||
|
||||
function getExportLabels() {
|
||||
return {
|
||||
createdAt: t('ai.chat.conversation.export.createdAt'),
|
||||
user: t('ai.chat.conversation.export.user'),
|
||||
assistant: t('ai.chat.conversation.export.assistant'),
|
||||
}
|
||||
}
|
||||
|
||||
function toExportSourceMessages(messages: ConversationExportSourceMessage[]): ConversationExportSourceMessage[] {
|
||||
return messages.map((message) => ({
|
||||
...message,
|
||||
timestamp: message.timestamp * 1000,
|
||||
}))
|
||||
}
|
||||
|
||||
async function handleExportConversation() {
|
||||
if (isExporting.value || !canExportConversation.value) return
|
||||
|
||||
isExporting.value = true
|
||||
try {
|
||||
const format = (aiGlobalSettings.value.exportFormat || 'markdown') as ExportFormat
|
||||
const labels = getExportLabels()
|
||||
let title = props.fallbackTitle || t('ai.chat.conversation.newChat')
|
||||
let createdAt = visibleExportMessages.value[0]?.timestamp ?? Date.now()
|
||||
let messages = visibleExportMessages.value
|
||||
|
||||
if (props.currentAIChatId) {
|
||||
const [conv, persistedMessages] = await Promise.all([
|
||||
useAIService().getAIChat(props.currentAIChatId),
|
||||
useAIService().getMessages(props.currentAIChatId),
|
||||
])
|
||||
|
||||
if (conv) {
|
||||
title = conv.title || title
|
||||
createdAt = conv.createdAt * 1000
|
||||
}
|
||||
|
||||
const persistedExportMessages = getExportableConversationMessages(toExportSourceMessages(persistedMessages))
|
||||
if (persistedExportMessages.length > 0) {
|
||||
messages = persistedExportMessages
|
||||
}
|
||||
}
|
||||
|
||||
if (messages.length === 0) {
|
||||
toast.warn(t('ai.chat.conversation.export.noMessages'))
|
||||
return
|
||||
}
|
||||
|
||||
const result = await exportConversation(title, messages, createdAt, format, labels)
|
||||
|
||||
if (result.success && result.filePath) {
|
||||
const filename = result.filePath.split('/').pop() || result.filePath
|
||||
const exportedFilePath = result.filePath
|
||||
toast.add({
|
||||
title: t('common.exportSuccess'),
|
||||
description: filename,
|
||||
color: 'primary',
|
||||
actions: [
|
||||
{
|
||||
label: t('common.openFolder'),
|
||||
onClick: () => {
|
||||
useCacheService().showInFolder(exportedFilePath)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
} else {
|
||||
toast.fail(t('common.exportFailed'), { description: result.error })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('导出对话失败:', error)
|
||||
toast.fail(t('common.exportFailed'), { description: String(error) })
|
||||
} finally {
|
||||
isExporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 打开当前 AI 日志文件并定位到文件
|
||||
async function openAiLogFile() {
|
||||
if (isOpeningLog.value) return
|
||||
isOpeningLog.value = true
|
||||
try {
|
||||
const result = await useAIService().showAiLogFile()
|
||||
if (!result?.success) {
|
||||
toast.fail(t('ai.chat.statusBar.log.openFailed'), {
|
||||
description: result?.error || t('ai.chat.statusBar.log.openFailedDesc'),
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('打开 AI 日志失败:', error)
|
||||
toast.fail(t('ai.chat.statusBar.log.openFailed'), { description: String(error) })
|
||||
} finally {
|
||||
isOpeningLog.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── Thinking level selector ───────────────────────────────────────────────────
|
||||
|
||||
const isThinkingPopoverOpen = ref(false)
|
||||
@@ -340,19 +218,33 @@ const thinkingLevelLabel = computed(() => {
|
||||
<span>{{ agentPhaseShortText }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Context 进度条 -->
|
||||
<!-- Context occupancy ring: geometry matches the DSH composer meter. -->
|
||||
<UTooltip v-if="contextTokens > 0" :ui="{ content: 'h-auto py-1.5' }">
|
||||
<div
|
||||
class="hidden shrink-0 items-center gap-1.5 rounded-md px-1.5 py-1 text-xs text-gray-400 dark:text-gray-500 md:flex"
|
||||
class="hidden h-7 w-7 shrink-0 place-items-center rounded-full text-gray-400 transition-colors hover:bg-gray-100 md:grid dark:text-gray-500 dark:hover:bg-gray-800"
|
||||
:aria-label="`${t('ai.chat.statusBar.agent.contextTokens')} ${contextUsagePercent}%`"
|
||||
>
|
||||
<div class="h-1.5 w-10 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div
|
||||
class="h-full rounded-full transition-all duration-300"
|
||||
:class="contextBarColor"
|
||||
:style="{ width: `${contextUsagePercent}%` }"
|
||||
<svg viewBox="0 0 14 14" width="14" height="14" aria-hidden="true">
|
||||
<circle
|
||||
cx="7"
|
||||
cy="7"
|
||||
:r="contextRingRadius"
|
||||
fill="none"
|
||||
class="stroke-gray-300 dark:stroke-gray-700"
|
||||
stroke-width="2"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-[10px]">{{ contextUsagePercent }}%</span>
|
||||
<circle
|
||||
cx="7"
|
||||
cy="7"
|
||||
:r="contextRingRadius"
|
||||
fill="none"
|
||||
class="stroke-gray-400 transition-[stroke-dasharray] duration-300 dark:stroke-gray-500"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
:stroke-dasharray="contextRingDasharray"
|
||||
transform="rotate(-90 7 7)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<template #content>
|
||||
<div class="space-y-0.5 whitespace-nowrap text-xs">
|
||||
@@ -365,27 +257,6 @@ const thinkingLevelLabel = computed(() => {
|
||||
</div>
|
||||
</template>
|
||||
</UTooltip>
|
||||
|
||||
<!-- 导出按钮 -->
|
||||
<button
|
||||
class="flex shrink-0 items-center gap-1 rounded-md px-1.5 py-1 text-xs text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 disabled:cursor-not-allowed disabled:opacity-60 dark:hover:bg-gray-800 dark:hover:text-gray-300"
|
||||
:title="t('ai.chat.statusBar.export.title')"
|
||||
:disabled="isExporting || !canExportConversation"
|
||||
@click="handleExportConversation"
|
||||
>
|
||||
<UIcon name="i-heroicons-arrow-down-tray" class="h-3.5 w-3.5" />
|
||||
<span class="hidden xl:inline">{{ t('ai.chat.statusBar.export.label') }}</span>
|
||||
</button>
|
||||
<!-- 日志按钮 -->
|
||||
<button
|
||||
class="flex shrink-0 items-center gap-1 rounded-md px-1.5 py-1 text-xs text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 disabled:cursor-not-allowed disabled:opacity-60 dark:hover:bg-gray-800 dark:hover:text-gray-300"
|
||||
:title="t('ai.chat.statusBar.log.title')"
|
||||
:disabled="isOpeningLog"
|
||||
@click="openAiLogFile"
|
||||
>
|
||||
<UIcon name="i-heroicons-document-text" class="h-3.5 w-3.5" />
|
||||
<span class="hidden xl:inline">{{ t('ai.chat.statusBar.log.label') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -88,8 +88,8 @@ defineExpose({ focus, getSelection, setSelectionRange, syncHeight })
|
||||
: '',
|
||||
]"
|
||||
>
|
||||
<div class="relative px-4 pt-2.5 pb-2.5">
|
||||
<div class="flex items-start gap-2 pr-10">
|
||||
<div class="px-4 pt-2.5 pb-2.5" :class="{ relative: !embedded }">
|
||||
<div class="flex items-start gap-2" :class="{ 'pr-10': !embedded }">
|
||||
<div
|
||||
v-if="activeSkillName"
|
||||
class="inline-flex max-w-[180px] shrink-0 items-center rounded-md bg-primary-50 px-2 text-sm leading-6 font-medium text-primary-700 dark:bg-primary-500/10 dark:text-primary-400"
|
||||
|
||||
@@ -652,7 +652,7 @@ defineExpose({
|
||||
|
||||
<template>
|
||||
<div class="shrink-0" :class="[props.embedded ? '' : 'pt-2 pb-2']">
|
||||
<div ref="rootRef" class="relative w-full max-w-4xl mx-auto">
|
||||
<div ref="rootRef" class="w-full max-w-4xl mx-auto" :class="{ relative: !props.embedded }">
|
||||
<SlashCommandMenu
|
||||
:visible="showSlashMenu"
|
||||
:skills="filteredSkills"
|
||||
|
||||
@@ -98,6 +98,9 @@ const {
|
||||
|
||||
provide('session-switch-loading', isSessionSwitching)
|
||||
|
||||
const isAIChatRestoring = ref(activeTab.value === 'ai-chat' && typeof route.query.aiChatId === 'string')
|
||||
const showSessionLoading = computed(() => isSessionSwitching.value || isAIChatRestoring.value)
|
||||
|
||||
// 当前筛选后的消息总数
|
||||
const filteredMessageCount = computed(() => {
|
||||
return memberActivity.value.reduce((sum, m) => sum + m.messageCount, 0)
|
||||
@@ -112,7 +115,7 @@ const filteredMemberCount = computed(() => {
|
||||
<template>
|
||||
<div class="relative flex h-full flex-col dark:bg-page-dark" style="padding-top: var(--titlebar-area-height)">
|
||||
<div
|
||||
v-if="isSessionSwitching"
|
||||
v-if="showSessionLoading"
|
||||
data-testid="group-chat-switch-loading"
|
||||
class="absolute inset-0 z-20 flex cursor-wait items-center justify-center bg-page-bg dark:bg-page-dark"
|
||||
:style="{ paddingTop: 'var(--titlebar-area-height)' }"
|
||||
@@ -146,7 +149,7 @@ const filteredMemberCount = computed(() => {
|
||||
<!-- Tab Content -->
|
||||
<div class="relative flex-1 overflow-y-auto">
|
||||
<!-- Loading Overlay -->
|
||||
<LoadingState v-if="isLoading && !isSessionSwitching" variant="overlay" :text="t('common.loading')" />
|
||||
<LoadingState v-if="isLoading && !showSessionLoading" variant="overlay" :text="t('common.loading')" />
|
||||
|
||||
<div class="h-full">
|
||||
<Transition name="tab-slide" mode="out-in">
|
||||
@@ -176,6 +179,7 @@ const filteredMemberCount = computed(() => {
|
||||
:session-id="currentSessionId!"
|
||||
:session-name="session.name"
|
||||
chat-type="group"
|
||||
@restore-loading-change="isAIChatRestoring = $event"
|
||||
/>
|
||||
<MemoryTab
|
||||
v-else-if="activeTab === 'memory'"
|
||||
|
||||
@@ -92,6 +92,9 @@ const {
|
||||
|
||||
provide('session-switch-loading', isSessionSwitching)
|
||||
|
||||
const isAIChatRestoring = ref(activeTab.value === 'ai-chat' && typeof route.query.aiChatId === 'string')
|
||||
const showSessionLoading = computed(() => isSessionSwitching.value || isAIChatRestoring.value)
|
||||
|
||||
// 当前筛选后的消息总数
|
||||
const filteredMessageCount = computed(() => {
|
||||
return memberActivity.value.reduce((sum, m) => sum + m.messageCount, 0)
|
||||
@@ -132,7 +135,7 @@ const otherMemberAvatar = computed(() => {
|
||||
<template>
|
||||
<div class="relative flex h-full flex-col dark:bg-page-dark" style="padding-top: var(--titlebar-area-height)">
|
||||
<div
|
||||
v-if="isSessionSwitching"
|
||||
v-if="showSessionLoading"
|
||||
data-testid="private-chat-switch-loading"
|
||||
class="absolute inset-0 z-20 flex cursor-wait items-center justify-center bg-page-bg dark:bg-page-dark"
|
||||
:style="{ paddingTop: 'var(--titlebar-area-height)' }"
|
||||
@@ -165,7 +168,7 @@ const otherMemberAvatar = computed(() => {
|
||||
<!-- Tab Content -->
|
||||
<div class="relative flex-1 overflow-y-auto">
|
||||
<!-- Loading Overlay -->
|
||||
<LoadingState v-if="isLoading && !isSessionSwitching" variant="overlay" :text="t('common.loading')" />
|
||||
<LoadingState v-if="isLoading && !showSessionLoading" variant="overlay" :text="t('common.loading')" />
|
||||
|
||||
<div class="h-full">
|
||||
<Transition name="tab-slide" mode="out-in">
|
||||
@@ -189,6 +192,7 @@ const otherMemberAvatar = computed(() => {
|
||||
:session-id="currentSessionId!"
|
||||
:session-name="session.name"
|
||||
chat-type="private"
|
||||
@restore-loading-change="isAIChatRestoring = $event"
|
||||
/>
|
||||
<MemoryTab
|
||||
v-else-if="activeTab === 'memory'"
|
||||
|
||||
Reference in new issue
Block a user