feat: view graph entities, relations and visualization

This commit is contained in:
Per Stark
2025-02-10 10:49:54 +01:00
parent 6dcae4471b
commit 85b6291a46
8 changed files with 197 additions and 4101 deletions

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,12 @@ use axum::{
use axum_session::Session;
use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use plotly::{Configuration, Layout, Plot, Scatter};
use futures::SinkExt;
use plotly::{
common::{Line, Marker, Mode},
layout::{Axis, Camera, LayoutScene, ProjectionType},
Configuration, Layout, Plot, Scatter, Scatter3D,
};
use surrealdb::{engine::any::Any, Surreal};
use tokio::join;
use tracing::info;
@@ -54,46 +59,79 @@ pub async fn show_knowledge_page(
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
// In your handler function
let mut plot = Plot::new();
// Create node positions (you might want to use a proper layout algorithm)
let node_x: Vec<f64> = entities.iter().enumerate().map(|(i, _)| i as f64).collect();
let node_y: Vec<f64> = vec![0.0; entities.len()];
let node_text: Vec<String> = entities.iter().map(|e| e.description.clone()).collect();
// Fibonacci sphere distribution
let node_count = entities.len();
let golden_ratio = (1.0 + 5.0_f64.sqrt()) / 2.0;
let node_positions: Vec<(f64, f64, f64)> = (0..node_count)
.map(|i| {
let i = i as f64;
let theta = 2.0 * std::f64::consts::PI * i / golden_ratio;
let phi = (1.0 - 2.0 * (i + 0.5) / node_count as f64).acos();
let x = phi.sin() * theta.cos();
let y = phi.sin() * theta.sin();
let z = phi.cos();
(x, y, z)
})
.collect();
// Add nodes
let nodes = Scatter::new(node_x.clone(), node_y.clone())
.mode(plotly::common::Mode::Markers)
.text_array(node_text)
.name("Entities")
.hover_template("%{text}");
let node_x: Vec<f64> = node_positions.iter().map(|(x, _, _)| *x).collect();
let node_y: Vec<f64> = node_positions.iter().map(|(_, y, _)| *y).collect();
let node_z: Vec<f64> = node_positions.iter().map(|(_, _, z)| *z).collect();
// Add edges
let mut edge_x = Vec::new();
let mut edge_y = Vec::new();
// Nodes trace
let nodes = Scatter3D::new(node_x.clone(), node_y.clone(), node_z.clone())
.mode(Mode::Markers)
.marker(Marker::new().size(8).color("#1f77b4"))
.text_array(
entities
.iter()
.map(|e| e.description.clone())
.collect::<Vec<_>>(),
)
.hover_template("Entity: %{text}<br>");
// Edges traces
for rel in &relationships {
let from_idx = entities.iter().position(|e| e.id == rel.out).unwrap_or(0);
let to_idx = entities.iter().position(|e| e.id == rel.in_).unwrap_or(0);
edge_x.extend_from_slice(&[from_idx as f64, to_idx as f64, std::f64::NAN]);
edge_y.extend_from_slice(&[0.0, 0.0, std::f64::NAN]);
let edge_x = vec![node_x[from_idx], node_x[to_idx]];
let edge_y = vec![node_y[from_idx], node_y[to_idx]];
let edge_z = vec![node_z[from_idx], node_z[to_idx]];
let edge_trace = Scatter3D::new(edge_x, edge_y, edge_z)
.mode(Mode::Lines)
.line(Line::new().color("#888").width(2.0))
.hover_template(&format!(
"Relationship: {}<br>",
rel.metadata.relationship_type
))
.show_legend(false);
plot.add_trace(edge_trace);
}
let edges = Scatter::new(edge_x, edge_y)
.mode(plotly::common::Mode::Lines)
.name("Relationships");
plot.add_trace(edges);
plot.add_trace(nodes);
// Layout
let layout = Layout::new()
.title("Knowledge Graph")
.scene(
LayoutScene::new()
.x_axis(Axis::new().visible(false))
.y_axis(Axis::new().visible(false))
.z_axis(Axis::new().visible(false))
.camera(
Camera::new()
.projection(ProjectionType::Perspective.into())
.eye((1.5, 1.5, 1.5).into()),
),
)
.show_legend(false)
.height(600);
.paper_background_color("rbga(250,100,0,0)")
.plot_background_color("rbga(0,0,0,0)");
plot.set_layout(layout);
// Convert to HTML
let html = plot.to_html();

View File

@@ -29,12 +29,12 @@
machines, with different resource requirements:</p>
<ul>
<li>
<strong>Server:</strong> Lightweight, using roughly 50MB of RAM. A minimum of 1 core and 256MB of RAM is
recommended.
<strong>Server:</strong> Lightweight. A minimum of 1 core and 256MB of RAM is recommended.
</li>
<li>
<strong>Worker:</strong> Handles content parsing, typically consuming about 60MB of RAM—occasionally peaking up
to 1GB. We recommend allocating at least 2 cores and 1024MB of RAM.
<strong>Worker:</strong> Handles content parsing and creation of database entities. It's recommended to allocate at
least two cores and 1024 MB RAM. It will run on less but might run into constraints depending on the content being
parsed.
</li>
</ul>

View File

@@ -6,7 +6,7 @@
class="text-5xl sm:text-6xl py-4 pt-10 font-extrabold bg-linear-to-r from-primary to-secondary text-transparent bg-clip-text font-satoshi">
Simplify Your Knowledge Management
</h1>
<p class="text-xl text-base-content/70">
<p class="text-xl ">
Capture, connect, and retrieve your knowledge effortlessly with Minne
</p>
@@ -15,21 +15,21 @@
<div class="card bg-base-100 shadow-hover">
<div class="card-body items-center">
<div class="skeleton h-32 w-32 rounded-full"></div>
<h3 class="card-title">Easy Capture</h3>
<h3 class="card-title text-xl">Easy Capture</h3>
<p>Save anything instantly - texts, links, images, and more</p>
</div>
</div>
<div class="card bg-base-100 shadow-hover">
<div class="card-body items-center">
<div class="skeleton h-32 w-32 rounded-full"></div>
<h3 class="card-title">Smart Analysis</h3>
<h3 class="card-title text-xl">Smart Analysis</h3>
<p>AI-powered content analysis and organization</p>
</div>
</div>
<div class="card bg-base-100 shadow-hover">
<div class="card-body items-center">
<div class="skeleton h-32 w-32 rounded-full"></div>
<h3 class="card-title">Knowledge Graph</h3>
<h3 class="card-title text-xl">Knowledge Graph</h3>
<p>Visualize connections between your ideas</p>
</div>
</div>

View File

@@ -1,17 +1,18 @@
{% extends 'body_base.html' %}
{% block main %}
<main class="flex justify-center grow mt-2 sm:mt-4 gap-6">
<main class="flex justify-center grow mt-2 sm:mt-4 gap-6 mb-10">
<div class="container">
{{plot_html|safe}}
<h2>Entities</h2>
{% for entity in entities %}
<p>{{entity.id}} - {{entity.description}} </p>
{% endfor %}
<h2 class="text-2xl font-bold mb-2">Entities</h2>
{% include "knowledge/entity_list.html" %}
<h2 class="mt-10">Relationships</h2>
<p>{{relationships}}</p>
<h2 class="text-2xl font-bold mb-2 mt-10">Relationships</h2>
{% include "knowledge/relationship_table.html" %}
<div class="rounded-box overflow-clip mt-10 shadow">
{{plot_html|safe}}
</div>
</div>
</main>

View File

@@ -0,0 +1,24 @@
<div class="grid sm:grid-cols-2 md:grid-cols-3 gap-4" id="entity_list">
{% for entity in entities %}
<div class="card min-w-72 bg-base-100 shadow-sm">
<div class="card-body">
<h2 class="card-title">{{entity.name}}
<span class="badge badge-xs badge-primary">{{entity.entity_type}}</span>
</h2>
<p>{{entity.description}}</p>
<div class="flex justify-between items-center">
<p>{{entity.updated_at | datetimeformat(format="short", tz=user.timezeone)}}</p>
<div>
<button hx-patch="/knowledge-entity/{{entity.id}}" class="btn btn-square btn-ghost btn-sm">
{% include "icons/edit_icon.html" %}
</button>
<button hx-delete="/knowledge-entity/{{entity.id}}" hx-target="#entity_list" hx-swap="outerHTML"
class="btn btn-square btn-ghost btn-sm">
{% include "icons/delete_icon.html" %}
</button>
</div>
</div>
</div>
</div>
{% endfor %}
</div>

View File

@@ -0,0 +1,89 @@
<div class="overflow-x-auto shadow rounded-box border border-base-content/5 bg-base-100 ">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Origin</th>
<th>Target</th>
<th>Type</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for relationship in relationships %}
<tr>
<td>{{ loop.index }}</td>
<!-- Origin column -->
<td>
{% for entity in entities if entity.id == relationship.in %}
<span class="cursor-pointer tooltip tooltip-info" data-tip="Click for more details"
hx-get="/knowledge-entity/{{entity.id}}" hx-trigger="click" hx-target="#entity_detail_modal"
hx-swap="innerHTML">
{{ entity.name }}
</span>
{% else %}
{{ relationship.in }}
{% endfor %}
</td>
<!-- Target column -->
<td>
{% for entity in entities if entity.id == relationship.out %}
<span class="cursor-pointer tooltip tooltip-info" data-tip="Click for more details"
hx-get="/knowledge-entity/{{entity.id}}" hx-trigger="click" hx-target="#entity_detail_modal"
hx-swap="innerHTML">
{{ entity.name }}
</span>
{% else %}
{{ relationship.out }}
{% endfor %}
</td>
<td>{{ relationship.metadata.relationship_type }}</td>
<td>
<button class="btn btn-sm btn-outline" hx-get="/relationship/{{ relationship.id }}/edit"
hx-target="#modal_container" hx-swap="innerHTML">
Edit
</button>
</td>
</tr>
{% endfor %}
<!-- New linking row -->
<tr id="new_relationship">
<td></td>
<td>
<select name="origin_id" class="select select-bordered w-full new_relationship_input">
<option disabled selected>Select Origin</option>
{% for entity in entities %}
<option value="{{ entity.id }}">{{ entity.name }}</option>
{% endfor %}
</select>
</td>
<td>
<select name="target_id" class="select select-bordered w-full new_relationship_input">
<option disabled selected>Select Target</option>
{% for entity in entities %}
<option value="{{ entity.id }}">{{ entity.name }}</option>
{% endfor %}
</select>
</td>
<td>
<input name="relationship_type" type="text" placeholder="RelatedTo"
class="input input-bordered w-full new_relationship_input" />
</td>
<td>
<button type="button" class="btn btn-primary btn-sm" hx-post="/relationship/create"
hx-target="#relationship_table" hx-swap="outerHTML" hx-include=".new_relationship_input">
Save
</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Modal containers for dynamic content -->
<div id="entity_detail_modal" class="mt-4"></div>
<div id="modal_container"></div>

View File

@@ -1,4 +1,4 @@
<nav class="navbar bg-base-200">
<nav class="navbar bg-base-200 !p-0">
<div class="container flex mx-auto">
<div class="flex-1 flex items-center">
<a class="text-2xl text-primary font-bold" href="/" hx-boost="true">Minne</a>