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::Session;
use axum_session_auth::AuthSession; use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool; 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 surrealdb::{engine::any::Any, Surreal};
use tokio::join; use tokio::join;
use tracing::info; use tracing::info;
@@ -54,46 +59,79 @@ pub async fn show_knowledge_page(
.await .await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?; .map_err(|e| HtmlError::new(e, state.templates.clone()))?;
// In your handler function
let mut plot = Plot::new(); let mut plot = Plot::new();
// Create node positions (you might want to use a proper layout algorithm) // Fibonacci sphere distribution
let node_x: Vec<f64> = entities.iter().enumerate().map(|(i, _)| i as f64).collect(); let node_count = entities.len();
let node_y: Vec<f64> = vec![0.0; entities.len()]; let golden_ratio = (1.0 + 5.0_f64.sqrt()) / 2.0;
let node_text: Vec<String> = entities.iter().map(|e| e.description.clone()).collect(); 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 node_x: Vec<f64> = node_positions.iter().map(|(x, _, _)| *x).collect();
let nodes = Scatter::new(node_x.clone(), node_y.clone()) let node_y: Vec<f64> = node_positions.iter().map(|(_, y, _)| *y).collect();
.mode(plotly::common::Mode::Markers) let node_z: Vec<f64> = node_positions.iter().map(|(_, _, z)| *z).collect();
.text_array(node_text)
.name("Entities")
.hover_template("%{text}");
// Add edges // Nodes trace
let mut edge_x = Vec::new(); let nodes = Scatter3D::new(node_x.clone(), node_y.clone(), node_z.clone())
let mut edge_y = Vec::new(); .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 { for rel in &relationships {
let from_idx = entities.iter().position(|e| e.id == rel.out).unwrap_or(0); 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); 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]); let edge_x = vec![node_x[from_idx], node_x[to_idx]];
edge_y.extend_from_slice(&[0.0, 0.0, std::f64::NAN]); 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); plot.add_trace(nodes);
// Layout
let layout = Layout::new() 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) .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); plot.set_layout(layout);
// Convert to HTML // Convert to HTML
let html = plot.to_html(); let html = plot.to_html();

View File

@@ -29,12 +29,12 @@
machines, with different resource requirements:</p> machines, with different resource requirements:</p>
<ul> <ul>
<li> <li>
<strong>Server:</strong> Lightweight, using roughly 50MB of RAM. A minimum of 1 core and 256MB of RAM is <strong>Server:</strong> Lightweight. A minimum of 1 core and 256MB of RAM is recommended.
recommended.
</li> </li>
<li> <li>
<strong>Worker:</strong> Handles content parsing, typically consuming about 60MB of RAM—occasionally peaking up <strong>Worker:</strong> Handles content parsing and creation of database entities. It's recommended to allocate at
to 1GB. We recommend allocating at least 2 cores and 1024MB of RAM. least two cores and 1024 MB RAM. It will run on less but might run into constraints depending on the content being
parsed.
</li> </li>
</ul> </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"> 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 Simplify Your Knowledge Management
</h1> </h1>
<p class="text-xl text-base-content/70"> <p class="text-xl ">
Capture, connect, and retrieve your knowledge effortlessly with Minne Capture, connect, and retrieve your knowledge effortlessly with Minne
</p> </p>
@@ -15,21 +15,21 @@
<div class="card bg-base-100 shadow-hover"> <div class="card bg-base-100 shadow-hover">
<div class="card-body items-center"> <div class="card-body items-center">
<div class="skeleton h-32 w-32 rounded-full"></div> <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> <p>Save anything instantly - texts, links, images, and more</p>
</div> </div>
</div> </div>
<div class="card bg-base-100 shadow-hover"> <div class="card bg-base-100 shadow-hover">
<div class="card-body items-center"> <div class="card-body items-center">
<div class="skeleton h-32 w-32 rounded-full"></div> <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> <p>AI-powered content analysis and organization</p>
</div> </div>
</div> </div>
<div class="card bg-base-100 shadow-hover"> <div class="card bg-base-100 shadow-hover">
<div class="card-body items-center"> <div class="card-body items-center">
<div class="skeleton h-32 w-32 rounded-full"></div> <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> <p>Visualize connections between your ideas</p>
</div> </div>
</div> </div>

View File

@@ -1,17 +1,18 @@
{% extends 'body_base.html' %} {% extends 'body_base.html' %}
{% block main %} {% 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"> <div class="container">
{{plot_html|safe}} <h2 class="text-2xl font-bold mb-2">Entities</h2>
<h2>Entities</h2> {% include "knowledge/entity_list.html" %}
{% for entity in entities %}
<p>{{entity.id}} - {{entity.description}} </p>
{% endfor %}
<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> </div>
</main> </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="container flex mx-auto">
<div class="flex-1 flex items-center"> <div class="flex-1 flex items-center">
<a class="text-2xl text-primary font-bold" href="/" hx-boost="true">Minne</a> <a class="text-2xl text-primary font-bold" href="/" hx-boost="true">Minne</a>