重构: TopBar 布局 — header 中间区 + 底部工具区(标题/目标/历史/摘要)
This commit is contained in:
@@ -20,7 +20,21 @@
|
||||
⏳ {{ $t('aiChat.pendingApprovalCount', { n: pendingApprovalCount }) }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 模型选择器(对话透明化 L1:折叠为紧凑下拉) -->
|
||||
<!-- 提供商状态 + 模型选择器 -->
|
||||
<div class="ai-header-center">
|
||||
<div
|
||||
class="ai-provider-bar"
|
||||
:class="{ 'ai-provider-bar--switchable': store.state.providers.length > 1 }"
|
||||
v-if="store.state.providers.length > 0"
|
||||
:title="store.state.providers.length > 1 ? $t('aiChat.clickToSwitchProvider') : ''"
|
||||
@click="cycleProvider"
|
||||
>
|
||||
<span class="provider-dot"></span>
|
||||
<span class="provider-name">{{ activeProviderName }}</span>
|
||||
</div>
|
||||
<div class="ai-provider-bar ai-provider-empty" v-else>
|
||||
<span class="provider-hint">{{ $t('aiChat.providerNotConfigured') }}</span>
|
||||
</div>
|
||||
<div class="ai-model-picker" :title="$t('aiChat.specifyModeHint') || ''">
|
||||
<select
|
||||
v-if="enabledModels.length"
|
||||
@@ -34,6 +48,7 @@
|
||||
</select>
|
||||
<span v-else class="ai-model-empty">{{ $t('aiChat.noModels') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ai-header-actions">
|
||||
<button class="ai-btn-icon" @click="emit('new-conversation')" :title="$t('aiChat.newConversation')">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
|
||||
@@ -92,33 +107,48 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下半栏:provider + goals 并排 -->
|
||||
<div class="ai-bottom-bar">
|
||||
<!-- 提供商状态 -->
|
||||
<div
|
||||
class="ai-provider-bar"
|
||||
:class="{ 'ai-provider-bar--switchable': store.state.providers.length > 1 }"
|
||||
v-if="store.state.providers.length > 0"
|
||||
:title="store.state.providers.length > 1 ? $t('aiChat.clickToSwitchProvider') : ''"
|
||||
@click="cycleProvider"
|
||||
>
|
||||
<span class="provider-dot"></span>
|
||||
<span class="provider-name">{{ activeProviderName }}</span>
|
||||
</div>
|
||||
<div class="ai-provider-bar ai-provider-empty" v-else>
|
||||
<span class="provider-hint">{{ $t('aiChat.providerNotConfigured') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 目标入口(扁平化,不抢眼) -->
|
||||
<!-- 底部工具区:标题 + 目标 + 历史消息 + 上下文摘要 -->
|
||||
<div class="ai-bottom-tools" ref="goalsRef">
|
||||
<span v-if="currentConvTitle" class="ai-bottom-title">{{ currentConvTitle }}</span>
|
||||
<div class="ai-bottom-tools-right">
|
||||
<!-- 目标计数 -->
|
||||
<div v-if="goals.length" class="ai-goals-inline">
|
||||
<span class="ai-goals-inline-badge" @click="goalsExpanded = !goalsExpanded">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
|
||||
<span class="ai-goals-inline-badge ai-tool-badge" @click="goalsExpanded = !goalsExpanded" title="查看目标">
|
||||
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
|
||||
{{ goals.length }}
|
||||
</span>
|
||||
<div v-if="goalsExpanded" class="ai-goals-inline-list">
|
||||
<div v-for="(g, i) in goals" :key="i" class="ai-goal-inline-item">
|
||||
<span>{{ g }}</span>
|
||||
<button class="ai-goal-remove" @click="removeGoal(i)">×</button>
|
||||
<span class="ai-goal-text">{{ g }}</span>
|
||||
<button class="ai-goal-remove" @click.stop="removeGoal(i)">×</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 历史消息 -->
|
||||
<div v-if="historyMsgs.length" class="ai-history-inline">
|
||||
<span class="ai-history-inline-badge ai-tool-badge" @click="historyExpanded = !historyExpanded" title="查看历史消息">
|
||||
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
{{ historyMsgs.length }}
|
||||
</span>
|
||||
<div v-if="historyExpanded" class="ai-history-inline-list">
|
||||
<div v-for="h in historyMsgs" :key="h.id" class="ai-history-inline-item" @click="scrollToMessage(h.id)">
|
||||
<span class="ai-history-index">{{ h.num }}</span>
|
||||
<span class="ai-history-text">{{ h.text }}</span>
|
||||
<span class="ai-history-time">{{ h.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 上下文摘要 -->
|
||||
<div v-if="summaryMsgs.length" class="ai-summary-inline">
|
||||
<span class="ai-summary-inline-badge ai-tool-badge" @click="summaryExpanded = !summaryExpanded" title="查看上下文摘要">
|
||||
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h16M4 18h12"/></svg>
|
||||
{{ summaryMsgs.length }}
|
||||
</span>
|
||||
<div v-if="summaryExpanded" class="ai-summary-inline-list">
|
||||
<div v-for="(s, i) in summaryMsgs" :key="i" class="ai-summary-inline-item" @click="scrollToMessage(s.id)">
|
||||
<span class="ai-summary-label">{{ $t('aiChat.compressedSummaryLabel') || '上下文摘要' }} #{{ summaryMsgs.length - i }}</span>
|
||||
<p class="ai-summary-preview">{{ s.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,11 +167,11 @@
|
||||
// 第三批抽离: 顶部工具栏 TopBar(从 AiChat.vue 行 12-145 template + 行 741-902 script 迁移)
|
||||
// 零行为变更。provider/model 选择逻辑直接读 store(单例共享),动作按钮 emit 回父,
|
||||
// 父持有 clear/compress/confirm 逻辑 + 单一 toast 源(provider-switched 回父弹 toast)。
|
||||
import { ref, computed, watch, onBeforeUnmount } from 'vue'
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAiStore } from '../../stores/ai'
|
||||
import { aiApi } from '../../api/ai'
|
||||
import type { ModelConfig } from '../../api/types'
|
||||
import type { AiMessage, ModelConfig } from '../../api/types'
|
||||
|
||||
defineProps<{
|
||||
/** 分离窗口模式(嵌入 vs 分离决定顶部按钮组) */
|
||||
@@ -222,21 +252,114 @@ watch(() => store.state.activeConversationId, () => {
|
||||
store.modelOverride.value = enabledModels.value[0]?.model_id || null
|
||||
})
|
||||
|
||||
// ── 当前对话标题 ──
|
||||
const currentConvTitle = computed(() => {
|
||||
const id = store.state.activeConversationId
|
||||
if (!id) return ''
|
||||
const conv = store.state.conversations.find(c => c.id === id)
|
||||
return conv?.title || ''
|
||||
})
|
||||
|
||||
// ── 🎯 对话目标面板(对话透明化 L1:Goal visibility) ──
|
||||
// 从最新 switch 返回的 conversation summary 中取 pinned_goals,
|
||||
// 增删操作经后端 IPC 持久化到 DB 并同步 per_conv 内存。
|
||||
const goals = ref<string[]>([])
|
||||
/** 目标列表展开/折叠 */
|
||||
const goalsExpanded = ref(false)
|
||||
|
||||
/** 监听 activeConversationId 变化,加载目标的精简实现:从 store.conversations 查找当前会话的目标。 */
|
||||
// ── 📋 历史消息(当前会话 user 消息列表) ──
|
||||
interface HistoryItem { id: string; text: string; time: string; num: number }
|
||||
/** 时间显示:今日 HH:mm / 昨天 HH:mm / MM-DD HH:mm */
|
||||
function fmtTime(ts: number): string {
|
||||
const d = new Date(ts)
|
||||
const now = new Date()
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
const hm = `${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
// 同一天 → 只显示时间
|
||||
if (d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate()) {
|
||||
return hm
|
||||
}
|
||||
// 昨天
|
||||
const yest = new Date(now)
|
||||
yest.setDate(yest.getDate() - 1)
|
||||
if (d.getFullYear() === yest.getFullYear() && d.getMonth() === yest.getMonth() && d.getDate() === yest.getDate()) {
|
||||
return `昨天 ${hm}`
|
||||
}
|
||||
// 更早
|
||||
return `${pad(d.getMonth()+1)}-${pad(d.getDate())} ${hm}`
|
||||
}
|
||||
const MAX_HISTORY = 20
|
||||
const MAX_CHARS = 80
|
||||
const historyMsgs = computed<HistoryItem[]>(() => {
|
||||
const allUserMsgs = store.state.messages
|
||||
.filter((m): m is AiMessage & { timestamp: number } => m.role === 'user' && !!m.content && !!m.timestamp)
|
||||
.map((m, idx) => ({
|
||||
id: m.id,
|
||||
text: m.content.length > MAX_CHARS ? m.content.slice(0, MAX_CHARS) + '…' : m.content,
|
||||
time: fmtTime(m.timestamp),
|
||||
num: idx + 1,
|
||||
}))
|
||||
return allUserMsgs.slice(-MAX_HISTORY).reverse()
|
||||
})
|
||||
const historyExpanded = ref(false)
|
||||
|
||||
/** 点击历史消息项 → 滚动定位到对应的消息气泡 */
|
||||
function scrollToMessage(msgId: string) {
|
||||
historyExpanded.value = false
|
||||
// 消息列表已有 data-msg-id 属性,直接 DOM 查询定位
|
||||
const el = document.querySelector(`[data-msg-id="${msgId}"]`)
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}
|
||||
}
|
||||
|
||||
/** 目标/历史下拉容器 DOM 引用(用于点击外部检测) */
|
||||
const goalsRef = ref<HTMLElement | null>(null)
|
||||
|
||||
function onBottomToolsClickOutside(e: MouseEvent) {
|
||||
if (!goalsRef.value) return
|
||||
if (goalsExpanded.value && !goalsRef.value.contains(e.target as Node)) {
|
||||
goalsExpanded.value = false
|
||||
}
|
||||
if (historyExpanded.value && !goalsRef.value.contains(e.target as Node)) {
|
||||
historyExpanded.value = false
|
||||
}
|
||||
if (summaryExpanded.value && !goalsRef.value.contains(e.target as Node)) {
|
||||
summaryExpanded.value = false
|
||||
}
|
||||
}
|
||||
// ── 📄 上下文摘要(压缩/归档的系统消息) ──
|
||||
interface SummaryItem { id: string; text: string }
|
||||
const summaryMsgs = computed<SummaryItem[]>(() => {
|
||||
return store.state.messages
|
||||
.filter(m => m.role === 'system' && !!m.content)
|
||||
.map(m => ({
|
||||
id: m.id,
|
||||
text: m.content.length > 120 ? m.content.slice(0, 120) + '…' : m.content,
|
||||
}))
|
||||
})
|
||||
const summaryExpanded = ref(false)
|
||||
|
||||
/** 按 Escape 关闭所有下拉 */
|
||||
function onBottomToolsKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
goalsExpanded.value = false
|
||||
historyExpanded.value = false
|
||||
summaryExpanded.value = false
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', onBottomToolsClickOutside)
|
||||
document.addEventListener('keydown', onBottomToolsKeydown)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', onBottomToolsClickOutside)
|
||||
document.removeEventListener('keydown', onBottomToolsKeydown)
|
||||
})
|
||||
|
||||
/** 监听 activeConversationId 变化,加载目标 */
|
||||
watch(() => store.state.activeConversationId, (convId) => {
|
||||
loadGoals(convId)
|
||||
}, { immediate: true })
|
||||
/** 消息变化后也刷新目标(发消息→save_conversation→DB→store 需重读) */
|
||||
/** 消息变化后也刷新目标 */
|
||||
watch(() => store.state.messages.length, () => {
|
||||
// loadConversations 异步,等 store 刷新后重读
|
||||
setTimeout(() => loadGoals(store.state.activeConversationId), 100)
|
||||
})
|
||||
|
||||
@@ -246,13 +369,14 @@ function loadGoals(convId: string | null) {
|
||||
goals.value = conv?.pinned_goals ?? []
|
||||
}
|
||||
|
||||
/** 移除第 i 个目标:删除后持久化全量列表到后端。 */
|
||||
/** 移除第 i 个目标(移除最后一个自动收起) */
|
||||
async function removeGoal(i: number) {
|
||||
const convId = store.state.activeConversationId
|
||||
if (!convId) return
|
||||
const next = [...goals.value]
|
||||
next.splice(i, 1)
|
||||
goals.value = next
|
||||
if (next.length === 0) goalsExpanded.value = false
|
||||
await aiApi.updateConversationGoals(convId, next)
|
||||
}
|
||||
|
||||
@@ -317,6 +441,10 @@ onBeforeUnmount(() => {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--df-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 200px;
|
||||
}
|
||||
/* AE-2025-02:审批徽标(待审批>0 时显于头部,点击跳首个审批卡) */
|
||||
.ai-approval-badge {
|
||||
@@ -340,9 +468,6 @@ onBeforeUnmount(() => {
|
||||
.ai-model-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
margin-right: 4px;
|
||||
padding-left: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.ai-model-select {
|
||||
@@ -360,8 +485,6 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
.ai-model-select--compact {
|
||||
max-width: 130px;
|
||||
margin-left: auto;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.ai-model-select:disabled {
|
||||
opacity: 0.6;
|
||||
@@ -411,76 +534,27 @@ onBeforeUnmount(() => {
|
||||
.ai-btn-spinner {
|
||||
animation: ai-btn-spin 0.9s linear infinite;
|
||||
}
|
||||
|
||||
/* ═══ 🎯 对话目标面板(对话透明化 L1) ═══ */
|
||||
.ai-goals-panel {
|
||||
padding: 6px 14px;
|
||||
border-bottom: 0.5px solid var(--df-border);
|
||||
background: color-mix(in srgb, var(--df-accent) 6%, transparent);
|
||||
}
|
||||
.ai-goals-header {
|
||||
/* 底部工具区(标题左 + 工具右) */
|
||||
.ai-bottom-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
justify-content: space-between;
|
||||
padding: 0 14px 4px;
|
||||
}
|
||||
.ai-goals-title {
|
||||
.ai-bottom-title {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--df-accent);
|
||||
color: var(--df-text-secondary);
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.ai-goals-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.ai-goal-item {
|
||||
.ai-bottom-tools-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 1px 0;
|
||||
}
|
||||
.ai-goal-icon {
|
||||
font-size: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ai-goal-text {
|
||||
font-size: 11px;
|
||||
color: var(--df-text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.ai-goal-remove {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--df-text-dim);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
padding: 0 2px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.ai-goal-item:hover .ai-goal-remove {
|
||||
opacity: 1;
|
||||
}
|
||||
.ai-goal-remove:hover {
|
||||
color: var(--df-danger);
|
||||
}
|
||||
|
||||
/* ═══ Provider Bar + Goals(并排) ═══ */
|
||||
.ai-bottom-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 2px 14px;
|
||||
border-bottom: 0.5px solid var(--df-border);
|
||||
min-height: 26px;
|
||||
}
|
||||
/* ═══ Provider Bar (header 中间区) ═══ */
|
||||
.ai-provider-bar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -510,33 +584,152 @@ onBeforeUnmount(() => {
|
||||
background: var(--df-danger-bg);
|
||||
}
|
||||
|
||||
/* 目标入口(扁平低调) */
|
||||
/* 历史消息(底部) */
|
||||
.ai-history-inline {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
/* 底部工具通用徽标 */
|
||||
.ai-tool-badge {
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--df-text);
|
||||
background: var(--df-bg-card);
|
||||
border: 0.5px solid var(--df-border);
|
||||
transition: color 0.15s, background 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ai-tool-badge:hover {
|
||||
background: var(--df-bg-card-hover);
|
||||
}
|
||||
|
||||
/* 目标计数(底部) */
|
||||
.ai-goals-inline {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.ai-goals-inline-badge {
|
||||
.ai-history-inline-list {
|
||||
position: absolute;
|
||||
left: auto; right: 0;
|
||||
top: 100%;
|
||||
z-index: 100;
|
||||
min-width: 240px;
|
||||
max-width: 400px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: var(--df-bg);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-sm);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
padding: 4px 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.ai-history-inline-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
color: var(--df-text-secondary);
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.ai-history-inline-item:hover {
|
||||
background: var(--df-bg-card);
|
||||
color: var(--df-text);
|
||||
}
|
||||
.ai-history-index {
|
||||
flex-shrink: 0;
|
||||
font-size: 10px;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
color: var(--df-text-dim);
|
||||
min-width: 24px;
|
||||
text-align: right;
|
||||
font-family: var(--df-font-mono);
|
||||
}
|
||||
.ai-history-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.ai-history-time {
|
||||
flex-shrink: 0;
|
||||
font-size: 10px;
|
||||
color: var(--df-text-dim);
|
||||
font-family: var(--df-font-mono);
|
||||
}
|
||||
|
||||
/* ═══ 上下文摘要(底部) ═══ */
|
||||
.ai-summary-inline {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
color: var(--df-text-dim);
|
||||
background: transparent;
|
||||
transition: color 0.15s, background 0.15s;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.ai-goals-inline-badge:hover {
|
||||
opacity: 1;
|
||||
.ai-summary-inline-list {
|
||||
position: absolute;
|
||||
left: auto; right: 0;
|
||||
top: 100%;
|
||||
z-index: 100;
|
||||
min-width: 280px;
|
||||
max-width: 420px;
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
background: var(--df-bg);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-sm);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
padding: 6px 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.ai-summary-inline-item {
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
border-bottom: 0.5px solid var(--df-border);
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.ai-summary-inline-item:last-child { border-bottom: none; }
|
||||
.ai-summary-inline-item:hover {
|
||||
background: var(--df-bg-card);
|
||||
}
|
||||
.ai-summary-label {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--df-accent);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.ai-summary-preview {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
color: var(--df-text-secondary);
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* header 中间区:provider + 模型选择器并排 */
|
||||
.ai-header-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.ai-goals-inline-list {
|
||||
position: absolute;
|
||||
top: 100%; left: 0;
|
||||
left: auto; right: 0;
|
||||
z-index: 100;
|
||||
min-width: 200px;
|
||||
max-width: 360px;
|
||||
@@ -550,13 +743,21 @@ onBeforeUnmount(() => {
|
||||
.ai-goal-inline-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 3px 8px;
|
||||
gap: 4px;
|
||||
padding: 3px 6px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.ai-goal-inline-item:hover {
|
||||
background: var(--df-bg-card, #f5f5f5);
|
||||
background: var(--df-bg-card);
|
||||
}
|
||||
.ai-goal-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ai-goal-remove:hover {
|
||||
color: var(--df-danger, #e5484d);
|
||||
|
||||
Reference in New Issue
Block a user