feat: 增强知识库索引与设置页面模块化拆分
扩展知识库索引任务和 RAG 检索支持增量入库和文档去重,优 化本体检测和规则匹配精度,前端设置页面拆分为 LLM、邮件 和 Hermes 员工同步子面板并重构样式,新增日志详情组件和 知识入库日志模型,补充单元测试覆盖。
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
<aside class="rail" aria-label="主导航">
|
||||
<div class="rail-brand">
|
||||
<div class="brand-mark" aria-hidden="true">
|
||||
<svg viewBox="0 0 36 36">
|
||||
<img v-if="companyLogo" :src="companyLogo" alt="System Logo" class="custom-logo" />
|
||||
<svg v-else viewBox="0 0 36 36">
|
||||
<path d="M19.8 4.5c5.7 1.1 9.9 5.7 10.5 11.6-2.8-.9-5.5-.7-7.9.6-2.8 1.5-4.5 4.3-5.2 8.2-4.4-2.8-6.5-6.5-6.3-11.1.2-4.2 3.5-7.8 8.9-9.3Z" />
|
||||
<path d="M9 7.6c-3 3.5-4 7.3-2.9 11.2 1.2 4.2 4.6 7 10.1 8.5-2 1.8-4.6 2.6-7.6 2.3C5.1 26.7 3.5 23.1 3.7 19 4 14.4 5.7 10.6 9 7.6Z" />
|
||||
</svg>
|
||||
@@ -57,6 +58,10 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
companyLogo: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
currentUser: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
@@ -145,6 +150,14 @@ const displayCompanyName = computed(() => props.companyName || 'X-Financial')
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #07936f;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.custom-logo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.brand-mark svg {
|
||||
@@ -195,10 +208,9 @@ const displayCompanyName = computed(() => props.companyName || 'X-Financial')
|
||||
}
|
||||
|
||||
.nav-btn.active {
|
||||
background: linear-gradient(90deg, rgba(16, 185, 129, 0.16), rgba(16, 185, 129, 0.08));
|
||||
border-color: rgba(16, 185, 129, 0.1);
|
||||
background: #ecfdf5;
|
||||
border-color: rgba(16, 185, 129, 0.12);
|
||||
color: #059669;
|
||||
box-shadow: inset 3px 0 0 #10b981;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
@@ -224,7 +236,7 @@ const displayCompanyName = computed(() => props.companyName || 'X-Financial')
|
||||
min-width: 0;
|
||||
color: currentColor;
|
||||
font-size: 14px;
|
||||
font-weight: 750;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
727
web/src/components/logs/KnowledgeIngestRunPanel.vue
Normal file
727
web/src/components/logs/KnowledgeIngestRunPanel.vue
Normal file
@@ -0,0 +1,727 @@
|
||||
<template>
|
||||
<article class="knowledge-ingest-panel panel">
|
||||
<header class="ingest-head">
|
||||
<div>
|
||||
<span class="eyebrow">LightRAG 知识归集</span>
|
||||
<h3>{{ model.folder || '未指定目录' }}</h3>
|
||||
<p>{{ model.phaseLabel }} · {{ model.statusLabel }}</p>
|
||||
</div>
|
||||
<div class="progress-ring" :aria-label="`归集进度 ${model.progress.percent}%`">
|
||||
<strong>{{ model.progress.percent }}%</strong>
|
||||
<span>进度</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="progress-bar" aria-hidden="true">
|
||||
<span :style="{ width: `${model.progress.percent}%` }"></span>
|
||||
</div>
|
||||
|
||||
<div class="metric-strip">
|
||||
<div v-for="metric in model.metrics" :key="metric.label" class="metric-tile">
|
||||
<span>{{ metric.label }}</span>
|
||||
<strong>{{ metric.value }}</strong>
|
||||
<small>{{ metric.hint }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ingest-workspace">
|
||||
<aside class="file-rail">
|
||||
<button
|
||||
v-for="document in model.documents"
|
||||
:key="document.documentId"
|
||||
type="button"
|
||||
class="file-item"
|
||||
:class="{ active: selectedDocumentId === document.documentId }"
|
||||
@click="selectDocument(document.documentId)"
|
||||
>
|
||||
<i :class="documentIcon(document)"></i>
|
||||
<span class="file-copy">
|
||||
<strong>{{ document.name }}</strong>
|
||||
<small>
|
||||
{{ document.phaseLabel }} · {{ document.chunkCount }} chunk
|
||||
</small>
|
||||
</span>
|
||||
<span class="mini-status" :class="document.statusTone">
|
||||
{{ document.statusLabel }}
|
||||
</span>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<section v-if="selectedDocument" class="file-detail">
|
||||
<div class="detail-topline">
|
||||
<div>
|
||||
<h4>{{ selectedDocument.name }}</h4>
|
||||
<p>
|
||||
{{ selectedDocument.folder || '根目录' }}
|
||||
<span v-if="selectedDocument.extension"> · {{ selectedDocument.extension }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<span class="status-chip" :class="selectedDocument.statusTone">
|
||||
{{ selectedDocument.statusLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-stats">
|
||||
<div>
|
||||
<span>原文字符</span>
|
||||
<strong>{{ formatKnowledgeMetric(selectedDocument.textChars) }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>索引字符</span>
|
||||
<strong>{{ formatKnowledgeMetric(selectedDocument.indexedTextChars) }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Chunk</span>
|
||||
<strong>{{ formatKnowledgeMetric(selectedDocument.chunkCount) }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>实体 / 关系</span>
|
||||
<strong>
|
||||
{{ formatKnowledgeMetric(selectedDocument.entityCount) }}
|
||||
/
|
||||
{{ formatKnowledgeMetric(selectedDocument.relationCount) }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="selectedDocument.error" class="error-note">
|
||||
{{ selectedDocument.error }}
|
||||
</p>
|
||||
|
||||
<div class="detail-section-grid">
|
||||
<section class="detail-section">
|
||||
<div class="section-head">
|
||||
<h5>Chunk 信息</h5>
|
||||
<span>{{ selectedDocument.chunks.length }} 条</span>
|
||||
</div>
|
||||
<div v-if="selectedDocument.chunks.length" class="chunk-list">
|
||||
<div v-for="chunk in selectedDocument.chunks" :key="chunk.id" class="chunk-row">
|
||||
<span class="chunk-index">#{{ chunk.order + 1 }}</span>
|
||||
<div>
|
||||
<strong>{{ compactId(chunk.id) }}</strong>
|
||||
<p>{{ chunk.summary || '暂无摘要' }}</p>
|
||||
</div>
|
||||
<small>{{ chunk.tokens }} tokens</small>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="compact-empty">暂无 chunk 明细</div>
|
||||
</section>
|
||||
|
||||
<section class="detail-section">
|
||||
<div class="section-head">
|
||||
<h5>章节提取</h5>
|
||||
<span>{{ selectedDocument.sectionCount }} 条</span>
|
||||
</div>
|
||||
<div v-if="selectedDocument.sections.length" class="section-list">
|
||||
<div
|
||||
v-for="section in selectedDocument.sections"
|
||||
:key="section.title"
|
||||
class="section-row"
|
||||
>
|
||||
<strong>{{ section.title }}</strong>
|
||||
<p>{{ section.excerpt || '暂无章节摘要' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="compact-empty">暂无章节信息</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="detail-section">
|
||||
<div class="section-head">
|
||||
<h5>处理事件</h5>
|
||||
<span>{{ selectedDocument.events.length }} 条</span>
|
||||
</div>
|
||||
<div v-if="selectedDocument.events.length" class="event-list">
|
||||
<div
|
||||
v-for="event in selectedDocument.events"
|
||||
:key="`${event.at}-${event.message}`"
|
||||
class="event-row"
|
||||
:class="event.level"
|
||||
>
|
||||
<span></span>
|
||||
<div>
|
||||
<strong>{{ formatEventTime(event.at) }}</strong>
|
||||
<p>{{ event.message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="compact-empty">暂无处理事件</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="graph-section">
|
||||
<div class="section-head">
|
||||
<h4>图谱形成</h4>
|
||||
<span>
|
||||
{{ formatKnowledgeMetric(model.graph.entityCount) }} 实体 ·
|
||||
{{ formatKnowledgeMetric(model.graph.relationCount) }} 关系
|
||||
</span>
|
||||
</div>
|
||||
<div class="graph-grid">
|
||||
<div class="graph-pane">
|
||||
<h5>实体</h5>
|
||||
<div v-if="model.graph.entities.length" class="entity-cloud">
|
||||
<span v-for="entity in model.graph.entities" :key="entity">{{ entity }}</span>
|
||||
</div>
|
||||
<div v-else class="compact-empty">暂无实体</div>
|
||||
</div>
|
||||
<div class="graph-pane">
|
||||
<h5>关系</h5>
|
||||
<div v-if="model.graph.relations.length" class="relation-list">
|
||||
<div
|
||||
v-for="relation in model.graph.relations"
|
||||
:key="`${relation.source}-${relation.target}-${relation.type}`"
|
||||
class="relation-row"
|
||||
>
|
||||
<strong>{{ relation.source }}</strong>
|
||||
<i class="mdi mdi-arrow-right-thin"></i>
|
||||
<strong>{{ relation.target }}</strong>
|
||||
<span>{{ relation.type }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="compact-empty">暂无关系</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import {
|
||||
buildKnowledgeIngestLogModel,
|
||||
formatKnowledgeMetric
|
||||
} from '../../utils/knowledgeIngestLogModel.js'
|
||||
|
||||
const props = defineProps({
|
||||
run: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const selectedDocumentId = ref('')
|
||||
const model = computed(() => buildKnowledgeIngestLogModel(props.run))
|
||||
const selectedDocument = computed(
|
||||
() => model.value.documents.find((item) => item.documentId === selectedDocumentId.value) || null
|
||||
)
|
||||
|
||||
watch(
|
||||
() => model.value.selectedDocumentId,
|
||||
(nextDocumentId) => {
|
||||
if (!nextDocumentId) {
|
||||
selectedDocumentId.value = ''
|
||||
return
|
||||
}
|
||||
if (!selectedDocumentId.value || !model.value.documents.some((item) => item.documentId === selectedDocumentId.value)) {
|
||||
selectedDocumentId.value = nextDocumentId
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function selectDocument(documentId) {
|
||||
selectedDocumentId.value = documentId
|
||||
}
|
||||
|
||||
function documentIcon(document) {
|
||||
const extension = String(document?.extension || '').toLowerCase()
|
||||
if (extension === 'pdf') return 'mdi mdi-file-pdf-box'
|
||||
if (['doc', 'docx'].includes(extension)) return 'mdi mdi-file-word-box'
|
||||
if (['xls', 'xlsx', 'csv'].includes(extension)) return 'mdi mdi-file-excel-box'
|
||||
if (['ppt', 'pptx'].includes(extension)) return 'mdi mdi-file-powerpoint-box'
|
||||
return 'mdi mdi-file-document-outline'
|
||||
}
|
||||
|
||||
function compactId(value) {
|
||||
const text = String(value || '').trim()
|
||||
if (text.length <= 18) return text || 'chunk'
|
||||
return `${text.slice(0, 8)}...${text.slice(-6)}`
|
||||
}
|
||||
|
||||
function formatEventTime(value) {
|
||||
if (!value) return '刚刚'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return String(value)
|
||||
return date.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.knowledge-ingest-panel {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.ingest-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
color: #0f766e;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.ingest-head h3 {
|
||||
margin: 5px 0 0;
|
||||
color: #0f172a;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.ingest-head p {
|
||||
margin: 6px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.progress-ring {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid #d7e0ea;
|
||||
border-radius: 50%;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.progress-ring strong,
|
||||
.progress-ring span {
|
||||
grid-area: 1 / 1;
|
||||
}
|
||||
|
||||
.progress-ring strong {
|
||||
margin-top: -12px;
|
||||
color: #0f172a;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.progress-ring span {
|
||||
margin-top: 26px;
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 7px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: #e5eaf0;
|
||||
}
|
||||
|
||||
.progress-bar span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, #0f766e, #2563eb);
|
||||
transition: width 0.24s ease;
|
||||
}
|
||||
|
||||
.metric-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.metric-tile {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 11px 12px;
|
||||
border: 1px solid #e5edf5;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.metric-tile span,
|
||||
.metric-tile small {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.metric-tile strong {
|
||||
color: #0f172a;
|
||||
font-size: 18px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.ingest-workspace {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(230px, 0.85fr) minmax(0, 2fr);
|
||||
gap: 14px;
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.file-rail {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
width: 100%;
|
||||
min-height: 58px;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid #e5edf5;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.file-item.active {
|
||||
border-color: rgba(15, 118, 110, 0.38);
|
||||
background: #f0fdfa;
|
||||
box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.08);
|
||||
}
|
||||
|
||||
.file-item > i {
|
||||
color: #334155;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.file-copy {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.file-copy strong,
|
||||
.file-copy small {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-copy strong {
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.file-copy small {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mini-status,
|
||||
.status-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 24px;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mini-status.success,
|
||||
.status-chip.success {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.mini-status.warning,
|
||||
.status-chip.warning {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.mini-status.danger,
|
||||
.status-chip.danger {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.mini-status.muted,
|
||||
.status-chip.muted {
|
||||
background: #eef2f7;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.file-detail {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.detail-topline {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #e5edf5;
|
||||
}
|
||||
|
||||
.detail-topline h4 {
|
||||
margin: 0;
|
||||
color: #0f172a;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.detail-topline p {
|
||||
margin: 5px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.detail-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-stats div {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.detail-stats span {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.detail-stats strong {
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.error-note {
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 8px;
|
||||
background: #fff1f2;
|
||||
color: #991b1b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.detail-section-grid,
|
||||
.graph-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.detail-section,
|
||||
.graph-section,
|
||||
.graph-pane {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.section-head h4,
|
||||
.section-head h5,
|
||||
.graph-pane h5 {
|
||||
margin: 0;
|
||||
color: #0f172a;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.section-head span {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.chunk-list,
|
||||
.section-list,
|
||||
.event-list,
|
||||
.relation-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chunk-row,
|
||||
.section-row,
|
||||
.event-row,
|
||||
.relation-row {
|
||||
min-width: 0;
|
||||
border: 1px solid #e5edf5;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.chunk-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.chunk-index {
|
||||
color: #2563eb;
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.chunk-row strong,
|
||||
.section-row strong,
|
||||
.event-row strong,
|
||||
.relation-row strong {
|
||||
color: #0f172a;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.chunk-row p,
|
||||
.section-row p,
|
||||
.event-row p {
|
||||
margin: 4px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.chunk-row small {
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.section-row {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.event-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.event-row > span {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-top: 5px;
|
||||
border-radius: 999px;
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.event-row.error > span {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.entity-cloud {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.entity-cloud span {
|
||||
max-width: 100%;
|
||||
padding: 5px 9px;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 999px;
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.relation-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 10px;
|
||||
}
|
||||
|
||||
.relation-row strong {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.relation-row i {
|
||||
color: #0f766e;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.relation-row span {
|
||||
padding: 3px 7px;
|
||||
border-radius: 999px;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.compact-empty {
|
||||
min-height: 42px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 8px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.metric-strip,
|
||||
.detail-stats,
|
||||
.detail-section-grid,
|
||||
.graph-grid,
|
||||
.ingest-workspace {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.file-rail {
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.ingest-head,
|
||||
.detail-topline {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.progress-ring {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.mini-status {
|
||||
grid-column: 2;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.relation-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user