| | |
| | | class="ai-chat-drawer" |
| | | > |
| | | <div class="flex h-full min-h-0 flex-col overflow-hidden bg-[var(--art-main-bg-color)]"> |
| | | <div class="flex items-center gap-4 border-b border-[var(--el-border-color-lighter)] bg-[var(--art-main-bg-color)] px-6 py-4"> |
| | | <div class="flex size-11 items-center justify-center rounded-3xl bg-[var(--el-color-primary-light-9)] text-[var(--el-color-primary)]"> |
| | | <ArtSvgIcon icon="ri:robot-2-line" class="text-[22px]" /> |
| | | </div> |
| | | <div class="min-w-0 flex-1"> |
| | | <div class="flex flex-wrap items-center gap-2"> |
| | | <h3 class="text-base font-semibold text-[var(--art-gray-900)]">{{ $t('ai.drawer.title') }}</h3> |
| | | <ElTag v-if="streaming" type="success" effect="light" round>{{ $t('ai.drawer.streaming') }}</ElTag> |
| | | <div class="border-b border-[var(--el-border-color-lighter)] bg-[var(--art-main-bg-color)] px-5 py-3"> |
| | | <div class="flex items-center gap-3"> |
| | | <div class="flex size-10 items-center justify-center rounded-3xl bg-[var(--el-color-primary-light-9)] text-[var(--el-color-primary)]"> |
| | | <ArtSvgIcon icon="ri:robot-2-line" class="text-xl" /> |
| | | </div> |
| | | <p class="mt-1 truncate text-xs text-[var(--art-gray-500)]"> |
| | | {{ runtime?.promptName || runtime?.promptCode || DEFAULT_PROMPT_CODE }} |
| | | </p> |
| | | <div class="min-w-0 flex-1"> |
| | | <div class="flex flex-wrap items-center gap-2"> |
| | | <h3 class="text-base font-semibold text-[var(--art-gray-900)]">{{ $t('ai.drawer.title') }}</h3> |
| | | <ElTag v-if="streaming" type="success" effect="light" round>{{ $t('ai.drawer.streaming') }}</ElTag> |
| | | </div> |
| | | <p class="mt-1 truncate text-xs text-[var(--art-gray-500)]"> |
| | | {{ runtime?.promptName || runtime?.promptCode || DEFAULT_PROMPT_CODE }} |
| | | </p> |
| | | </div> |
| | | <ElButton plain :disabled="streaming" @click="startNewSession"> |
| | | <ArtSvgIcon icon="ri:add-line" class="mr-1 text-sm" /> |
| | | {{ $t('ai.drawer.newSession') }} |
| | | </ElButton> |
| | | <ArtIconButton icon="ri:close-line" @click="closeChat" /> |
| | | </div> |
| | | <ElButton plain :disabled="streaming" @click="startNewSession"> |
| | | <ArtSvgIcon icon="ri:add-line" class="mr-1 text-sm" /> |
| | | {{ $t('ai.drawer.newSession') }} |
| | | </ElButton> |
| | | <ArtIconButton icon="ri:close-line" @click="closeChat" /> |
| | | |
| | | <ElCollapseTransition> |
| | | <div |
| | | v-if="!runtimePreviewCollapsed" |
| | | class="mt-3 rounded-3xl bg-g-100/35 px-4 py-3 ring-1 ring-[var(--el-border-color-lighter)]" |
| | | > |
| | | <div class="flex flex-wrap items-start justify-between gap-3"> |
| | | <div class="min-w-0 flex-1"> |
| | | <div class="flex flex-wrap items-center gap-2"> |
| | | <div class="text-sm font-semibold text-[var(--art-gray-900)]">{{ $t('ai.drawer.runtimeOverview') }}</div> |
| | | <div class="text-xs text-[var(--art-gray-500)]">{{ usageSummaryText }}</div> |
| | | </div> |
| | | |
| | | <div class="mt-3 flex flex-wrap gap-2"> |
| | | <div |
| | | v-for="item in runtimeMetricCards" |
| | | :key="item.label" |
| | | class="flex min-w-[150px] flex-1 items-center gap-2 rounded-2xl bg-[var(--art-main-bg-color)] px-3 py-2 ring-1 ring-[var(--el-border-color-extra-light)]" |
| | | > |
| | | <ArtSvgIcon :icon="item.icon" class="text-sm text-[var(--art-gray-500)]" /> |
| | | <div class="min-w-0 flex-1"> |
| | | <div class="text-[11px] text-[var(--art-gray-500)]">{{ item.label }}</div> |
| | | <div class="truncate text-sm font-medium text-[var(--art-gray-900)]">{{ item.value }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="flex shrink-0 items-center 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> |
| | | <ElButton text @click="runtimePreviewCollapsed = true"> |
| | | {{ $t('ai.drawer.runtimePreviewCollapse') }} |
| | | </ElButton> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="mt-3 space-y-3 border-t border-[var(--el-border-color-extra-light)] pt-3"> |
| | | <div class="flex flex-wrap items-center gap-3"> |
| | | <ElSelect |
| | | v-if="selectableModelOptions.length" |
| | | v-model="selectedAiParamId" |
| | | :placeholder="$t('ai.drawer.modelSelectorLabel')" |
| | | :disabled="streaming || loadingRuntime || selectableModelOptions.length <= 1" |
| | | class="min-w-[280px]" |
| | | @change="handleModelChange" |
| | | > |
| | | <ElOption |
| | | v-for="item in selectableModelOptions" |
| | | :key="String(item.aiParamId)" |
| | | :label="formatModelOption(item)" |
| | | :value="item.aiParamId" |
| | | /> |
| | | </ElSelect> |
| | | |
| | | <div class="flex flex-wrap gap-2"> |
| | | <ElButton |
| | | v-for="item in quickLinks" |
| | | :key="item.path" |
| | | plain |
| | | size="small" |
| | | @click="navigateTo(item.path)" |
| | | > |
| | | {{ item.label }} |
| | | </ElButton> |
| | | <ElButton plain size="small" :disabled="!sessionId || streaming" @click="handleRetainLatestRound"> |
| | | {{ $t('ai.drawer.retainLatestRound') }} |
| | | </ElButton> |
| | | <ElButton plain size="small" :disabled="!sessionId || streaming" @click="handleClearMemory"> |
| | | {{ $t('ai.drawer.clearMemory') }} |
| | | </ElButton> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="flex flex-wrap gap-2"> |
| | | <ElTag effect="plain" round>{{ $t('ai.drawer.requestMetric', { value: runtimeSummary.requestId }) }}</ElTag> |
| | | <ElTag effect="plain" round>{{ $t('ai.drawer.sessionMetric', { id: sessionId || '--' }) }}</ElTag> |
| | | <ElTag effect="plain" round>{{ $t('ai.drawer.recentMetric', { value: runtimeSummary.recentMessageCount }) }}</ElTag> |
| | | <ElTag :type="runtimeSummary.hasSummary ? 'success' : 'info'" effect="light" round> |
| | | {{ $t(runtimeSummary.hasSummary ? 'ai.drawer.hasSummary' : 'ai.drawer.noSummary') }} |
| | | </ElTag> |
| | | <ElTag :type="runtimeSummary.hasFacts ? 'success' : 'info'" effect="light" round> |
| | | {{ $t(runtimeSummary.hasFacts ? 'ai.drawer.hasFacts' : 'ai.drawer.noFacts') }} |
| | | </ElTag> |
| | | </div> |
| | | |
| | | <ElAlert |
| | | v-if="runtime?.memorySummary" |
| | | type="info" |
| | | :closable="false" |
| | | :title="runtime.memorySummary" |
| | | /> |
| | | <ElAlert |
| | | v-if="runtime?.memoryFacts" |
| | | type="success" |
| | | :closable="false" |
| | | :title="runtime.memoryFacts" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </ElCollapseTransition> |
| | | |
| | | <ElCollapseTransition> |
| | | <div |
| | | v-if="runtimePreviewCollapsed" |
| | | class="mt-3 flex flex-wrap items-center justify-between gap-2 rounded-3xl bg-g-100/35 px-3 py-2 ring-1 ring-[var(--el-border-color-lighter)]" |
| | | > |
| | | <div class="flex min-w-0 flex-1 flex-wrap items-center gap-2"> |
| | | <div class="text-sm font-semibold text-[var(--art-gray-900)]">{{ $t('ai.drawer.runtimeOverview') }}</div> |
| | | <div class="inline-flex max-w-[220px] items-center gap-1 rounded-full bg-[var(--art-main-bg-color)] px-2.5 py-1 text-xs text-[var(--art-gray-500)] ring-1 ring-[var(--el-border-color-extra-light)]"> |
| | | <ArtSvgIcon icon="ri:cpu-line" class="text-[13px]" /> |
| | | <span class="truncate">{{ runtimeSummary.model }}</span> |
| | | </div> |
| | | <div class="inline-flex max-w-[220px] items-center gap-1 rounded-full bg-[var(--art-main-bg-color)] px-2.5 py-1 text-xs text-[var(--art-gray-500)] ring-1 ring-[var(--el-border-color-extra-light)]"> |
| | | <ArtSvgIcon icon="ri:magic-line" class="text-[13px]" /> |
| | | <span class="truncate">{{ runtimeSummary.promptName }}</span> |
| | | </div> |
| | | <div class="inline-flex items-center gap-1 rounded-full bg-[var(--art-main-bg-color)] px-2.5 py-1 text-xs text-[var(--art-gray-500)] ring-1 ring-[var(--el-border-color-extra-light)]"> |
| | | <ArtSvgIcon icon="ri:plug-2-line" class="text-[13px]" /> |
| | | <span>{{ runtimeSummary.mountedMcpCount }}</span> |
| | | </div> |
| | | <div class="truncate text-xs text-[var(--art-gray-500)]">{{ usageSummaryText }}</div> |
| | | </div> |
| | | <ElButton text @click="runtimePreviewCollapsed = false"> |
| | | {{ $t('ai.drawer.runtimePreviewExpand') }} |
| | | </ElButton> |
| | | </div> |
| | | </ElCollapseTransition> |
| | | </div> |
| | | |
| | | <div class="min-h-0 flex-1 bg-g-100/35 ai-chat-body"> |
| | | <aside class="box-border flex min-h-0 flex-col gap-4 p-4 ai-chat-sidebar"> |
| | | <aside class="box-border flex min-h-0 flex-col gap-3 p-3 ai-chat-sidebar"> |
| | | <div class="rounded-3xl bg-[var(--art-main-bg-color)] p-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.sessionList') }}</div> |
| | |
| | | </div> |
| | | </aside> |
| | | |
| | | <section class="box-border flex min-h-0 flex-1 flex-col gap-4 p-4 pl-0"> |
| | | <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="flex flex-wrap items-center justify-between gap-3"> |
| | | <div> |
| | | <div class="text-sm font-semibold text-[var(--art-gray-900)]">{{ $t('ai.drawer.runtimeOverview') }}</div> |
| | | <div class="mt-1 text-xs text-[var(--art-gray-500)]"> |
| | | {{ $t('ai.drawer.modelSelectorHint') }} |
| | | </div> |
| | | </div> |
| | | <ElButton text @click="runtimePanelExpanded = !runtimePanelExpanded"> |
| | | {{ $t(runtimePanelExpanded ? 'ai.drawer.runtimeCollapse' : 'ai.drawer.runtimeExpand') }} |
| | | </ElButton> |
| | | </div> |
| | | |
| | | <ElCollapseTransition> |
| | | <div v-show="runtimePanelExpanded" class="mt-4 space-y-4"> |
| | | <div class="grid gap-3 md:grid-cols-2 xl:grid-cols-4"> |
| | | <div |
| | | v-for="item in runtimeMetricCards" |
| | | :key="item.label" |
| | | class="rounded-2xl bg-g-100/55 px-4 py-3 ring-1 ring-[var(--el-border-color-extra-light)]" |
| | | > |
| | | <div class="mb-2 flex items-center gap-2 text-xs text-[var(--art-gray-500)]"> |
| | | <ArtSvgIcon :icon="item.icon" class="text-sm" /> |
| | | <span>{{ item.label }}</span> |
| | | </div> |
| | | <div class="truncate text-sm font-semibold text-[var(--art-gray-900)]"> |
| | | {{ item.value }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="flex flex-wrap items-center gap-3"> |
| | | <ElSelect |
| | | v-if="selectableModelOptions.length" |
| | | v-model="selectedAiParamId" |
| | | :placeholder="$t('ai.drawer.modelSelectorLabel')" |
| | | :disabled="streaming || loadingRuntime || selectableModelOptions.length <= 1" |
| | | class="min-w-[280px]" |
| | | @change="handleModelChange" |
| | | > |
| | | <ElOption |
| | | v-for="item in selectableModelOptions" |
| | | :key="String(item.aiParamId)" |
| | | :label="formatModelOption(item)" |
| | | :value="item.aiParamId" |
| | | /> |
| | | </ElSelect> |
| | | |
| | | <div class="flex flex-wrap gap-2"> |
| | | <ElButton |
| | | v-for="item in quickLinks" |
| | | :key="item.path" |
| | | plain |
| | | size="small" |
| | | @click="navigateTo(item.path)" |
| | | > |
| | | {{ item.label }} |
| | | </ElButton> |
| | | <ElButton plain size="small" :disabled="!sessionId || streaming" @click="handleRetainLatestRound"> |
| | | {{ $t('ai.drawer.retainLatestRound') }} |
| | | </ElButton> |
| | | <ElButton plain size="small" :disabled="!sessionId || streaming" @click="handleClearMemory"> |
| | | {{ $t('ai.drawer.clearMemory') }} |
| | | </ElButton> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="flex flex-wrap gap-2"> |
| | | <ElTag effect="plain" round>{{ $t('ai.drawer.requestMetric', { value: runtimeSummary.requestId }) }}</ElTag> |
| | | <ElTag effect="plain" round>{{ $t('ai.drawer.sessionMetric', { id: sessionId || '--' }) }}</ElTag> |
| | | <ElTag effect="plain" round>{{ $t('ai.drawer.recentMetric', { value: runtimeSummary.recentMessageCount }) }}</ElTag> |
| | | <ElTag :type="runtimeSummary.hasSummary ? 'success' : 'info'" effect="light" round> |
| | | {{ $t(runtimeSummary.hasSummary ? 'ai.drawer.hasSummary' : 'ai.drawer.noSummary') }} |
| | | </ElTag> |
| | | <ElTag :type="runtimeSummary.hasFacts ? 'success' : 'info'" effect="light" round> |
| | | {{ $t(runtimeSummary.hasFacts ? 'ai.drawer.hasFacts' : 'ai.drawer.noFacts') }} |
| | | </ElTag> |
| | | </div> |
| | | |
| | | <ElAlert |
| | | v-if="runtime?.memorySummary" |
| | | type="info" |
| | | :closable="false" |
| | | :title="runtime.memorySummary" |
| | | /> |
| | | <ElAlert |
| | | v-if="runtime?.memoryFacts" |
| | | type="success" |
| | | :closable="false" |
| | | :title="runtime.memoryFacts" |
| | | /> |
| | | </div> |
| | | </ElCollapseTransition> |
| | | </div> |
| | | |
| | | <section class="box-border flex min-h-0 flex-1 flex-col gap-3 p-3 pl-0"> |
| | | <ElAlert |
| | | v-if="drawerError" |
| | | type="warning" |
| | |
| | | </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 gap-3 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> |
| | | |
| | | <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)]"> |
| | |
| | | const sessionKeyword = ref('') |
| | | const loadingRuntime = ref(false) |
| | | const streaming = ref(false) |
| | | const runtimePanelExpanded = ref(false) |
| | | const runtimePreviewCollapsed = ref(true) |
| | | const tracePanelExpanded = ref(false) |
| | | const messagesBottomRef = ref(null) |
| | | const renameDialog = reactive({ |
| | |
| | | |
| | | watch(isDrawerVisible, async (visible) => { |
| | | if (visible) { |
| | | runtimePanelExpanded.value = false |
| | | runtimePreviewCollapsed.value = true |
| | | tracePanelExpanded.value = false |
| | | await initializeDrawer() |
| | | scrollMessagesToBottom() |
| | |
| | | } |
| | | |
| | | .ai-chat-sidebar { |
| | | width: 320px; |
| | | width: 248px; |
| | | } |
| | | |
| | | .ai-chat-workspace { |
| | | display: flex; |
| | | gap: 16px; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .ai-chat-trace-column { |
| | | width: 360px; |
| | | width: 312px; |
| | | flex-shrink: 0; |
| | | transition: |
| | | width 0.2s ease, |
| | |
| | | } |
| | | |
| | | .ai-chat-trace-column--collapsed { |
| | | width: 88px; |
| | | width: 72px; |
| | | } |
| | | |
| | | .ai-chat-trace-collapsed-label { |