Initial commit
This commit is contained in:
@@ -0,0 +1,607 @@
|
||||
<template>
|
||||
<div class="sessions-view">
|
||||
<!-- Stats Cards -->
|
||||
<div class="stats-row">
|
||||
<div class="stat-card">
|
||||
<p class="stat-lbl">活跃会话集群</p>
|
||||
<h3 class="stat-val">{{ activeCount }}/{{ total }}</h3>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<p class="stat-lbl">当前筛选调用量</p>
|
||||
<h3 class="stat-val accent">{{ filteredCalls.toLocaleString() }} <span class="stat-unit">次</span></h3>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<p class="stat-lbl">引擎分布情况</p>
|
||||
<div class="engine-dots">
|
||||
<span v-for="(c, eng) in engineDist" :key="eng" class="engine-dot" :style="{ background: getEngineMeta(eng).color }" :title="`${getEngineMeta(eng).label}: ${c}`">{{ getEngineMeta(eng).label }}({{ c }})</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<p class="stat-lbl">异常/中断率</p>
|
||||
<h3 class="stat-val success">{{ errorRate }}%</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search + Filter -->
|
||||
<div class="search-bar">
|
||||
<div class="search-wrap">
|
||||
<span class="search-ico">🔍</span>
|
||||
<input v-model="query" class="search-input" placeholder="输入摘要、会话 ID、文件路径关键字检索..." @keyup.enter="doSearch" />
|
||||
<button v-if="query" class="search-clear" @click="query = ''; doSearch()">✕</button>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<span class="filter-lbl">状态过滤:</span>
|
||||
<button v-for="opt in statusOpts" :key="opt.key" class="filter-btn" :class="{ active: statusFilter === opt.key }" @click="statusFilter = opt.key; doSearch()">{{ opt.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Session Cards -->
|
||||
<div class="session-list">
|
||||
<div class="list-header" v-if="sessions.length">
|
||||
<span class="list-header-text">已过滤出 {{ displayedSessions.length }} 个智能体会话监控流 (Engine: {{ selectedEngine.toUpperCase() }})</span>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading-state">加载中...</div>
|
||||
<div v-else-if="!displayedSessions.length" class="empty-state">
|
||||
<p>无符合当前引擎或状态过滤条件的会话实例。</p>
|
||||
</div>
|
||||
<div v-else class="card-grid">
|
||||
<div
|
||||
v-for="s in displayedSessions"
|
||||
:key="s.key || s.id"
|
||||
class="session-card"
|
||||
:style="{ borderLeftColor: getEngineMeta(s.engine).color }"
|
||||
@click="openDrawer(s)"
|
||||
>
|
||||
<div class="card-main">
|
||||
<div class="card-tags">
|
||||
<span class="engine-badge" :style="{ background: getEngineMeta(s.engine).bg, color: getEngineMeta(s.engine).color, borderColor: getEngineMeta(s.engine).border }">
|
||||
{{ s.engine_label || getEngineMeta(s.engine).label }}
|
||||
</span>
|
||||
<span class="status-badge" :class="'status-' + (s.status || 'unknown')">
|
||||
<span class="status-dot-card" :class="'dot-' + (s.status || 'unknown')"></span>
|
||||
{{ statusLabel(s.status) }}
|
||||
</span>
|
||||
<span class="id-preview mono">会话 ID: <span class="id-highlight">{{ (s.id || '').substring(0, 15) }}...</span></span>
|
||||
<button class="name-btn-card" @click.stop="openNameEditor(s)" title="自定义名称">✏️</button>
|
||||
</div>
|
||||
<h4 class="card-summary">{{ s.custom_name || s.summary || '(无消息)' }}</h4>
|
||||
<div v-if="s.custom_name" class="card-original-summary">{{ s.summary }}</div>
|
||||
<div class="card-path">
|
||||
<span class="path-ico">📁</span>
|
||||
<span class="path-text">{{ s.cwd || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-meta">
|
||||
<div class="meta-col">
|
||||
<span class="meta-col-label">对应版本</span>
|
||||
<span class="meta-col-val">{{ s.cli_version || s.model || '-' }}</span>
|
||||
</div>
|
||||
<div class="meta-col">
|
||||
<span class="meta-col-label">工具调用</span>
|
||||
<span class="meta-col-val calls" :class="callsClass(s.tool_call_count)">{{ s.tool_call_count || 0 }} 次</span>
|
||||
</div>
|
||||
<div class="meta-col">
|
||||
<span class="meta-col-label">开始时间</span>
|
||||
<span class="meta-col-val time">{{ formatTimestamp(s.created_at) }}</span>
|
||||
</div>
|
||||
<div class="meta-col">
|
||||
<button class="detail-btn" @click.stop="goToDetailPage(s)" title="进入详情页">详情 →</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pagination" v-if="total > pageSize">
|
||||
<span class="page-info">共 {{ total }} 条</span>
|
||||
<div class="page-btns">
|
||||
<button class="page-btn" :disabled="page <= 1" @click="goPage(page - 1)">‹</button>
|
||||
<button v-for="p in pageNums" :key="p" v-if="p !== '...'" class="page-btn" :class="{ active: p === page }" @click="goPage(p)">{{ p }}</button>
|
||||
<span v-else class="page-ellipsis">...</span>
|
||||
<button class="page-btn" :disabled="page >= maxPage" @click="goPage(page + 1)">›</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detail Drawer -->
|
||||
<div v-if="drawerSession" class="drawer-overlay" @click.self="closeDrawer">
|
||||
<div class="drawer">
|
||||
<div class="drawer-head">
|
||||
<div class="drawer-title-bar">
|
||||
<span class="drawer-badge" :style="{ background: getEngineMeta(drawerSession.engine).bg, color: getEngineMeta(drawerSession.engine).color, borderColor: getEngineMeta(drawerSession.engine).border }">
|
||||
{{ getEngineMeta(drawerSession.engine).label }} · 运行诊断
|
||||
</span>
|
||||
<button class="drawer-close" @click="closeDrawer">✕</button>
|
||||
</div>
|
||||
<h3 class="drawer-summary">{{ drawerSession.custom_name || drawerSession.summary || '(无消息)' }}</h3>
|
||||
<div v-if="drawerSession.custom_name" class="drawer-original-summary">{{ drawerSession.summary }}</div>
|
||||
<div class="drawer-name-row">
|
||||
<p class="drawer-id mono">完整会话 UUID: {{ drawerSession.id }}</p>
|
||||
<button class="name-edit-btn" @click.stop="openNameEditor(drawerSession)">✏️ 命名</button>
|
||||
<button class="name-edit-btn" @click.stop="generateDrawerTitle" :disabled="drawerAiLoading">🤖 {{ drawerAiLoading ? '生成中...' : 'AI 生成' }}</button>
|
||||
<button v-if="drawerSession.custom_name" class="name-edit-btn" @click.stop="clearCustomName(drawerSession)">🗑 清除</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-body" v-if="drawerDetail">
|
||||
<!-- detail content unchanged -->
|
||||
<div class="drawer-meta-box">
|
||||
<div class="meta-row"><span class="meta-lbl">宿主执行用户:</span><span class="meta-val">{{ drawerSession.model || '-' }}</span></div>
|
||||
<div class="meta-row"><span class="meta-lbl">引擎运行时版本:</span><span class="meta-val">{{ drawerSession.cli_version || '-' }}</span></div>
|
||||
<div class="meta-row"><span class="meta-lbl">工作物理路径:</span><span class="meta-val accent mono">{{ drawerSession.cwd || '-' }}</span></div>
|
||||
<div class="meta-row"><span class="meta-lbl">运行总时长:</span><span class="meta-val">{{ drawerSession.duration_ms ? Math.round(drawerSession.duration_ms / 60000) + ' 分钟' : '暂无数据' }}</span></div>
|
||||
<div class="meta-row"><span class="meta-lbl">诊断执行状态:</span><span class="meta-val">
|
||||
<span class="status-tag-drawer" :class="'tag-' + (drawerSession.status || 'unknown')">{{ statusLabel(drawerSession.status) }}</span>
|
||||
</span></div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-section">
|
||||
<h4 class="drawer-section-title">交互式智能体思考环与工具调用 Trace 链路</h4>
|
||||
<div v-if="!drawerDetail.messages?.length && drawerSession?.engine === 'agy' && drawerSession?.has_detail_db === false" class="trace-empty">该会话的详细 Trace 数据存储在 per-conversation protobuf 中,本地详情缓存不可用。</div>
|
||||
<div v-else-if="!drawerDetail.messages?.length" class="trace-empty">该历史会话没有持久化的 Trace 调试详情数据。</div>
|
||||
<div v-else class="trace-list">
|
||||
<div v-for="(msg, i) in drawerDetail.messages.slice(0, 30)" :key="i" class="trace-item">
|
||||
<span class="trace-dot" :class="traceDot(msg.type)"></span>
|
||||
<div class="trace-content">
|
||||
<span class="trace-tag" :class="'trace-tag-' + traceTag(msg.type)">{{ traceLabel(msg.type) }}</span>
|
||||
<span class="trace-msg">{{ msg.message || msg.name || msg.output_preview || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="drawerError" class="drawer-body">
|
||||
<div class="drawer-error-box">
|
||||
<p class="drawer-error-text">{{ drawerError }}</p>
|
||||
<button class="drawer-retry-btn" @click="retryDrawer">重试</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="!drawerDetail && !drawerError" class="drawer-body">
|
||||
<div class="loading-state">加载中...</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-footer">
|
||||
<button class="footer-btn-copy" @click="copyResume(drawerSession)">
|
||||
📋 复制恢复命令
|
||||
</button>
|
||||
<button class="footer-btn-uuid" @click="copyUUID(drawerSession.id)">
|
||||
📋 复制 UUID 句柄
|
||||
</button>
|
||||
<button class="footer-btn-detail" @click="goToDetailPage(drawerSession)">
|
||||
→ 进入详情页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Name Editor Dialog -->
|
||||
<div v-if="editNameVisible" class="dialog-overlay" @click.self="editNameVisible = false">
|
||||
<div class="dialog">
|
||||
<div class="dialog-head">
|
||||
<h3>自定义名称</h3>
|
||||
<button class="dialog-close" @click="editNameVisible = false">✕</button>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<p class="dialog-hint">为本会话设置一个易记的名称,仅在本地使用。</p>
|
||||
<input
|
||||
v-model="editNameValue"
|
||||
class="dialog-input"
|
||||
placeholder="输入自定义名称..."
|
||||
maxlength="100"
|
||||
@keyup.enter="saveCustomName"
|
||||
/>
|
||||
<p class="dialog-counter">{{ editNameValue.length }}/100</p>
|
||||
</div>
|
||||
<div class="dialog-foot">
|
||||
<button class="dialog-btn" @click="editNameVisible = false">取消</button>
|
||||
<button class="dialog-btn primary" @click="saveCustomName">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, inject, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getSessions, getSessionDetail, getGlobalTools, formatTimestamp, getEngineMeta, ENGINE_META, setCustomName, deleteCustomName, generateTitle } from '../api/index.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const props = defineProps({ selectedEngine: { type: String, default: 'all' } })
|
||||
|
||||
const sessions = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const query = ref('')
|
||||
const statusFilter = ref('all')
|
||||
const page = ref(1)
|
||||
const pageSize = 50
|
||||
const drawerSession = ref(null)
|
||||
const drawerDetail = ref(null)
|
||||
const drawerError = ref(null)
|
||||
const drawerAiLoading = ref(false)
|
||||
const router = useRouter()
|
||||
const activeSessions = ref(0)
|
||||
const filteredCalls = ref(0)
|
||||
|
||||
const statusOpts = [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'active', label: '执行中' },
|
||||
{ key: 'success', label: '已完成' },
|
||||
{ key: 'failed', label: '已中断' },
|
||||
]
|
||||
|
||||
const engineDist = computed(() => {
|
||||
const dist = {}
|
||||
for (const s of sessions.value) {
|
||||
dist[s.engine] = (dist[s.engine] || 0) + 1
|
||||
}
|
||||
return dist
|
||||
})
|
||||
|
||||
const errorCount = computed(() => sessions.value.filter(s => s.status === 'error' || s.status === 'interrupted').length)
|
||||
const errorRate = computed(() => sessions.value.length ? Math.round(errorCount.value / sessions.value.length * 100) : 0)
|
||||
const activeCount = computed(() => sessions.value.filter(s => s.status === 'active').length)
|
||||
|
||||
const displayedSessions = computed(() => {
|
||||
let list = sessions.value
|
||||
if (statusFilter.value === 'success') list = list.filter(s => s.status === 'success')
|
||||
else if (statusFilter.value === 'failed') list = list.filter(s => s.status === 'error' || s.status === 'interrupted')
|
||||
else if (statusFilter.value === 'active') list = list.filter(s => s.status === 'active')
|
||||
return list
|
||||
})
|
||||
|
||||
const maxPage = computed(() => Math.ceil(total.value / pageSize))
|
||||
const pageNums = computed(() => {
|
||||
const p = [], tp = maxPage.value
|
||||
if (tp <= 7) { for (let i = 1; i <= tp; i++) p.push(i) }
|
||||
else {
|
||||
p.push(1)
|
||||
if (page.value > 3) p.push('...')
|
||||
for (let i = Math.max(2, page.value - 1); i <= Math.min(tp - 1, page.value + 1); i++) p.push(i)
|
||||
if (page.value < tp - 2) p.push('...')
|
||||
p.push(tp)
|
||||
}
|
||||
return p
|
||||
})
|
||||
|
||||
function statusLabel(status) {
|
||||
return { success: '已成功', error: '已中断', interrupted: '已中断', active: '执行中', unknown: '未知' }[status] || status
|
||||
}
|
||||
|
||||
function callsClass(c) {
|
||||
if (c > 100) return 'calls-danger'
|
||||
if (c > 50) return 'calls-warn'
|
||||
return 'calls-normal'
|
||||
}
|
||||
|
||||
function traceTag(type) {
|
||||
const map = { user_message: 'primary', function_call: 'warning', function_call_output: 'info', assistant_message: 'success' }
|
||||
return map[type] || ''
|
||||
}
|
||||
function traceDot(type) {
|
||||
const map = { user_message: 'dot-primary', function_call: 'dot-warning', function_call_output: 'dot-info', assistant_message: 'dot-success' }
|
||||
return map[type] || 'dot-default'
|
||||
}
|
||||
function traceLabel(type) {
|
||||
const map = { user_message: '用户', function_call: '工具', function_call_output: '输出', assistant_message: 'AI' }
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
async function loadSessions() {
|
||||
loading.value = true
|
||||
try {
|
||||
// Map frontend status values to backend status values
|
||||
let apiStatus = statusFilter.value;
|
||||
if (apiStatus === 'all') apiStatus = null;
|
||||
// 'failed' is handled by backend as error+interrupted
|
||||
|
||||
const data = await getSessions({
|
||||
engine: props.selectedEngine,
|
||||
query: query.value,
|
||||
status: apiStatus,
|
||||
limit: pageSize,
|
||||
offset: (page.value - 1) * pageSize,
|
||||
})
|
||||
sessions.value = data.sessions || []
|
||||
total.value = data.total || 0
|
||||
// Also fetch stats for calls
|
||||
const tools = await getGlobalTools({ engine: props.selectedEngine })
|
||||
filteredCalls.value = tools.tool_usage?.reduce((a, b) => a + b.total_calls, 0) || 0
|
||||
} catch (err) {
|
||||
ElMessage.error('加载失败: ' + err.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function doSearch() { page.value = 1; loadSessions() }
|
||||
function goPage(p) { if (p >= 1 && p <= maxPage.value) { page.value = p; loadSessions() } }
|
||||
|
||||
async function openDrawer(s) {
|
||||
drawerSession.value = s
|
||||
drawerDetail.value = null
|
||||
drawerError.value = null
|
||||
try {
|
||||
const data = await getSessionDetail(s.engine, s.id)
|
||||
drawerDetail.value = data
|
||||
} catch (err) {
|
||||
drawerError.value = '加载详情失败: ' + (err.message || '')
|
||||
}
|
||||
}
|
||||
|
||||
function retryDrawer() {
|
||||
if (drawerSession.value) openDrawer(drawerSession.value)
|
||||
}
|
||||
|
||||
function closeDrawer() { drawerSession.value = null; drawerDetail.value = null; drawerError.value = null }
|
||||
|
||||
function copyResume(s) {
|
||||
if (!s.resume_command) return
|
||||
navigator.clipboard.writeText(s.resume_command).then(() => {
|
||||
ElMessage.success(`已复制: ${s.resume_command}`)
|
||||
}).catch(() => { ElMessage.success(`手动复制: ${s.resume_command}`) })
|
||||
}
|
||||
|
||||
function copyUUID(id) {
|
||||
navigator.clipboard.writeText(id).then(() => ElMessage.success('UUID 已复制'))
|
||||
}
|
||||
|
||||
// Custom name editing
|
||||
const editNameVisible = ref(false)
|
||||
const editNameValue = ref('')
|
||||
const editNameTarget = ref(null)
|
||||
|
||||
function openNameEditor(session) {
|
||||
editNameTarget.value = session
|
||||
editNameValue.value = session.custom_name || ''
|
||||
editNameVisible.value = true
|
||||
}
|
||||
|
||||
async function saveCustomName() {
|
||||
const s = editNameTarget.value
|
||||
if (!s) return
|
||||
try {
|
||||
const result = await setCustomName(s.engine, s.id, editNameValue.value)
|
||||
// Update local state
|
||||
s.custom_name = result.custom_name || null
|
||||
// Also update drawer session if same
|
||||
if (drawerSession.value?.id === s.id && drawerSession.value?.engine === s.engine) {
|
||||
drawerSession.value.custom_name = s.custom_name
|
||||
}
|
||||
editNameVisible.value = false
|
||||
ElMessage.success('名称已保存')
|
||||
} catch (err) {
|
||||
ElMessage.error('保存失败: ' + err.message)
|
||||
}
|
||||
}
|
||||
|
||||
async function clearCustomName(session) {
|
||||
try {
|
||||
await deleteCustomName(session.engine, session.id)
|
||||
session.custom_name = null
|
||||
if (drawerSession.value?.id === session.id && drawerSession.value?.engine === session.engine) {
|
||||
drawerSession.value.custom_name = null
|
||||
}
|
||||
ElMessage.success('名称已清除')
|
||||
} catch (err) {
|
||||
ElMessage.error('清除失败: ' + err.message)
|
||||
}
|
||||
}
|
||||
|
||||
function goToDetailPage(session) {
|
||||
if (!session) return
|
||||
closeDrawer()
|
||||
router.push({ name: 'session-detail', params: { id: session.id }, query: { engine: session.engine } })
|
||||
}
|
||||
|
||||
async function generateDrawerTitle() {
|
||||
const s = drawerSession.value
|
||||
if (!s) return
|
||||
drawerAiLoading.value = true
|
||||
try {
|
||||
const result = await generateTitle(s.engine, s.id)
|
||||
if (result.title) {
|
||||
editNameTarget.value = s
|
||||
editNameValue.value = result.title
|
||||
editNameVisible.value = true
|
||||
ElMessage.success('标题已生成,可点击保存确认')
|
||||
} else {
|
||||
ElMessage.warning(result.error || '生成失败')
|
||||
}
|
||||
} catch (err) {
|
||||
ElMessage.error('生成失败: ' + err.message)
|
||||
} finally {
|
||||
drawerAiLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.selectedEngine, () => { page.value = 1; loadSessions() })
|
||||
onMounted(loadSessions)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sessions-view { width: 100%; }
|
||||
|
||||
/* Stats */
|
||||
.stats-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 16px; }
|
||||
.stat-card { padding: 16px 20px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg-card); }
|
||||
.stat-lbl { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
|
||||
.stat-val { font-size: 22px; font-weight: 800; font-family: 'SF Mono', monospace; color: var(--text-primary); }
|
||||
.stat-val.accent { color: var(--accent-light); }
|
||||
.stat-val.success { color: var(--success); }
|
||||
.stat-unit { font-size: 13px; font-weight: 400; color: var(--text-muted); }
|
||||
.engine-dots { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
|
||||
.engine-dot { font-size: 10px; padding: 1px 6px; border-radius: 4px; color: #fff; font-family: 'SF Mono', monospace; }
|
||||
|
||||
/* Search */
|
||||
.search-bar { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-bottom: 16px; }
|
||||
.search-wrap { position: relative; flex: 1; min-width: 200px; max-width: 480px; }
|
||||
.search-ico { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); font-size: 14px; opacity: 0.5; }
|
||||
.search-input { width: 100%; padding: 9px 32px 9px 36px; border-radius: 10px; border: 1px solid var(--border); background: var(--bg-input); color: var(--text-primary); font-size: 13px; outline: none; }
|
||||
.search-input:focus { border-color: var(--accent); }
|
||||
.search-clear { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 12px; }
|
||||
.filter-group { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||||
.filter-lbl { font-size: 11px; color: var(--text-muted); }
|
||||
.filter-btn { padding: 5px 12px; border-radius: 8px; border: 1px solid var(--border); background: transparent; color: var(--text-secondary); font-size: 11px; cursor: pointer; }
|
||||
.filter-btn:hover { background: var(--accent-bg); }
|
||||
.filter-btn.active { background: var(--accent-bg); border-color: var(--accent); color: var(--accent-light); font-weight: 600; }
|
||||
|
||||
/* Session Cards */
|
||||
.session-list { }
|
||||
.list-header { margin-bottom: 12px; }
|
||||
.list-header-text { font-size: 12px; color: var(--text-muted); font-weight: 500; }
|
||||
|
||||
.loading-state { padding: 48px; text-align: center; color: var(--text-muted); font-size: 14px; }
|
||||
.empty-state { padding: 48px; text-align: center; color: var(--text-muted); border-radius: 12px; border: 1px solid var(--border); background: var(--bg-card); font-size: 13px; }
|
||||
|
||||
.card-grid { display: flex; flex-direction: column; gap: 12px; }
|
||||
.session-card {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 16px 20px; border-radius: 12px; border: 1px solid var(--border);
|
||||
background: var(--bg-card); cursor: pointer; transition: all 0.12s;
|
||||
border-left: 4px solid var(--accent);
|
||||
}
|
||||
.session-card:hover { border-color: var(--text-muted); background: var(--bg-card-hover); }
|
||||
.card-main { flex: 1; min-width: 0; padding-right: 16px; }
|
||||
.card-tags { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; flex-wrap: wrap; }
|
||||
.engine-badge { padding: 2px 8px; border-radius: 4px; font-size: 9px; font-weight: 700; letter-spacing: 0.5px; border: 1px solid; font-family: 'SF Mono', monospace; }
|
||||
.status-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 4px; font-size: 9px; font-weight: 600; }
|
||||
.status-badge.status-success { background: rgba(16,185,129,0.1); color: var(--success); }
|
||||
.status-badge.status-error { background: rgba(239,68,68,0.1); color: var(--danger); }
|
||||
.status-badge.status-interrupted { background: rgba(245,158,11,0.1); color: var(--warning); }
|
||||
.status-badge.status-active { background: rgba(59,130,246,0.1); color: #3b82f6; animation: pulse 1.5s infinite; }
|
||||
.status-badge.status-unknown { background: rgba(100,116,139,0.1); color: var(--text-muted); }
|
||||
.status-dot-card { width: 6px; height: 6px; border-radius: 50%; }
|
||||
.dot-success { background: var(--success); }
|
||||
.dot-error { background: var(--danger); }
|
||||
.dot-interrupted { background: var(--warning); }
|
||||
.dot-active { background: #3b82f6; }
|
||||
.dot-unknown { background: var(--text-muted); }
|
||||
.id-preview { font-size: 10px; color: var(--text-muted); }
|
||||
.id-highlight { color: var(--accent-light); }
|
||||
.name-btn-card { background: none; border: none; font-size: 12px; cursor: pointer; opacity: 0.4; padding: 0 2px; }
|
||||
.name-btn-card:hover { opacity: 1; }
|
||||
.card-summary { font-size: 13px; font-weight: 500; color: var(--text-primary); line-height: 1.4; margin-bottom: 6px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.card-original-summary { font-size: 11px; color: var(--text-muted); margin-bottom: 6px; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.card-path { display: flex; align-items: center; gap: 4px; font-size: 10px; color: var(--text-muted); }
|
||||
.path-ico { font-size: 12px; }
|
||||
.path-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: 'SF Mono', monospace; }
|
||||
|
||||
.card-meta { display: flex; align-items: center; gap: 16px; flex-shrink: 0; }
|
||||
.meta-col { text-align: center; }
|
||||
.meta-col-label { display: block; font-size: 9px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; }
|
||||
.meta-col-val { font-size: 11px; font-family: 'SF Mono', monospace; color: var(--text-secondary); }
|
||||
.meta-col-val.time { font-size: 10px; }
|
||||
.meta-col-val.calls { font-weight: 600; padding: 1px 8px; border-radius: 10px; }
|
||||
.calls-danger { color: var(--danger); background: rgba(239,68,68,0.1); }
|
||||
.calls-warn { color: var(--warning); background: rgba(245,158,11,0.1); }
|
||||
.calls-normal { color: var(--text-secondary); background: rgba(100,116,139,0.1); }
|
||||
.arrow { font-size: 18px; color: var(--text-muted); }
|
||||
|
||||
/* Pagination */
|
||||
.pagination { display: flex; align-items: center; justify-content: center; gap: 16px; margin-top: 16px; padding: 12px; }
|
||||
.page-info { font-size: 12px; color: var(--text-muted); }
|
||||
.page-btns { display: flex; gap: 4px; align-items: center; }
|
||||
.page-btn { min-width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 6px; border: 1px solid var(--border); background: var(--bg-input); color: var(--text-secondary); font-size: 12px; font-family: 'SF Mono', monospace; cursor: pointer; }
|
||||
.page-btn:hover { border-color: var(--accent); color: var(--accent-light); }
|
||||
.page-btn.active { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||
.page-btn:disabled { opacity: 0.4; }
|
||||
.page-ellipsis { padding: 0 4px; color: var(--text-muted); }
|
||||
|
||||
/* Drawer */
|
||||
.drawer-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 500; display: flex; justify-content: flex-end; }
|
||||
.drawer { width: 100%; max-width: 560px; height: 100%; background: var(--bg-card); border-left: 1px solid var(--border); display: flex; flex-direction: column; animation: slideIn 0.25s ease; }
|
||||
@keyframes slideIn { from { transform: translateX(100%); } to { transform: translateX(0); } }
|
||||
.drawer-head { padding: 20px; border-bottom: 1px solid var(--border); background: rgba(15,23,42,0.3); }
|
||||
.drawer-title-bar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
||||
.drawer-badge { padding: 3px 10px; border-radius: 4px; font-size: 10px; font-weight: 700; border: 1px solid; font-family: 'SF Mono', monospace; }
|
||||
.drawer-close { background: none; border: none; color: var(--text-muted); font-size: 18px; cursor: pointer; }
|
||||
.drawer-close:hover { color: var(--text-primary); }
|
||||
.drawer-summary { font-size: 14px; font-weight: 600; color: var(--text-primary); line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.drawer-id { font-size: 10px; color: var(--text-muted); margin-top: 4px; }
|
||||
|
||||
.drawer-body { flex: 1; overflow-y: auto; padding: 16px 20px; }
|
||||
.drawer-meta-box { padding: 12px 16px; border-radius: 10px; border: 1px solid var(--border); background: rgba(15,23,42,0.2); margin-bottom: 16px; }
|
||||
.meta-row { display: grid; grid-template-columns: 100px 1fr; gap: 8px; padding: 5px 0; font-size: 11px; }
|
||||
.meta-lbl { color: var(--text-muted); }
|
||||
.meta-val { color: var(--text-primary); font-weight: 500; }
|
||||
.meta-val.accent { color: var(--accent-light); }
|
||||
.status-tag-drawer { display: inline-block; padding: 1px 8px; border-radius: 4px; font-size: 9px; font-weight: 700; }
|
||||
.tag-success { background: rgba(16,185,129,0.1); color: var(--success); }
|
||||
.tag-error { background: rgba(239,68,68,0.1); color: var(--danger); }
|
||||
.tag-interrupted { background: rgba(245,158,11,0.1); color: var(--warning); }
|
||||
.tag-active { background: rgba(59,130,246,0.1); color: #3b82f6; }
|
||||
.tag-unknown { background: rgba(100,116,139,0.1); color: var(--text-muted); }
|
||||
|
||||
.drawer-section { margin-bottom: 16px; }
|
||||
.drawer-section-title { font-size: 12px; font-weight: 600; color: var(--text-secondary); margin-bottom: 8px; }
|
||||
.drawer-error-box { padding: 32px; text-align: center; }
|
||||
.drawer-error-text { font-size: 13px; color: var(--danger); margin-bottom: 12px; }
|
||||
.drawer-retry-btn { padding: 6px 16px; border-radius: 6px; border: 1px solid var(--accent); background: var(--accent-bg); color: var(--accent-light); cursor: pointer; font-size: 12px; }
|
||||
.trace-empty { padding: 24px; text-align: center; color: var(--text-muted); font-size: 12px; border: 1px dashed var(--border); border-radius: 8px; }
|
||||
.trace-list { }
|
||||
.trace-item { display: flex; gap: 8px; padding: 4px 0; align-items: flex-start; }
|
||||
.trace-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; margin-top: 4px; }
|
||||
.dot-primary { background: var(--accent); }
|
||||
.dot-success { background: var(--success); }
|
||||
.dot-warning { background: var(--warning); }
|
||||
.dot-info { background: var(--text-muted); }
|
||||
.dot-default { background: var(--text-muted); }
|
||||
.trace-content { flex: 1; min-width: 0; }
|
||||
.trace-tag { display: inline-block; padding: 0 6px; border-radius: 3px; font-size: 9px; font-weight: 600; margin-right: 6px; }
|
||||
.trace-tag-primary { background: var(--accent-bg); color: var(--accent-light); }
|
||||
.trace-tag-success { background: rgba(16,185,129,0.1); color: var(--success); }
|
||||
.trace-tag-warning { background: rgba(245,158,11,0.1); color: var(--warning); }
|
||||
.trace-tag-info { background: rgba(100,116,139,0.1); color: var(--text-muted); }
|
||||
.trace-msg { font-size: 10px; color: var(--text-secondary); }
|
||||
|
||||
.drawer-footer { display: flex; gap: 8px; padding: 16px 20px; border-top: 1px solid var(--border); background: rgba(15,23,42,0.2); }
|
||||
.footer-btn-copy, .footer-btn-uuid { flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 10px; border-radius: 8px; font-size: 12px; font-weight: 600; cursor: pointer; }
|
||||
.footer-btn-copy { background: var(--accent); border: none; color: #fff; }
|
||||
.footer-btn-copy:hover { opacity: 0.9; }
|
||||
.footer-btn-uuid { border: 1px solid var(--border); background: var(--bg-input); color: var(--text-secondary); }
|
||||
.footer-btn-uuid:hover { background: var(--bg-card-hover); }
|
||||
|
||||
/* Name editor */
|
||||
.drawer-original-summary { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
|
||||
.drawer-name-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.name-edit-btn { background: none; border: 1px solid var(--border); padding: 2px 8px; border-radius: 4px; font-size: 10px; color: var(--text-secondary); cursor: pointer; }
|
||||
.name-edit-btn:hover { border-color: var(--accent); color: var(--accent-light); }
|
||||
.detail-btn { background: var(--accent-bg); border: 1px solid var(--accent-border); padding: 4px 10px; border-radius: 4px; font-size: 11px; font-weight: 600; color: var(--accent-light); cursor: pointer; white-space: nowrap; }
|
||||
.detail-btn:hover { background: var(--accent); color: #fff; }
|
||||
.dialog-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 600; display: flex; align-items: center; justify-content: center; }
|
||||
.dialog { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; width: 90%; max-width: 420px; }
|
||||
.dialog-head { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid var(--border); }
|
||||
.dialog-head h3 { font-size: 15px; font-weight: 600; color: var(--text-primary); }
|
||||
.dialog-close { background: none; border: none; color: var(--text-muted); font-size: 18px; cursor: pointer; }
|
||||
.dialog-body { padding: 16px 20px; }
|
||||
.dialog-hint { font-size: 12px; color: var(--text-muted); margin-bottom: 8px; }
|
||||
.dialog-input { width: 100%; padding: 9px 12px; border-radius: 8px; border: 1px solid var(--border); background: var(--bg-input); color: var(--text-primary); font-size: 13px; outline: none; }
|
||||
.dialog-input:focus { border-color: var(--accent); }
|
||||
.dialog-counter { text-align: right; font-size: 11px; color: var(--text-muted); margin-top: 4px; }
|
||||
.dialog-foot { display: flex; gap: 8px; padding: 12px 20px; border-top: 1px solid var(--border); justify-content: flex-end; }
|
||||
.dialog-btn { padding: 8px 16px; border-radius: 8px; border: 1px solid var(--border); background: transparent; color: var(--text-secondary); font-size: 12px; cursor: pointer; }
|
||||
.dialog-btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||
.dialog-btn:hover { opacity: 0.9; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-row { grid-template-columns: repeat(2, 1fr); }
|
||||
.session-card { flex-direction: column; align-items: flex-start; gap: 8px; }
|
||||
.card-meta { width: 100%; justify-content: space-around; }
|
||||
.drawer { max-width: 100%; }
|
||||
}
|
||||
.footer-btn-detail {
|
||||
flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px;
|
||||
padding: 10px; border-radius: 8px; font-size: 12px; font-weight: 600; cursor: pointer;
|
||||
border: 1px solid var(--accent-border); background: var(--accent-bg);
|
||||
color: var(--accent-light); transition: all 0.12s;
|
||||
}
|
||||
.footer-btn-detail:hover { background: var(--accent); color: #fff; }
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.drawer-footer { flex-wrap: wrap; }
|
||||
.footer-btn-copy, .footer-btn-uuid, .footer-btn-detail { flex: 1 1 100%; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user