优化: 脉络卡归属置顶与源会话跳转
- 当前会话归属项目组置顶,focused 标记高亮 - source_ref 解析出可跳转会话 id,消息级引用反查补全 - 条目相对时间/空态引导/标题点击跳源,失败静默
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<!-- 脉络卡:同项目近期决策摘要,纯读侧。用户点「带入上下文」向输入框投递追加文本,
|
||||
由用户编辑后自行发送——注入决策权在用户,不触碰 L3 召回铁律。 -->
|
||||
由用户编辑后自行发送——注入决策权在用户,不触碰 L3 召回铁律。
|
||||
有源会话的条目标题可点跳回原文,空源或跳转失败静默不打断阅读。 -->
|
||||
<div class="ctx-briefs">
|
||||
<button
|
||||
class="ctx-briefs-toggle"
|
||||
@@ -19,9 +20,19 @@
|
||||
<div v-if="loading" class="ctx-briefs-empty">{{ t('aiChat.contextBriefsLoading') }}</div>
|
||||
<div v-else-if="groups.length === 0" class="ctx-briefs-empty">{{ t('aiChat.contextBriefsEmpty') }}</div>
|
||||
<div v-for="g in groups" v-else :key="g.source_project" class="ctx-briefs-group">
|
||||
<div class="ctx-briefs-project">{{ g.source_project }}</div>
|
||||
<div class="ctx-briefs-project">
|
||||
<span v-if="g.focused" class="ctx-briefs-focus-dot" />{{ g.source_project }}
|
||||
</div>
|
||||
<div v-for="b in g.briefs" :key="g.source_project + b.title" class="ctx-briefs-item">
|
||||
<div class="ctx-briefs-item-title">{{ b.title }}</div>
|
||||
<div class="ctx-briefs-item-head">
|
||||
<button
|
||||
v-if="b.source_conv"
|
||||
class="ctx-briefs-item-title ctx-briefs-item-link"
|
||||
@click="openSource(b)"
|
||||
>{{ b.title }}</button>
|
||||
<span v-else class="ctx-briefs-item-title">{{ b.title }}</span>
|
||||
<span class="ctx-briefs-item-time">{{ formatRelative(b.updated_at) }}</span>
|
||||
</div>
|
||||
<div class="ctx-briefs-item-summary">{{ b.summary }}</div>
|
||||
<button
|
||||
class="ctx-briefs-bring"
|
||||
@@ -35,26 +46,32 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { aiApi } from '../../api/ai'
|
||||
import { useAiStore } from '../../stores/ai'
|
||||
import { pushConvDraftAppend } from '../../composables/ai/aiShared'
|
||||
import { useAiConversations } from '../../composables/ai/useAiConversations'
|
||||
import { formatRelative } from '../../utils/time'
|
||||
|
||||
interface ContextBrief {
|
||||
title: string
|
||||
summary: string
|
||||
source_ref: string | null
|
||||
source_conv: string | null
|
||||
source_project: string | null
|
||||
reuse_count: number
|
||||
updated_at: string
|
||||
}
|
||||
interface ProjectBriefs {
|
||||
source_project: string
|
||||
focused?: boolean
|
||||
briefs: ContextBrief[]
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const store = useAiStore()
|
||||
const { openConversation } = useAiConversations()
|
||||
|
||||
const expanded = ref(false)
|
||||
const loading = ref(false)
|
||||
@@ -62,17 +79,39 @@ const groups = ref<ProjectBriefs[]>([])
|
||||
|
||||
const totalBriefs = computed(() => groups.value.reduce((n, g) => n + g.briefs.length, 0))
|
||||
|
||||
/** 拉取脉络,按当前会话标注聚焦分组;会话已切走时丢弃过期响应(防旧 focused 污染新列表) */
|
||||
async function load() {
|
||||
const convId = store.state.activeConversationId ?? undefined
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await aiApi.listContextBriefs(undefined, convId)
|
||||
if ((store.state.activeConversationId ?? undefined) === convId) groups.value = result
|
||||
} catch {
|
||||
if ((store.state.activeConversationId ?? undefined) === convId) groups.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggle() {
|
||||
expanded.value = !expanded.value
|
||||
if (expanded.value && groups.value.length === 0) {
|
||||
loading.value = true
|
||||
try {
|
||||
groups.value = await aiApi.listContextBriefs()
|
||||
} catch {
|
||||
groups.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
if (expanded.value && groups.value.length === 0) await load()
|
||||
}
|
||||
|
||||
/** 切会话重新拉取(focused 跟随当前会话);收起态只清缓存,保持按需加载 */
|
||||
watch(() => store.state.activeConversationId, () => {
|
||||
groups.value = []
|
||||
if (expanded.value) void load()
|
||||
})
|
||||
|
||||
/** 跳回摘要来源会话(source_conv 由后端自 source_ref 解析,消息级引用已反查);
|
||||
* 源为空或已删除(not_found)一律静默,不加 toast */
|
||||
async function openSource(b: ContextBrief) {
|
||||
if (!b.source_conv) return
|
||||
try {
|
||||
await openConversation(b.source_conv)
|
||||
} catch {
|
||||
// 静默:跳转失败不打断阅读
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,14 +163,45 @@ function bringIn(g: ProjectBriefs, b: ContextBrief) {
|
||||
color: var(--text-primary, #333);
|
||||
padding: 5px 2px 2px;
|
||||
}
|
||||
.ctx-briefs-focus-dot {
|
||||
display: inline-block;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
margin-right: 4px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent, #4080ff);
|
||||
vertical-align: 2px;
|
||||
}
|
||||
.ctx-briefs-item {
|
||||
padding: 4px 2px;
|
||||
border-top: 1px dashed var(--border, #eee);
|
||||
}
|
||||
.ctx-briefs-item-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
}
|
||||
.ctx-briefs-item-title {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary, #333);
|
||||
}
|
||||
.ctx-briefs-item-link {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ctx-briefs-item-link:hover {
|
||||
color: var(--accent, #4080ff);
|
||||
}
|
||||
.ctx-briefs-item-time {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary, #999);
|
||||
}
|
||||
.ctx-briefs-item-summary {
|
||||
color: var(--text-secondary, #666);
|
||||
margin: 2px 0;
|
||||
|
||||
Reference in New Issue
Block a user