Procházet zdrojové kódy

WIP: 为会话浮动菜单补充置顶操作

[gstack-context]
Decisions: 菜单首项动态显示置顶或取消置顶;操作后重新读取服务器会话顺序
Remaining: 全量测试、重载 8800、真实页面检查
Skill: /frontend-skill
[/gstack-context]
samien před 2 měsíci
rodič
revize
740364f903

+ 9 - 0
src/client/views/ChatView.vue

@@ -59,6 +59,7 @@ const messages = computed(() => chatMessagesToUi(chat.messages, profile => profi
 const conversationMediaItems = computed(() => mediaItemsFromMessages(messages.value))
 const lightboxMedia = computed(() => conversationMediaItems.value.map(item => ({ url: item.previewUrl || item.downloadUrl || '', name: item.name, type: item.kind as 'image' | 'video' })).filter(item => item.url))
 const activeSession = computed(() => chat.activeSession)
+const actionSession = computed(() => chat.sessions.find(session => session.id === actionSessionId.value))
 const connected = computed(() => ['connected', 'ready'].includes(chat.connectionState))
 const interaction = computed(() => chatInteraction(chat.pendingApproval, chat.pendingClarification))
 const reference = computed<ComposerReference | null>(() => quoted.value ? { id: quoted.value.id, content: quoted.value.content, author: quoted.value.author } : null)
@@ -233,6 +234,13 @@ async function saveRename() {
   closeSessionActions()
 }
 
+async function toggleSessionPinned() {
+  const session = actionSession.value
+  if (!session) return
+  await chat.setSessionPinned(session.id, !session.pinned, session.profile)
+  closeSessionActions()
+}
+
 async function deleteSession() {
   const session = chat.sessions.find(item => item.id === actionSessionId.value)
   if (!session) return
@@ -403,6 +411,7 @@ watch(() => chat.activeSessionId, async id => {
           <div><button class="quiet-button" type="button" @click="renaming = false">取消</button><button class="solid-button" type="button" @click="saveRename">保存</button></div>
         </template>
         <template v-else>
+          <button class="action-row" role="menuitem" type="button" @click="toggleSessionPinned"><AppIcon name="pin" :size="14" />{{ actionSession?.pinned ? '取消置顶' : '置顶会话' }}</button>
           <button class="action-row" role="menuitem" type="button" @click="renaming = true"><AppIcon name="edit" :size="14" />重命名</button>
           <button class="action-row danger" role="menuitem" type="button" @click="deleteSession"><AppIcon name="trash" :size="14" />删除会话</button>
         </template>

+ 7 - 2
tests/e2e/workspace.spec.ts

@@ -192,14 +192,19 @@ test('keeps the canonical logo and yaoyao-webui composer geometry', async ({ pag
   await expect(page.getByRole('button', { name: '会话操作' })).toBeVisible()
   await page.getByRole('button', { name: '会话操作' }).click()
   const sessionMenu = page.getByRole('menu', { name: '会话操作' })
+  await expect(sessionMenu.getByText('取消置顶', { exact: true })).toBeVisible()
   await expect(sessionMenu.getByText('重命名', { exact: true })).toBeVisible()
   await expect(sessionMenu.getByText('删除会话', { exact: true })).toBeVisible()
   await expect(page.getByText('置顶会话', { exact: true })).toHaveCount(0)
   const menuBox = await sessionMenu.evaluate(element => element.getBoundingClientRect().toJSON())
   expect(menuBox.width).toBeLessThanOrEqual(220)
-  expect(menuBox.height).toBeLessThan(100)
+  expect(menuBox.height).toBeLessThan(130)
   await expect(page.locator('.session-action-layer')).toHaveCount(0)
-  await page.keyboard.press('Escape')
+  await sessionMenu.getByText('取消置顶', { exact: true }).click()
+  await expect(page.locator('.desktop-sidebar').getByText('已置顶 1', { exact: true })).toHaveCount(0)
+  await page.getByRole('button', { name: '会话操作' }).click()
+  await sessionMenu.getByText('置顶会话', { exact: true }).click()
+  await expect(page.locator('.desktop-sidebar').getByText('已置顶 1', { exact: true })).toBeVisible()
 
   const compactSession = page.locator('.desktop-sidebar .sidebar-item--single-line').first()
   const compactMetrics = await compactSession.evaluate(element => {

+ 6 - 0
tests/fixtures/fake-hermes.mjs

@@ -93,6 +93,12 @@ const server = createServer(async (request, response) => {
   if (/^\/api\/sessions\/[^/]+$/.test(url.pathname)) {
     const id = decodeURIComponent(url.pathname.split('/').at(-1))
     const session = sessions.find(item => item.id === id && (!url.searchParams.get('profile') || item.profile === url.searchParams.get('profile')))
+    if (session && request.method === 'PATCH') {
+      const input = await body(request)
+      if (typeof input.pinned === 'boolean') session.pinned = input.pinned
+      if (typeof input.title === 'string' && input.title.trim()) session.title = input.title.trim()
+      return json(response, 200, { ok: true, pinned: session.pinned, title: session.title })
+    }
     return session ? json(response, 200, session) : json(response, 404, { detail: 'Not found' })
   }
   if (url.pathname === '/api/session-unread') return json(response, 200, { items: { 'session-demo': 0 } })