Files
agent-frontend-web/docs/superpowers/specs/2026-08-08-frontend-header-restructure-design.md
T

10 KiB
Raw Blame History

Frontend 顶部重排 + 搜索高亮 + 分组默认展开 Design Spec

日期: 2026-08-08 状态: Approved 范围: UI 调整(仅本地代码,不做 git 推送) 前置: 已完成 docs/superpowers/specs/2026-08-08-frontend-dark-theme-sidebar-search-chat-design.md 的深色主题 + 会话搜索/分组 + 聊天气泡节奏改动。本 spec 在此基础上扩展。

背景

根目录 chat.png 中聊天区顶部为:[agent avatar] [agent name] …… [icon: clear] [model select] [icon: collapse]

当前 ChatHeader.vue 顶部右侧有四个按钮:清空会话 / 选择模型 / 智能体 / 文件。"智能体" / "文件" 是互斥 tab,会同时影响 filePanelVisiblepreviewVisible。本次去掉两个 tab,换成一个独立的"折叠文件面板"icon,让文件面板默认展开,由 icon 控制显隐。

同时修正:

  • 搜索框聚焦时仅边框变 accent、placeholder 略亮(已配置样式但未生效);
  • 会话分组默认全部展开(当前默认仅当前会话所在分组展开)。

目标 / 非目标

目标

  • ChatHeader 顶部右侧:[icon: clear] + [el-select: model] + [icon: sidebar-fold/unfold]
  • 标题前加 agentName 首字圆形 icon(accent 背景,白色字);
  • 文件面板默认展开,点击 sidebar-fold icon 折叠,再点 sidebar-unfold icon 展开;
  • 搜索框聚焦高亮边框与 placeholder
  • 会话分组默认全部展开(today / yesterday / earlier);
  • 移除 LlmModelModal 在 ChatHeader 中的引用(文件保留);
  • 移除 handleTabChange 中"agent"分支与 setActiveTab 调用点。

非目标

  • 不改 WebSocket、流式消息、自动下拉、InputAreaUserArea、右键菜单、ChatWindow、浅色主题、theme.scss
  • 不持久化文件面板折叠状态、模型选择状态;
  • 不动后端 listLlmModelsgetLlmAngent 等接口;
  • 不在本次引入 useFilePanelStore / useModelStore(保持父组件持有状态)。

改动清单

1. src/views/frontend/components/ChatHeader.vue

模板变更

<template>
  <div class="chat-header">
    <div class="header-left">
      <div class="agent-avatar">{{ agentNameInitial }}</div>
      <span class="conversation-name">{{ agentName }}</span>
    </div>
    <div class="header-right">
      <el-tooltip content="清空会话" placement="bottom" :show-after="300">
        <el-button class="icon-btn" link @click="emit('clear')">
          <el-icon><Delete /></el-icon>
        </el-button>
      </el-tooltip>

      <el-select
        class="model-select"
        :model-value="modelValue"
        @update:model-value="handleSelectChange"
        size="default"
        placeholder="选择模型"
      >
        <el-option
          v-for="opt in modelOptions"
          :key="opt.value"
          :label="opt.label"
          :value="opt.value"
        />
      </el-select>

      <el-tooltip :content="filePanelVisible ? '收起文件面板' : '展开文件面板'" placement="bottom" :show-after="300">
        <el-button class="icon-btn" link @click="handleToggleClick">
          <el-icon><component :is="filePanelVisible ? Fold : Expand" /></el-icon>
        </el-button>
      </el-tooltip>
    </div>
  </div>
</template>

注:Element Plus 图标库中"面板折叠 / 展开"对应的图标名为 Fold / Expand(无 SidebarFold / SidebarUnfold 同名图标)。语义与"收起 / 展开文件面板"完全对应。


#### 脚本变更

- `import { Delete, Fold, Expand } from '@element-plus/icons-vue'`
- 删除 `import LlmModelModal from './LlmModelModal.vue'`、`activeTab` ref、`showModelModal` ref、`handleTabClick` 函数、`setActiveTab` expose 方法;
- 删除 `emit('tab-change', tab)`
- 新增 props`modelOptions: Array<{label, value}>`、`modelValue: String`、`filePanelVisible: Boolean`
- 新增 emits`update:modelValue`、`toggle-file-panel`
- 新增 computed `agentNameInitial`:取 `agentName` 的第一个字符(中文取首字、英文取首字母),为空时返回 `?`;
- 新增 `handleSelectChange(value)`emit `update:modelValue`
- 新增 `handleToggleClick()`emit `toggle-file-panel`
- 删除 `handleModelSelect(model)` 与 `model-change` emit——切换模型统一通过 `update:modelValue` 单事件流,避免重复。

#### 样式新增

