322 lines
9.0 KiB
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) {
|
|
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) {
|
|
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) {
|
|
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>
|