neural-sync/frontend/src/components/ContextManager.vue

322 lines
9.0 KiB
Vue

<template>
<n-space vertical>
<n-card title="活体上下文 (认知节点)" size="medium" class="glass-card">
<template #header-extra>
<n-button type="primary" @click="fetchNodes" ghost>刷新</n-button>
<n-button @click="showModal = true" style="margin-left: 12px;" ghost>新增认知</n-button>
</template>
<n-collapse accordion>
<n-collapse-item v-for="node in parsedNodes" :key="node.id" :name="node.id">
<template #header>
<div class="node-header">
<n-tag :type="node.status === 'active' ? 'success' : 'warning'" size="small" round style="margin-right: 12px;">
v{{ node.version }}
</n-tag>
<span class="node-title">{{ node.extractedTitle || '未命名节点' }}</span>
<n-tag v-if="node.techStack.length" size="tiny" type="info" ghost style="margin-left: 12px;">
{{ node.techStack.length }} 技术栈
</n-tag>
</div>
</template>
<div class="node-content">
<!-- AI Insights Section -->
<div v-if="node.ai_insights && (node.ai_insights.moat || node.ai_insights.tech_specs)" class="section-block insight-block">
<div class="section-title">✨ AI 核心洞察 (Insights)</div>
<!-- Core Moat -->
<div v-if="node.ai_insights.moat" class="insight-item">
<div class="insight-label">🛡️ 核心护城河</div>
<div class="insight-value">{{ node.ai_insights.moat }}</div>
</div>
<!-- Tech Specs -->
<div v-if="node.ai_insights.tech_specs" class="insight-specs">
<div class="insight-label">⚙️ 关键技术参数</div>
<n-grid :x-gap="12" :y-gap="8" :cols="2">
<n-grid-item v-for="(val, key) in node.ai_insights.tech_specs" :key="key">
<div class="spec-card">
<span class="spec-key">{{ key }}</span>
<span class="spec-val">{{ val }}</span>
</div>
</n-grid-item>
</n-grid>
</div>
</div>
<!-- Tech Stack Section -->
<div v-if="node.techStack.length" class="section-block">
<div class="section-title">🛠️ 技术栈 (Tech Stack)</div>
<n-space size="small">
<n-tag v-for="tech in node.techStack" :key="tech" size="small" type="info" secondary>
{{ tech }}
</n-tag>
</n-space>
</div>
<!-- Core Logic Section -->
<div v-if="node.coreLogic" class="section-block">
<div class="section-title">🧠 核心逻辑 (Core Logic)</div>
<div class="core-logic-text">{{ node.coreLogic }}</div>
</div>
<n-divider />
<!-- Full Markdown Content -->
<div class="markdown-body" v-html="node.htmlContent"></div>
</div>
</n-collapse-item>
</n-collapse>
</n-card>
<n-modal v-model:show="showModal">
<n-card style="width: 600px" title="定义新认知" :bordered="false" size="huge" role="dialog" aria-modal="true" class="glass-modal">
<n-form ref="formRef" :model="formValue">
<n-form-item label="内容 (Markdown)" path="content">
<n-input v-model:value="formValue.content" type="textarea" placeholder="# 标题\n\n## 技术栈\n- Python\n- Vue\n\n## 核心逻辑\n..." :rows="10" />
</n-form-item>
</n-form>
<template #footer>
<n-button type="primary" block @click="createNode">提交认知</n-button>
</template>
</n-card>
</n-modal>
</n-space>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { NSpace, NCard, NButton, NCollapse, NCollapseItem, NTag, NModal, NForm, NFormItem, NInput, useMessage, NDivider, NGrid, NGridItem } from 'naive-ui'
import axios from 'axios'
import { marked } from 'marked'
const message = useMessage()
const nodes = ref<any[]>([])
const showModal = ref(false)
const formValue = ref({
content: ''
})
// Heuristic Extraction
const extractMetadata = (content: string) => {
const lines = content.split('\n')
let title = ''
let techStack: string[] = []
let coreLogic = ''
// 1. Extract Title (First H1 or first non-empty line)
const titleMatch = content.match(/^#\s+(.*)/m)
if (titleMatch && titleMatch[1]) {
title = titleMatch[1].trim()
} else {
const firstLine = lines.find(l => l.trim().length > 0)
if (firstLine) title = firstLine.replace(/^#+\s+/, '').substring(0, 50)
}
// 1.5 Extract AI Insights (If stored in JSON in backend but not parsed here, we rely on backend data)
// This frontend extractor is for 'raw markdown' metadata.
// The 'node' object from API already has 'ai_insights'.
// We just need to ensure we don't overwrite it or we display it.
// 2. Extract Tech Stack (Keywords or Section)
// Simple keyword search for now
const keywords = ['Python', 'Vue', 'FastAPI', 'PostgreSQL', 'SQLite', 'Redis', 'Docker', 'Kubernetes', 'Ollama', 'LLM', 'RAG', 'LangChain', 'ComfyUI']
const foundKeywords = new Set<string>()
keywords.forEach(k => {
if (content.toLowerCase().includes(k.toLowerCase())) {
foundKeywords.add(k)
}
})
// Also look for explicit list items under "Tech" header
// (Simplified: just use keywords for robustness)
techStack = Array.from(foundKeywords)
// 3. Extract Core Logic (Summary)
// Look for "Core Logic", "Architecture", "Introduction" headers
const logicRegex = /##\s*(Core Logic|Architecture|Introduction|About|Features)([\s\S]*?)(##|$)/i
const logicMatch = content.match(logicRegex)
if (logicMatch && logicMatch[2]) {
coreLogic = logicMatch[2].trim().substring(0, 300) + (logicMatch[2].length > 300 ? '...' : '')
} else {
// Fallback: First paragraph after title
const paragraphs = content.split(/\n\s*\n/)
if (paragraphs.length > 1 && paragraphs[1]) {
coreLogic = paragraphs[1].trim().substring(0, 200) + '...'
}
}
return {
title,
techStack,
coreLogic,
htmlContent: marked.parse(content)
}
}
const parsedNodes = computed(() => {
return nodes.value.map(node => {
const meta = extractMetadata(node.content)
return {
...node,
extractedTitle: meta.title,
techStack: meta.techStack,
coreLogic: meta.coreLogic,
htmlContent: meta.htmlContent
}
})
})
const fetchNodes = async () => {
try {
const res = await axios.get('http://127.0.0.1:8000/context_nodes/')
nodes.value = res.data
} catch (error) {
message.error('获取认知节点失败')
}
}
const createNode = async () => {
try {
const res = await axios.post('http://127.0.0.1:8000/context_nodes/', formValue.value)
message.success('认知已提交!')
nodes.value.push(res.data)
showModal.value = false
formValue.value.content = ''
} catch (error) {
message.error('创建认知节点失败')
}
}
onMounted(fetchNodes)
</script>
<style scoped>
.glass-card {
background: rgba(30, 41, 59, 0.7);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.node-header {
display: flex;
align-items: center;
}
.node-title {
font-weight: bold;
font-size: 16px;
color: #e2e8f0;
}
/* Insights Styling */
.insight-block {
background: rgba(99, 226, 183, 0.1);
border: 1px solid rgba(99, 226, 183, 0.2);
}
.insight-item {
margin-bottom: 12px;
}
.insight-label {
font-size: 11px;
color: #63e2b7;
font-weight: bold;
margin-bottom: 4px;
text-transform: uppercase;
}
.insight-value {
font-size: 14px;
color: #e2e8f0;
font-style: italic;
padding-left: 8px;
border-left: 2px solid #63e2b7;
}
.insight-specs {
margin-top: 12px;
}
.spec-card {
background: rgba(0, 0, 0, 0.3);
padding: 8px;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
.spec-key {
font-size: 12px;
color: #94a3b8;
}
.spec-val {
font-size: 12px;
color: #63e2b7;
font-weight: bold;
font-family: monospace;
}
.section-block {
margin-bottom: 16px;
background: rgba(0, 0, 0, 0.2);
padding: 12px;
border-radius: 8px;
}
.section-title {
font-size: 12px;
color: #94a3b8;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.core-logic-text {
font-size: 14px;
color: #cbd5e1;
line-height: 1.6;
}
.markdown-body {
color: #cbd5e1;
font-size: 14px;
line-height: 1.6;
}
.markdown-body :deep(h1),
.markdown-body :deep(h2),
.markdown-body :deep(h3) {
color: #e2e8f0;
margin-top: 16px;
margin-bottom: 8px;
}
.markdown-body :deep(p) {
margin-bottom: 12px;
}
.markdown-body :deep(ul),
.markdown-body :deep(ol) {
padding-left: 20px;
margin-bottom: 12px;
}
.markdown-body :deep(code) {
background: rgba(255, 255, 255, 0.1);
padding: 2px 4px;
border-radius: 4px;
font-family: monospace;
}
.markdown-body :deep(pre) {
background: #0f172a;
padding: 12px;
border-radius: 8px;
overflow-x: auto;
}
</style>