mirror of
https://github.com/perstarkse/minne.git
synced 2026-04-25 02:08:30 +02:00
feat: view graph entities, relations and visualization
This commit is contained in:
4058
assets/style.css
4058
assets/style.css
File diff suppressed because one or more lines are too long
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
24
templates/knowledge/entity_list.html
Normal file
24
templates/knowledge/entity_list.html
Normal 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>
|
||||||
89
templates/knowledge/relationship_table.html
Normal file
89
templates/knowledge/relationship_table.html
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user