create new nodes
This commit is contained in:
parent
508a7cbf5e
commit
a7e4e82765
9 changed files with 186 additions and 44 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -625,7 +625,7 @@ dependencies = [
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"fast2s",
|
"fast2s",
|
||||||
"graph",
|
"graph",
|
||||||
"itertools",
|
"itertools 0.12.1",
|
||||||
"jieba-rs",
|
"jieba-rs",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
|
@ -2017,6 +2017,15 @@ dependencies = [
|
||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "0.4.8"
|
version = "0.4.8"
|
||||||
|
@ -2781,6 +2790,7 @@ dependencies = [
|
||||||
"csv",
|
"csv",
|
||||||
"dirs",
|
"dirs",
|
||||||
"futures",
|
"futures",
|
||||||
|
"itertools 0.13.0",
|
||||||
"miette",
|
"miette",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
@ -18,4 +18,5 @@
|
||||||
justify-items: stretch;
|
justify-items: stretch;
|
||||||
|
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
|
@ -11,14 +11,18 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import TimeAgo from "javascript-time-ago";
|
import TimeAgo from "javascript-time-ago";
|
||||||
import en from "javascript-time-ago/locale/en";
|
import en from "javascript-time-ago/locale/en";
|
||||||
import Sidebar from "./components/Sidebar";
|
import Sidebar from "./components/Sidebar";
|
||||||
|
import { atom, useAtom } from "jotai";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
TimeAgo.addDefaultLocale(en);
|
TimeAgo.addDefaultLocale(en);
|
||||||
|
|
||||||
function App() {
|
export const nodesOpenedAtom = atom<string[]>([]);
|
||||||
const [nodesOpened, setNodesOpened] = useState<string[]>(() => []);
|
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [nodesOpened, setNodesOpened] = useAtom(nodesOpenedAtom);
|
||||||
|
|
||||||
|
// Open today's journal entry if it's not already opened
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
console.log("ndoes", nodesOpened);
|
console.log("ndoes", nodesOpened);
|
||||||
|
|
|
@ -5,11 +5,34 @@ import { getVersion } from "@tauri-apps/api/app";
|
||||||
import ListIcon from "@mui/icons-material/List";
|
import ListIcon from "@mui/icons-material/List";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { sidebarExpandedAtom } from "./Sidebar";
|
import { sidebarExpandedAtom } from "./Sidebar";
|
||||||
|
import { nodesOpenedAtom } from "../App";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
const version = await getVersion();
|
const version = await getVersion();
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
|
const setNodesOpened = useSetAtom(nodesOpenedAtom);
|
||||||
const setSidebarExpanded = useSetAtom(sidebarExpandedAtom);
|
const setSidebarExpanded = useSetAtom(sidebarExpandedAtom);
|
||||||
|
|
||||||
|
const createNewJournalPage = useCallback(() => {
|
||||||
|
(async () => {
|
||||||
|
const resp = await fetch("http://localhost:5195/node", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
type: "panorama/journal/page",
|
||||||
|
extra_data: {
|
||||||
|
"panorama/journal/page/content": "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
setNodesOpened((prev) => [data.node_id, ...prev]);
|
||||||
|
})();
|
||||||
|
}, [setNodesOpened]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.Header}>
|
<div className={styles.Header}>
|
||||||
|
@ -23,7 +46,7 @@ export default function Header() {
|
||||||
<span className={styles.title}>Panorama</span>
|
<span className={styles.title}>Panorama</span>
|
||||||
<span className={styles.version}>v{version}</span>
|
<span className={styles.version}>v{version}</span>
|
||||||
</div>
|
</div>
|
||||||
<button type="button">
|
<button type="button" onClick={createNewJournalPage}>
|
||||||
<NoteAddIcon />
|
<NoteAddIcon />
|
||||||
</button>
|
</button>
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
resize: horizontal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
@ -20,6 +22,11 @@
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 2px 12px;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
|
|
@ -85,6 +85,8 @@ export default function NodeDisplay({ id }: NodeDisplayProps) {
|
||||||
<>Status: {status}</>
|
<>Status: {status}</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.footer}>{id}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -93,7 +95,7 @@ function NodeDisplayHeaderLoaded({ id, data }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
Type {data.type} · Last updated{" "}
|
Type {data.type} · Last updated{" "}
|
||||||
<ReactTimeAgo date={data.created_at * 1000} /> · {id}
|
<ReactTimeAgo date={data.created_at * 1000} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ cozo = { version = "0.7.6", features = ["storage-rocksdb"] }
|
||||||
csv = "1.3.0"
|
csv = "1.3.0"
|
||||||
dirs = "5.0.1"
|
dirs = "5.0.1"
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
|
itertools = "0.13.0"
|
||||||
miette = "5.5.0"
|
miette = "5.5.0"
|
||||||
serde = { version = "1.0.202", features = ["derive"] }
|
serde = { version = "1.0.202", features = ["derive"] }
|
||||||
serde_json = "1.0.117"
|
serde_json = "1.0.117"
|
||||||
|
|
|
@ -59,7 +59,7 @@ async fn main() -> Result<()> {
|
||||||
let state = AppState { db };
|
let state = AppState { db };
|
||||||
|
|
||||||
let cors = CorsLayer::new()
|
let cors = CorsLayer::new()
|
||||||
.allow_methods([Method::GET, Method::POST])
|
.allow_methods([Method::GET, Method::POST, Method::PUT])
|
||||||
.allow_headers(cors::Any)
|
.allow_headers(cors::Any)
|
||||||
.allow_origin(cors::Any);
|
.allow_origin(cors::Any);
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
use cozo::{DataValue, ScriptMutability, Vector};
|
use cozo::{DataValue, DbInstance, MultiTransaction, ScriptMutability, Vector};
|
||||||
|
use itertools::Itertools;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{error::AppResult, AppState};
|
use crate::{error::AppResult, AppState};
|
||||||
|
|
||||||
|
@ -60,7 +62,7 @@ pub async fn get_node(
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct UpdateData {
|
pub struct UpdateData {
|
||||||
title: Option<String>,
|
title: Option<String>,
|
||||||
extra_data: Option<HashMap<String, Value>>,
|
extra_data: Option<ExtraData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_node(
|
pub async fn update_node(
|
||||||
|
@ -92,28 +94,7 @@ pub async fn update_node(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(extra_data) = update_data.extra_data {
|
if let Some(extra_data) = update_data.extra_data {
|
||||||
let result = tx.run_script(
|
let result = get_rows_for_extra_keys(&tx, &extra_data)?;
|
||||||
"
|
|
||||||
?[key, relation, field_name, type] :=
|
|
||||||
*fqkey_to_dbkey{key, relation, field_name, type},
|
|
||||||
is_in(key, $keys)
|
|
||||||
",
|
|
||||||
btmap! {
|
|
||||||
"keys".to_owned() => DataValue::List(
|
|
||||||
extra_data
|
|
||||||
.keys()
|
|
||||||
.map(|s| DataValue::from(s.as_str()))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let s = |s: &DataValue| s.get_str().unwrap().to_owned();
|
|
||||||
let result = result
|
|
||||||
.rows
|
|
||||||
.into_iter()
|
|
||||||
.map(|row| (s(&row[0]), (s(&row[1]), s(&row[2]), s(&row[3]))))
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
|
|
||||||
for (key, (relation, field_name, ty)) in result.iter() {
|
for (key, (relation, field_name, ty)) in result.iter() {
|
||||||
let new_value = extra_data.get(key).unwrap();
|
let new_value = extra_data.get(key).unwrap();
|
||||||
|
@ -127,7 +108,7 @@ pub async fn update_node(
|
||||||
:update {relation} {{ node_id, {field_name} }}
|
:update {relation} {{ node_id, {field_name} }}
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
println!("QUERY: {query:?}");
|
|
||||||
let result = tx.run_script(
|
let result = tx.run_script(
|
||||||
&query,
|
&query,
|
||||||
btmap! {
|
btmap! {
|
||||||
|
@ -135,8 +116,6 @@ pub async fn update_node(
|
||||||
"input_data".to_owned() => new_value,
|
"input_data".to_owned() => new_value,
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
println!("RESULT: {result:?}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,8 +143,85 @@ pub async fn node_types() -> AppResult<Json<Value>> {
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_node() -> AppResult<()> {
|
#[derive(Debug, Deserialize)]
|
||||||
Ok(())
|
pub struct CreateNodeOpts {
|
||||||
|
// TODO: Allow submitting a string
|
||||||
|
// id: Option<String>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
ty: String,
|
||||||
|
extra_data: Option<ExtraData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_node(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(opts): Json<CreateNodeOpts>,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
|
let node_id = Uuid::now_v7();
|
||||||
|
let node_id = node_id.to_string();
|
||||||
|
println!("Opts: {opts:?}");
|
||||||
|
|
||||||
|
let tx = state.db.multi_transaction(true);
|
||||||
|
|
||||||
|
let result = tx.run_script(
|
||||||
|
"
|
||||||
|
?[id, type] <- [[$node_id, $type]]
|
||||||
|
:put node { id, type }
|
||||||
|
",
|
||||||
|
btmap! {
|
||||||
|
"node_id".to_owned() => DataValue::from(node_id.clone()),
|
||||||
|
"type".to_owned() => DataValue::from(opts.ty),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(extra_data) = opts.extra_data {
|
||||||
|
let result = get_rows_for_extra_keys(&tx, &extra_data)?;
|
||||||
|
|
||||||
|
let result_by_relation = result
|
||||||
|
.iter()
|
||||||
|
.into_group_map_by(|(key, (relation, field_name, ty))| relation);
|
||||||
|
println!("Result by relation: {result_by_relation:?}");
|
||||||
|
|
||||||
|
for (relation, fields) in result_by_relation.iter() {
|
||||||
|
let fields_mapping = fields
|
||||||
|
.into_iter()
|
||||||
|
.map(|(key, (_, field_name, _))| {
|
||||||
|
let new_value = extra_data.get(*key).unwrap();
|
||||||
|
// TODO: Make this more generic
|
||||||
|
let new_value = DataValue::from(new_value.as_str().unwrap());
|
||||||
|
(field_name.to_owned(), new_value)
|
||||||
|
})
|
||||||
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
|
||||||
|
let keys = fields_mapping.keys().collect::<Vec<_>>();
|
||||||
|
let keys_joined = keys.iter().join(", ");
|
||||||
|
|
||||||
|
let query = format!(
|
||||||
|
"
|
||||||
|
?[ node_id, {keys_joined} ] <- [$input_data]
|
||||||
|
:insert {relation} {{ node_id, {keys_joined} }}
|
||||||
|
"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut params = vec![];
|
||||||
|
params.push(DataValue::from(node_id.clone()));
|
||||||
|
for key in keys {
|
||||||
|
params.push(fields_mapping[key].clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = tx.run_script(
|
||||||
|
&query,
|
||||||
|
btmap! {
|
||||||
|
"input_data".to_owned() => DataValue::List(params),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.commit()?;
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"node_id": node_id,
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -179,13 +235,17 @@ pub async fn search_nodes(
|
||||||
) -> AppResult<Json<Value>> {
|
) -> AppResult<Json<Value>> {
|
||||||
let results = state.db.run_script(
|
let results = state.db.run_script(
|
||||||
"
|
"
|
||||||
?[node_id, content, score] := ~journal:text_index {node_id, content, |
|
results[node_id, content, score] := ~journal:text_index {node_id, content, |
|
||||||
query: $q,
|
query: $q,
|
||||||
k: 10,
|
k: 10,
|
||||||
score_kind: 'tf_idf',
|
score_kind: 'tf_idf',
|
||||||
bind_score: score
|
bind_score: score
|
||||||
}
|
}
|
||||||
|
|
||||||
|
?[node_id, content, title, score] :=
|
||||||
|
results[node_id, content, score],
|
||||||
|
*node{ id: node_id, title }
|
||||||
|
|
||||||
:order -score
|
:order -score
|
||||||
",
|
",
|
||||||
btmap! {
|
btmap! {
|
||||||
|
@ -201,7 +261,8 @@ pub async fn search_nodes(
|
||||||
json!({
|
json!({
|
||||||
"node_id": row[0].get_str().unwrap(),
|
"node_id": row[0].get_str().unwrap(),
|
||||||
"content": row[1].get_str().unwrap(),
|
"content": row[1].get_str().unwrap(),
|
||||||
"score": row[2].get_float().unwrap(),
|
"title": row[2].get_str().unwrap(),
|
||||||
|
"score": row[3].get_float().unwrap(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
@ -210,3 +271,36 @@ pub async fn search_nodes(
|
||||||
"results": results
|
"results": results
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExtraData = HashMap<String, Value>;
|
||||||
|
|
||||||
|
fn get_rows_for_extra_keys(
|
||||||
|
tx: &MultiTransaction,
|
||||||
|
extra_data: &ExtraData,
|
||||||
|
) -> AppResult<HashMap<String, (String, String, String)>> {
|
||||||
|
let result = tx.run_script(
|
||||||
|
"
|
||||||
|
?[key, relation, field_name, type] :=
|
||||||
|
*fqkey_to_dbkey{key, relation, field_name, type},
|
||||||
|
is_in(key, $keys)
|
||||||
|
",
|
||||||
|
btmap! {
|
||||||
|
"keys".to_owned() => DataValue::List(
|
||||||
|
extra_data
|
||||||
|
.keys()
|
||||||
|
.map(|s| DataValue::from(s.as_str()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let s = |s: &DataValue| s.get_str().unwrap().to_owned();
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
result
|
||||||
|
.rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| (s(&row[0]), (s(&row[1]), s(&row[2]), s(&row[3]))))
|
||||||
|
.collect::<HashMap<_, _>>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue