#
zhou zhou
2026-04-02 4d6b02dada557b4186cdcef843cd3859aeeaac01
rsf-design/src/components/core/layouts/art-chat-window/index.vue
@@ -7,28 +7,163 @@
    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>
@@ -104,102 +239,7 @@
          </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"
@@ -304,26 +344,8 @@
              </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)]">
@@ -489,7 +511,7 @@
  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({
@@ -583,7 +605,7 @@
  watch(isDrawerVisible, async (visible) => {
    if (visible) {
      runtimePanelExpanded.value = false
      runtimePreviewCollapsed.value = true
      tracePanelExpanded.value = false
      await initializeDrawer()
      scrollMessagesToBottom()
@@ -1050,16 +1072,16 @@
  }
  .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,
@@ -1067,7 +1089,7 @@
  }
  .ai-chat-trace-column--collapsed {
    width: 88px;
    width: 72px;
  }
  .ai-chat-trace-collapsed-label {