| | |
| | | class="rounded-2xl" |
| | | /> |
| | | |
| | | <div v-if="traceEvents.length" class="rounded-3xl bg-[var(--art-main-bg-color)] px-5 py-4 shadow-[0_12px_36px_rgba(15,23,42,0.05)] ring-1 ring-[var(--el-border-color-lighter)]"> |
| | | <div class="mb-3 flex items-center justify-between gap-3"> |
| | | <div class="text-sm font-semibold text-[var(--art-gray-900)]">{{ $t('ai.drawer.activityTrace') }}</div> |
| | | <ElTag effect="plain" round>{{ traceEvents.length }}</ElTag> |
| | | </div> |
| | | <div class="space-y-3"> |
| | | <div class="min-h-0 flex-1 ai-chat-workspace" :class="{ 'ai-chat-workspace--trace-collapsed': !tracePanelExpanded }"> |
| | | <div class="flex min-h-0 flex-col ai-chat-trace-column" :class="{ 'ai-chat-trace-column--collapsed': !tracePanelExpanded }"> |
| | | <div |
| | | v-for="item in traceEvents" |
| | | :key="item.traceId" |
| | | class="rounded-2xl bg-g-100/55 px-4 py-3 ring-1 ring-[var(--el-border-color-extra-light)]" |
| | | v-if="tracePanelExpanded" |
| | | class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-3xl bg-[var(--art-main-bg-color)] shadow-[0_12px_36px_rgba(15,23,42,0.05)] ring-1 ring-[var(--el-border-color-lighter)]" |
| | | > |
| | | <div class="flex flex-wrap items-center justify-between gap-3"> |
| | | <div class="min-w-0"> |
| | | <div class="flex items-center gap-2"> |
| | | <ElTag size="small" :type="item.traceType === 'thinking' ? 'primary' : 'success'" effect="light"> |
| | | {{ item.traceType === 'thinking' ? $t('ai.drawer.traceTypeThinking') : $t('ai.drawer.traceTypeTool') }} |
| | | </ElTag> |
| | | <span class="text-sm font-medium text-[var(--art-gray-900)]"> |
| | | {{ item.toolName || item.title || $t('ai.drawer.unknownTool') }} |
| | | </span> |
| | | </div> |
| | | <div class="mt-1 text-xs text-[var(--art-gray-500)]"> |
| | | {{ item.traceType === 'thinking' ? getThinkingStatusLabel(item.status) : getToolStatusLabel(item.status) }} |
| | | </div> |
| | | </div> |
| | | |
| | | <ElButton |
| | | v-if="item.traceType !== 'thinking'" |
| | | text |
| | | size="small" |
| | | @click="toggleTraceEventExpanded(item.traceId)" |
| | | > |
| | | {{ $t(expandedTraceIds.includes(item.traceId) ? 'ai.drawer.collapseDetail' : 'ai.drawer.viewDetail') }} |
| | | </ElButton> |
| | | </div> |
| | | |
| | | <div |
| | | v-if="item.traceType === 'thinking'" |
| | | class="mt-3 whitespace-pre-wrap break-words text-sm leading-6 text-[var(--art-gray-700)]" |
| | | > |
| | | {{ item.content || $t('ai.drawer.thinkingEmpty') }} |
| | | </div> |
| | | |
| | | <div |
| | | v-else-if="expandedTraceIds.includes(item.traceId)" |
| | | class="mt-3 space-y-2 text-sm leading-6 text-[var(--art-gray-700)]" |
| | | > |
| | | <div v-if="item.title">{{ item.title }}</div> |
| | | <div v-if="item.inputSummary">{{ $t('ai.drawer.toolInput', { value: item.inputSummary }) }}</div> |
| | | <div v-if="item.outputSummary">{{ $t('ai.drawer.toolOutput', { value: item.outputSummary }) }}</div> |
| | | <div v-if="item.errorMessage" class="text-[var(--el-color-danger)]"> |
| | | {{ $t('ai.drawer.toolError', { value: item.errorMessage }) }} |
| | | <div class="flex items-center justify-between gap-3 border-b border-[var(--el-border-color-extra-light)] px-5 py-4"> |
| | | <div class="text-sm font-semibold text-[var(--art-gray-900)]">{{ $t('ai.drawer.activityTrace') }}</div> |
| | | <div class="flex items-center gap-2"> |
| | | <ElTag effect="plain" round>{{ traceEvents.length }}</ElTag> |
| | | <ElButton text size="small" @click="tracePanelExpanded = false"> |
| | | {{ $t('ai.drawer.traceCollapse') }} |
| | | </ElButton> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-3xl bg-[var(--art-main-bg-color)] shadow-[0_12px_36px_rgba(15,23,42,0.05)] ring-1 ring-[var(--el-border-color-lighter)]"> |
| | | <div class="flex flex-wrap items-center justify-between gap-3 border-b border-[var(--el-border-color-extra-light)] px-5 py-4"> |
| | | <div> |
| | | <div class="text-sm font-semibold text-[var(--art-gray-900)]">{{ currentSessionTitle }}</div> |
| | | <div class="mt-1 text-xs text-[var(--art-gray-500)]"> |
| | | {{ usageSummaryText }} |
| | | <div class="min-h-0 flex-1 overflow-hidden"> |
| | | <ElScrollbar class="h-full bg-g-100/35 px-4 py-4"> |
| | | <div v-if="!traceEvents.length" class="rounded-3xl border border-dashed border-[var(--el-border-color)] bg-slate-50/80 px-4 py-8 text-center text-sm text-[var(--art-gray-500)]"> |
| | | {{ $t('ai.drawer.noActivityTrace') }} |
| | | </div> |
| | | |
| | | <div v-else class="space-y-3"> |
| | | <div |
| | | v-for="item in traceEvents" |
| | | :key="item.traceId" |
| | | class="rounded-2xl bg-[var(--art-main-bg-color)] px-4 py-3 ring-1 ring-[var(--el-border-color-extra-light)]" |
| | | > |
| | | <div class="flex flex-wrap items-center justify-between gap-3"> |
| | | <div class="min-w-0"> |
| | | <div class="flex items-center gap-2"> |
| | | <ElTag size="small" :type="item.traceType === 'thinking' ? 'primary' : 'success'" effect="light"> |
| | | {{ item.traceType === 'thinking' ? $t('ai.drawer.traceTypeThinking') : $t('ai.drawer.traceTypeTool') }} |
| | | </ElTag> |
| | | <span class="text-sm font-medium text-[var(--art-gray-900)]"> |
| | | {{ item.toolName || item.title || $t('ai.drawer.unknownTool') }} |
| | | </span> |
| | | </div> |
| | | <div class="mt-1 text-xs text-[var(--art-gray-500)]"> |
| | | {{ item.traceType === 'thinking' ? getThinkingStatusLabel(item.status) : getToolStatusLabel(item.status) }} |
| | | </div> |
| | | </div> |
| | | |
| | | <ElButton |
| | | v-if="item.traceType !== 'thinking'" |
| | | text |
| | | size="small" |
| | | @click="toggleTraceEventExpanded(item.traceId)" |
| | | > |
| | | {{ $t(expandedTraceIds.includes(item.traceId) ? 'ai.drawer.collapseDetail' : 'ai.drawer.viewDetail') }} |
| | | </ElButton> |
| | | </div> |
| | | |
| | | <div |
| | | v-if="item.traceType === 'thinking'" |
| | | class="mt-3 whitespace-pre-wrap break-words text-sm leading-6 text-[var(--art-gray-700)]" |
| | | > |
| | | {{ item.content || $t('ai.drawer.thinkingEmpty') }} |
| | | </div> |
| | | |
| | | <div |
| | | v-else-if="expandedTraceIds.includes(item.traceId)" |
| | | class="mt-3 space-y-2 text-sm leading-6 text-[var(--art-gray-700)]" |
| | | > |
| | | <div v-if="item.title">{{ item.title }}</div> |
| | | <div v-if="item.inputSummary">{{ $t('ai.drawer.toolInput', { value: item.inputSummary }) }}</div> |
| | | <div v-if="item.outputSummary">{{ $t('ai.drawer.toolOutput', { value: item.outputSummary }) }}</div> |
| | | <div v-if="item.errorMessage" class="text-[var(--el-color-danger)]"> |
| | | {{ $t('ai.drawer.toolError', { value: item.errorMessage }) }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </ElScrollbar> |
| | | </div> |
| | | </div> |
| | | <div class="flex flex-wrap gap-2"> |
| | | <ElTag v-if="usage?.totalTokens != null" effect="plain" round> |
| | | {{ $t('ai.drawer.tokenMetric', { |
| | | prompt: usage?.promptTokens ?? 0, |
| | | completion: usage?.completionTokens ?? 0, |
| | | total: usage?.totalTokens ?? 0 |
| | | }) }} |
| | | </ElTag> |
| | | </div> |
| | | </div> |
| | | |
| | | <ElScrollbar class="min-h-0 flex-1 bg-g-100/35 px-5 py-5"> |
| | | <div class="space-y-5"> |
| | | <div v-if="!messages.length" class="rounded-3xl border border-dashed border-[var(--el-border-color)] bg-slate-50/80 px-4 py-8 text-center text-sm text-[var(--art-gray-500)]"> |
| | | {{ $t('ai.drawer.emptyHint') }} |
| | | </div> |
| | | |
| | | <div |
| | | v-for="(message, index) in messages" |
| | | :key="`${message.role}-${index}`" |
| | | class="flex items-end gap-3" |
| | | :class="message.role === 'user' ? 'justify-end' : 'justify-start'" |
| | | <button |
| | | v-else |
| | | type="button" |
| | | class="flex min-h-0 flex-1 flex-col items-center justify-center gap-3 rounded-3xl bg-[var(--art-main-bg-color)] px-3 py-5 text-center shadow-[0_12px_36px_rgba(15,23,42,0.05)] ring-1 ring-[var(--el-border-color-lighter)] transition-colors hover:bg-[var(--el-color-primary-light-9)]/60" |
| | | @click="tracePanelExpanded = true" |
| | | > |
| | | <div |
| | | v-if="message.role !== 'user'" |
| | | class="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-[var(--el-color-primary-light-9)] text-[var(--el-color-primary)]" |
| | | > |
| | | <ArtSvgIcon icon="ri:robot-2-line" class="text-base" /> |
| | | <div class="flex size-11 items-center justify-center rounded-2xl bg-[var(--el-color-primary-light-9)] text-[var(--el-color-primary)]"> |
| | | <ArtSvgIcon icon="ri:sidebar-unfold-line" class="text-lg" /> |
| | | </div> |
| | | |
| | | <div class="max-w-[82%]"> |
| | | <div |
| | | class="rounded-3xl px-4 py-3 text-sm leading-6 shadow-[0_10px_30px_rgba(15,23,42,0.04)]" |
| | | :class=" |
| | | message.role === 'user' |
| | | ? 'rounded-br-xl bg-[var(--el-color-primary)] text-white' |
| | | : 'rounded-bl-xl bg-[var(--art-main-bg-color)] text-[var(--art-gray-900)] ring-1 ring-[var(--el-border-color-extra-light)]' |
| | | " |
| | | > |
| | | <MarkdownMessage |
| | | v-if="message.role === 'assistant'" |
| | | :content=" |
| | | message.content || (streaming && index === messages.length - 1 ? $t('ai.drawer.thinking') : '') |
| | | " |
| | | /> |
| | | <div v-else class="whitespace-pre-wrap break-words"> |
| | | {{ message.content || '' }} |
| | | </div> |
| | | </div> |
| | | <div class="text-xs font-medium leading-5 text-[var(--art-gray-700)] ai-chat-trace-collapsed-label"> |
| | | {{ $t('ai.drawer.activityTrace') }} |
| | | </div> |
| | | |
| | | <div |
| | | v-if="message.role === 'user'" |
| | | class="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-[var(--el-color-primary)] text-white" |
| | | > |
| | | <ArtSvgIcon icon="ri:user-3-line" class="text-base" /> |
| | | </div> |
| | | </div> |
| | | |
| | | <div ref="messagesBottomRef"></div> |
| | | </div> |
| | | </ElScrollbar> |
| | | |
| | | <div class="border-t border-[var(--el-border-color-extra-light)] bg-[var(--art-main-bg-color)] px-5 py-4"> |
| | | <div class="rounded-3xl bg-g-100/45 p-4 ring-1 ring-[var(--el-border-color-lighter)]"> |
| | | <ElInput |
| | | v-model="input" |
| | | type="textarea" |
| | | :autosize="{ minRows: 3, maxRows: 6 }" |
| | | resize="none" |
| | | :placeholder="$t('ai.drawer.inputPlaceholder')" |
| | | @keydown="handleInputKeydown" |
| | | /> |
| | | |
| | | <div class="mt-3 flex flex-wrap items-center justify-between gap-3"> |
| | | <ElTag effect="plain" round>{{ traceEvents.length }}</ElTag> |
| | | <div class="text-xs text-[var(--art-gray-500)]"> |
| | | {{ $t('ai.drawer.inputHotkeyHint') }} |
| | | {{ $t('ai.drawer.traceExpand') }} |
| | | </div> |
| | | </button> |
| | | </div> |
| | | |
| | | <div class="flex min-h-0 flex-1 flex-col gap-4 ai-chat-main-column"> |
| | | <div class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-3xl bg-[var(--art-main-bg-color)] shadow-[0_12px_36px_rgba(15,23,42,0.05)] ring-1 ring-[var(--el-border-color-lighter)]"> |
| | | <div class="flex flex-wrap items-center justify-between gap-3 border-b border-[var(--el-border-color-extra-light)] px-5 py-4"> |
| | | <div> |
| | | <div class="text-sm font-semibold text-[var(--art-gray-900)]">{{ currentSessionTitle }}</div> |
| | | <div class="mt-1 text-xs text-[var(--art-gray-500)]"> |
| | | {{ usageSummaryText }} |
| | | </div> |
| | | </div> |
| | | <div class="flex flex-wrap gap-2"> |
| | | <ElTag v-if="usage?.totalTokens != null" effect="plain" round> |
| | | {{ $t('ai.drawer.tokenMetric', { |
| | | prompt: usage?.promptTokens ?? 0, |
| | | completion: usage?.completionTokens ?? 0, |
| | | total: usage?.totalTokens ?? 0 |
| | | }) }} |
| | | </ElTag> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="flex flex-wrap items-center gap-2"> |
| | | <ElButton text @click="input = ''">{{ $t('ai.drawer.clearInput') }}</ElButton> |
| | | <ElButton |
| | | v-if="streaming" |
| | | type="warning" |
| | | plain |
| | | @click="stopStream(true)" |
| | | > |
| | | {{ $t('ai.drawer.stop') }} |
| | | </ElButton> |
| | | <ElButton |
| | | v-else |
| | | type="primary" |
| | | @click="handleSend" |
| | | > |
| | | <ArtSvgIcon icon="ri:send-plane-2-line" class="mr-1 text-sm" /> |
| | | {{ $t('ai.drawer.send') }} |
| | | </ElButton> |
| | | <ElScrollbar class="min-h-0 flex-1 bg-g-100/35 px-5 py-5"> |
| | | <div class="space-y-5"> |
| | | <div v-if="!messages.length" class="rounded-3xl border border-dashed border-[var(--el-border-color)] bg-slate-50/80 px-4 py-8 text-center text-sm text-[var(--art-gray-500)]"> |
| | | {{ $t('ai.drawer.emptyHint') }} |
| | | </div> |
| | | |
| | | <div |
| | | v-for="(message, index) in messages" |
| | | :key="`${message.role}-${index}`" |
| | | class="flex items-end gap-3" |
| | | :class="message.role === 'user' ? 'justify-end' : 'justify-start'" |
| | | > |
| | | <div |
| | | v-if="message.role !== 'user'" |
| | | class="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-[var(--el-color-primary-light-9)] text-[var(--el-color-primary)]" |
| | | > |
| | | <ArtSvgIcon icon="ri:robot-2-line" class="text-base" /> |
| | | </div> |
| | | |
| | | <div class="max-w-[82%]"> |
| | | <div |
| | | class="rounded-3xl px-4 py-3 text-sm leading-6 shadow-[0_10px_30px_rgba(15,23,42,0.04)]" |
| | | :class=" |
| | | message.role === 'user' |
| | | ? 'rounded-br-xl bg-[var(--el-color-primary)] text-white' |
| | | : 'rounded-bl-xl bg-[var(--art-main-bg-color)] text-[var(--art-gray-900)] ring-1 ring-[var(--el-border-color-extra-light)]' |
| | | " |
| | | > |
| | | <MarkdownMessage |
| | | v-if="message.role === 'assistant'" |
| | | :content=" |
| | | message.content || (streaming && index === messages.length - 1 ? $t('ai.drawer.thinking') : '') |
| | | " |
| | | /> |
| | | <div v-else class="whitespace-pre-wrap break-words"> |
| | | {{ message.content || '' }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div |
| | | v-if="message.role === 'user'" |
| | | class="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-[var(--el-color-primary)] text-white" |
| | | > |
| | | <ArtSvgIcon icon="ri:user-3-line" class="text-base" /> |
| | | </div> |
| | | </div> |
| | | |
| | | <div ref="messagesBottomRef"></div> |
| | | </div> |
| | | </ElScrollbar> |
| | | </div> |
| | | |
| | | <div class="rounded-3xl bg-[var(--art-main-bg-color)] px-5 py-4 shadow-[0_12px_36px_rgba(15,23,42,0.05)] ring-1 ring-[var(--el-border-color-lighter)]"> |
| | | <div class="rounded-3xl bg-g-100/45 p-4 ring-1 ring-[var(--el-border-color-lighter)]"> |
| | | <ElInput |
| | | v-model="input" |
| | | type="textarea" |
| | | :autosize="{ minRows: 3, maxRows: 6 }" |
| | | resize="none" |
| | | :placeholder="$t('ai.drawer.inputPlaceholder')" |
| | | @keydown="handleInputKeydown" |
| | | /> |
| | | |
| | | <div class="mt-3 flex flex-wrap items-center justify-between gap-3"> |
| | | <div class="text-xs text-[var(--art-gray-500)]"> |
| | | {{ $t('ai.drawer.inputHotkeyHint') }} |
| | | </div> |
| | | |
| | | <div class="flex flex-wrap items-center gap-2"> |
| | | <ElButton text @click="input = ''">{{ $t('ai.drawer.clearInput') }}</ElButton> |
| | | <ElButton |
| | | v-if="streaming" |
| | | type="warning" |
| | | plain |
| | | @click="stopStream(true)" |
| | | > |
| | | {{ $t('ai.drawer.stop') }} |
| | | </ElButton> |
| | | <ElButton |
| | | v-else |
| | | type="primary" |
| | | @click="handleSend" |
| | | > |
| | | <ArtSvgIcon icon="ri:send-plane-2-line" class="mr-1 text-sm" /> |
| | | {{ $t('ai.drawer.send') }} |
| | | </ElButton> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | </div> |
| | |
| | | const loadingRuntime = ref(false) |
| | | const streaming = ref(false) |
| | | const runtimePanelExpanded = ref(false) |
| | | const tracePanelExpanded = ref(false) |
| | | const messagesBottomRef = ref(null) |
| | | const renameDialog = reactive({ |
| | | open: false, |
| | |
| | | watch(isDrawerVisible, async (visible) => { |
| | | if (visible) { |
| | | runtimePanelExpanded.value = false |
| | | tracePanelExpanded.value = false |
| | | await initializeDrawer() |
| | | scrollMessagesToBottom() |
| | | return |
| | |
| | | width: 320px; |
| | | } |
| | | |
| | | .ai-chat-workspace { |
| | | display: flex; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .ai-chat-trace-column { |
| | | width: 360px; |
| | | flex-shrink: 0; |
| | | transition: |
| | | width 0.2s ease, |
| | | min-width 0.2s ease; |
| | | } |
| | | |
| | | .ai-chat-trace-column--collapsed { |
| | | width: 88px; |
| | | } |
| | | |
| | | .ai-chat-trace-collapsed-label { |
| | | writing-mode: vertical-rl; |
| | | text-orientation: mixed; |
| | | } |
| | | |
| | | .ai-chat-main-column { |
| | | min-width: 0; |
| | | } |
| | | |
| | | :deep(.ai-chat-drawer .el-drawer__body) { |
| | | padding: 0; |
| | | } |
| | |
| | | border-right: 0; |
| | | border-bottom: 1px solid var(--art-border-color); |
| | | } |
| | | |
| | | .ai-chat-workspace { |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .ai-chat-trace-column { |
| | | width: 100%; |
| | | min-height: 260px; |
| | | } |
| | | |
| | | .ai-chat-trace-column--collapsed { |
| | | width: 100%; |
| | | min-height: auto; |
| | | } |
| | | |
| | | .ai-chat-trace-collapsed-label { |
| | | writing-mode: horizontal-tb; |
| | | text-orientation: initial; |
| | | } |
| | | } |
| | | </style> |