```scss
.agent-avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--fe-accent);
  color: var(--fe-text-inverse);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  font-weight: 600;
  flex-shrink: 0;
}

.header-left {
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: 480px;
}

.icon-btn {
  padding: 4px 8px;
  background: transparent;
  border: none;
  color: var(--fe-text-secondary);
  height: 32px;
  width: 32px;
  cursor: pointer;
  border-radius: var(--fe-radius-md);
  &:hover {
    background: var(--fe-bg-hover);
    color: var(--fe-text-primary);
  }
  .el-icon { font-size: 18px; }
}

.model-select {
  width: 180px;
  :deep(.el-select__wrapper) {
    background: var(--fe-bg-elevated);
    box-shadow: 0 0 0 1px var(--fe-border) inset;
    border-radius: var(--fe-radius-md);
  }
  :deep(.el-select__placeholder) { color: var(--fe-text-muted); }
  :deep(.el-select__selected-item) { color: var(--fe-text-primary); }
}

2. src/views/frontend/components/ConversationSidebar.vue

搜索框聚焦高亮

在已有 .sidebar-search 块内追加:

.sidebar-search {
  :deep(.el-input__wrapper.is-focus) {
    box-shadow: 0 0 0 1px var(--fe-accent) inset;
    background: var(--fe-bg-elevated);
  }
  :deep(.el-input__wrapper.is-focus .el-input__inner::placeholder) {
    color: var(--fe-text-secondary);
  }
}

分组默认全部展开

修改 syncExpandedWithCurrent

function syncExpandedWithCurrent() {
  // 用户要求默认全部展开;当前会话所在分组由图标高亮区分,无需特殊收/展
  expandedGroups.value = new Set(['today', 'yesterday', 'earlier'])
}

toggleGroup 行为保持不变——用户可手动收/展某个分组,刷新或切换会话后会重置为全部展开。

3. src/views/frontend/index.vue

状态修改

const filePanelVisible = ref(true)   // 原为 false

删除 / 新增函数

  • 整体删除 handleTabChange 函数体(index.vue 模板上 @tab-change="..." 也删除);
  • 删除 handleFilePanelClose 中对 chatHeaderRef.value?.setActiveTab('agent') 的调用(仅保留函数体中 filePanelVisible.value = false; previewVisible.value = false);
  • 删除模板上 @model-change="handleModelChange"@clear 保留;改用 @update:model-value="handleModelSelectChange" 单一通道;
  • 新增 handleToggleFilePanel()
function handleToggleFilePanel() {
  filePanelVisible.value = !filePanelVisible.value
}

模板调整

<ChatHeader> 改为:

<ChatHeader
  ref="chatHeaderRef"
  :agent-name="currentConversation?.agentName || '请选择会话'"
  :current-model="currentModel"
  :model-options="modelSelectOptions"
  :model-value="currentModel?.modelName || currentModel?.name || ''"
  :file-panel-visible="filePanelVisible"
  @update:model-value="handleModelSelectChange"
  @toggle-file-panel="handleToggleFilePanel"
  @clear="handleClear"
/>

脚本新增

const modelSelectOptions = computed(() => modelList.value.map(m => ({
  label: m.name || m.modelName || '',
  value: m.modelName || m.name || ''
})))

function handleModelSelectChange(value) {
  const found = modelList.value.find(m => (m.modelName || m.name) === value)
  if (found) {
    currentModel.value = found
    ElMessage.success(`已选择模型: ${found.name || found.modelName}`)
  }
}

4. src/views/frontend/components/LlmModelModal.vue

不修改,保留文件以备后续扩展(不再从 ChatHeader 引用)。

数据流

  1. index.vue 持有 currentModel / modelList / filePanelVisible
  2. modelSelectOptionsmodel-value 传给 ChatHeader
  3. ChatHeader el-select change → emit update:modelValue(value) → 父按 value 在 modelList 中查找匹配项 → 写 currentModel → emit model-change
  4. ChatHeader 点击折叠 icon → emit toggle-file-panel → 父翻转 filePanelVisible
  5. 父根据 filePanelVisible 渲染 <FilePanel>

异常处理

  • modelList 为空:el-select 下拉空选项,不报错;
  • currentModel 找不到匹配项:handleModelSelectChange 静默不更新,保留旧值;
  • agentName 为空:agent-avatar 显示 ?
  • 文件面板折叠 / 展开不持久化,刷新页面后默认展开;
  • LlmModelModal.vue 文件保留但失去引用入口——下一轮如需"更多模型"再加回;本轮不删文件。

验证

步骤 期望
1 启动 dev,深色主题下顶部 ChatHeader:左侧 agent-avatar 圆形(accent 背景)+ agent 名称;右侧依次:垃圾桶 icon、模型下拉、文件面板折叠 icon
2 切换会话,agent-avatar 文字随 agentName 首字变化
3 文件面板默认展开;点击 sidebar-fold icon → 文件面板收起,icon 变为 sidebar-unfold;再点 → 展开
4 切换模型下拉 → 立即生效;ChatHeader 模型 select 与后端 listLlmModels 一致
5 点击垃圾桶 icon → 触发 handleClearWebSocket 发送 clear 类型消息,会话消息清空
6 搜索框聚焦:边框变为 accent 色,placeholder 颜色变浅;失焦恢复
7 会话侧栏分组今天 / 昨天 / 更早 默认全部展开;点击任一分组标题可手动折叠
8 浅色主题下所有上述功能保持一致(继承 --fe-*
9 WebSocket 流式消息、自动下拉、Token 轮询等不回归
10 不做 git 推送;本地变更未提交

风险

  • 删除 handleTabChange 时要确认 index.vue 模板上无 @tab-change="..." 残留;
  • 删除 setActiveTab expose 方法时确认 index.vue 内无 chatHeaderRef.value?.setActiveTab(...) 调用(handleFilePanelClose 中有,需要一并删除);
  • 移除 LlmModelModal 引用可能使该文件成为孤儿;保留即可,无外部影响;
  • el-selectel-button 共存于 .header-rightgap: 8px 样式需保持视觉留白与上一轮 8px 一致。