Spaces:
Running
Running
Download web/src/views/GraphView.vue from Benaah/MicProtocol: direct link, hf CLI and curl.
- Browser
- Download file 7.88 kB
-
https://huggingface.co/spaces/Benaah/MicProtocol/resolve/main/web/src/views/GraphView.vue
- Command line
-
hf download hf://spaces/Benaah/MicProtocol/web/src/views/GraphView.vue
-
curl -L -o GraphView.vue https://huggingface.co/spaces/Benaah/MicProtocol/resolve/main/web/src/views/GraphView.vue
7.88 kB
| <template> | |
| <div class="graph-view"> | |
| <div class="graph-header"> | |
| <h1>Knowledge Graph</h1> | |
| <p>Explore relationships between auditees, findings, recommendations, and legal frameworks.</p> | |
| </div> | |
| <!-- Query Panel --> | |
| <div class="query-panel"> | |
| <n-card title="Graph Queries" :bordered="false"> | |
| <div class="query-grid"> | |
| <n-button | |
| v-for="query in presetQueries" | |
| :key="query.id" | |
| quaternary | |
| class="query-btn" | |
| @click="runQuery(query.cypher)" | |
| > | |
| <template #icon> | |
| <n-icon><search-outline /></n-icon> | |
| </template> | |
| {{ query.label }} | |
| </n-button> | |
| </div> | |
| <n-divider /> | |
| <n-input | |
| v-model:value="customQuery" | |
| type="textarea" | |
| placeholder="Enter Cypher query..." | |
| :autosize="{ minRows: 3, maxRows: 6 }" | |
| class="cypher-input" | |
| /> | |
| <div class="query-actions"> | |
| <n-button type="primary" @click="runQuery(customQuery)" :loading="isLoading"> | |
| <template #icon><n-icon><play-outline /></n-icon></template> | |
| Run Query | |
| </n-button> | |
| <n-button quaternary @click="customQuery = ''"> | |
| Clear | |
| </n-button> | |
| </div> | |
| </n-card> | |
| </div> | |
| <!-- Graph Visualization --> | |
| <div class="graph-visualization"> | |
| <n-card :bordered="false" class="graph-card"> | |
| <div v-if="isLoading" class="graph-loading"> | |
| <n-spin size="large" /> | |
| <p>Loading graph data...</p> | |
| </div> | |
| <div v-else-if="graphData.nodes.length === 0" class="graph-empty"> | |
| <n-empty description="Run a query to visualize the knowledge graph"> | |
| <template #icon> | |
| <n-icon size="48"><share-social-outline /></n-icon> | |
| </template> | |
| </n-empty> | |
| </div> | |
| <div v-else ref="graphContainer" class="graph-container"> | |
| <v-chart class="chart" :option="chartOption" autoresize /> | |
| </div> | |
| </n-card> | |
| </div> | |
| <!-- Results Panel --> | |
| <div v-if="queryResults.length > 0" class="results-panel"> | |
| <n-card title="Query Results" :bordered="false"> | |
| <n-data-table | |
| :columns="resultColumns" | |
| :data="queryResults" | |
| :pagination="{ pageSize: 10 }" | |
| size="small" | |
| /> | |
| </n-card> | |
| </div> | |
| </div> | |
| </template> | |
| <script setup lang="ts"> | |
| import { ref, computed } from 'vue' | |
| import { use } from 'echarts/core' | |
| import { CanvasRenderer } from 'echarts/renderers' | |
| import { GraphChart } from 'echarts/charts' | |
| import { TooltipComponent, LegendComponent } from 'echarts/components' | |
| import VChart from 'vue-echarts' | |
| import { api } from '@/api/client' | |
| import type { GraphData, GraphNode, GraphEdge } from '@/types' | |
| import { SearchOutline, PlayOutline, ShareSocialOutline } from '@vicons/ionicons5' | |
| use([CanvasRenderer, GraphChart, TooltipComponent, LegendComponent]) | |
| const isLoading = ref(false) | |
| const customQuery = ref('') | |
| const graphData = ref<GraphData>({ nodes: [], edges: [] }) | |
| const queryResults = ref<any[]>([]) | |
| const presetQueries = [ | |
| { | |
| id: 'pending-recs', | |
| label: 'Pending Recommendations by Ministry', | |
| cypher: 'MATCH (a:Auditee)<-[:AUDITS]-(r:AuditReport)-[:CONTAINS]->(f:Finding)-[:RECOMMENDS]->(rec:Recommendation) WHERE rec.status = "Pending" RETURN a.name, rec.description, rec.deadline ORDER BY rec.deadline', | |
| }, | |
| { | |
| id: 'county-findings', | |
| label: 'Findings by County (2023/24)', | |
| cypher: 'MATCH (a:Auditee)-[:AUDITS]-(r:AuditReport)-[:CONTAINS]->(f:Finding) WHERE r.fiscal_year = "2023/24" AND a.type = "county" RETURN a.name, count(f) as findings ORDER BY findings DESC', | |
| }, | |
| { | |
| id: 'legal-refs', | |
| label: 'Most Cited Legal Provisions', | |
| cypher: 'MATCH (f:Finding)-[:CITES_LAW]->(l:LegalProvision) RETURN l.section, count(f) as citations ORDER BY citations DESC LIMIT 10', | |
| }, | |
| { | |
| id: 'adverse-opinions', | |
| label: 'Adverse/Disclaimer Opinions', | |
| cypher: 'MATCH (r:AuditReport) WHERE r.audit_opinion IN ["adverse", "disclaimer"] RETURN r.title, r.auditee, r.fiscal_year, r.audit_opinion', | |
| }, | |
| { | |
| id: 'amount-entities', | |
| label: 'Largest Financial Irregularities', | |
| cypher: 'MATCH (f:Finding)-[:INVOLVES_AMOUNT]->(a:FinancialAmount) RETURN f.description, a.value, a.currency ORDER BY a.value DESC LIMIT 20', | |
| }, | |
| ] | |
| const chartOption = computed(() => ({ | |
| tooltip: { | |
| trigger: 'item', | |
| formatter: (params: any) => { | |
| if (params.dataType === 'node') { | |
| return `<strong>${params.data.label}</strong><br/>Type: ${params.data.type}` | |
| } | |
| return `${params.data.source} -> ${params.data.target}` | |
| }, | |
| }, | |
| legend: { | |
| data: ['Auditee', 'Report', 'Finding', 'Legal', 'Amount'], | |
| bottom: 0, | |
| }, | |
| series: [ | |
| { | |
| type: 'graph', | |
| layout: 'force', | |
| data: graphData.value.nodes.map((n: GraphNode) => ({ | |
| id: n.id, | |
| name: n.label, | |
| label: n.label, | |
| type: n.type, | |
| symbolSize: n.type === 'auditee' ? 40 : n.type === 'report' ? 30 : 20, | |
| itemStyle: { | |
| color: getNodeColor(n.type), | |
| }, | |
| category: n.type, | |
| })), | |
| links: graphData.value.edges.map((e: GraphEdge) => ({ | |
| source: e.source, | |
| target: e.target, | |
| label: { | |
| show: true, | |
| formatter: e.type, | |
| }, | |
| })), | |
| categories: [ | |
| { name: 'Auditee' }, | |
| { name: 'Report' }, | |
| { name: 'Finding' }, | |
| { name: 'Legal' }, | |
| { name: 'Amount' }, | |
| ], | |
| roam: true, | |
| label: { | |
| show: true, | |
| position: 'right', | |
| fontSize: 12, | |
| }, | |
| force: { | |
| repulsion: 300, | |
| edgeLength: 100, | |
| }, | |
| emphasis: { | |
| focus: 'adjacency', | |
| lineStyle: { | |
| width: 4, | |
| }, | |
| }, | |
| }, | |
| ], | |
| })) | |
| const resultColumns = [ | |
| { title: 'Property', key: 'property' }, | |
| { title: 'Value', key: 'value' }, | |
| ] | |
| const getNodeColor = (type: string) => { | |
| const colors: Record<string, string> = { | |
| auditee: '#1a5fb4', | |
| report: '#26a269', | |
| finding: '#e5a50a', | |
| legal: '#c01c28', | |
| amount: '#9b59b6', | |
| } | |
| return colors[type] || '#7f8c8d' | |
| } | |
| const runQuery = async (cypher: string) => { | |
| if (!cypher.trim()) return | |
| isLoading.value = true | |
| try { | |
| const response = await api.graphQuery(cypher) | |
| graphData.value = response | |
| queryResults.value = response.nodes.map((n: GraphNode) => ({ | |
| property: n.label, | |
| value: JSON.stringify(n.properties), | |
| })) | |
| } catch (error) { | |
| console.error('Graph query failed:', error) | |
| } finally { | |
| isLoading.value = false | |
| } | |
| } | |
| </script> | |
| <style scoped lang="scss"> | |
| .graph-view { | |
| padding-bottom: 48px; | |
| } | |
| .graph-header { | |
| margin-bottom: 32px; | |
| h1 { | |
| font-size: 28px; | |
| font-weight: 700; | |
| margin-bottom: 8px; | |
| } | |
| p { | |
| color: var(--text-secondary); | |
| font-size: 15px; | |
| } | |
| } | |
| .query-panel { | |
| margin-bottom: 24px; | |
| } | |
| .query-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); | |
| gap: 8px; | |
| margin-bottom: 16px; | |
| } | |
| .query-btn { | |
| justify-content: flex-start; | |
| text-align: left; | |
| height: auto; | |
| padding: 8px 12px; | |
| font-size: 13px; | |
| } | |
| .cypher-input { | |
| font-family: 'SF Mono', Monaco, monospace; | |
| font-size: 13px; | |
| } | |
| .query-actions { | |
| display: flex; | |
| gap: 12px; | |
| margin-top: 12px; | |
| } | |
| .graph-visualization { | |
| margin-bottom: 24px; | |
| } | |
| .graph-card { | |
| min-height: 500px; | |
| } | |
| .graph-loading, | |
| .graph-empty { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| height: 500px; | |
| gap: 16px; | |
| } | |
| .graph-container { | |
| height: 500px; | |
| } | |
| .chart { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .results-panel { | |
| margin-top: 24px; | |
| } | |
| @media (max-width: 768px) { | |
| .query-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| </style> | |