From a7e4e827656ebd56ae8e8debbfc05b2ae1b044e8 Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Mon, 27 May 2024 01:27:17 -0500 Subject: [PATCH] create new nodes --- Cargo.lock | 12 +- app/src/App.module.scss | 1 + app/src/App.tsx | 8 +- app/src/components/Header.tsx | 25 ++- app/src/components/NodeDisplay.module.scss | 7 + app/src/components/NodeDisplay.tsx | 4 +- crates/panorama-daemon/Cargo.toml | 1 + crates/panorama-daemon/src/main.rs | 2 +- crates/panorama-daemon/src/node.rs | 170 ++++++++++++++++----- 9 files changed, 186 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7200e4f..75e3796 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -625,7 +625,7 @@ dependencies = [ "env_logger", "fast2s", "graph", - "itertools", + "itertools 0.12.1", "jieba-rs", "lazy_static", "log", @@ -2017,6 +2017,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.8" @@ -2781,6 +2790,7 @@ dependencies = [ "csv", "dirs", "futures", + "itertools 0.13.0", "miette", "serde", "serde_json", diff --git a/app/src/App.module.scss b/app/src/App.module.scss index b52331b..864225b 100644 --- a/app/src/App.module.scss +++ b/app/src/App.module.scss @@ -18,4 +18,5 @@ justify-items: stretch; padding: 12px; + gap: 12px; } \ No newline at end of file diff --git a/app/src/App.tsx b/app/src/App.tsx index 6fe1e3a..634417d 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -11,14 +11,18 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import TimeAgo from "javascript-time-ago"; import en from "javascript-time-ago/locale/en"; import Sidebar from "./components/Sidebar"; +import { atom, useAtom } from "jotai"; const queryClient = new QueryClient(); TimeAgo.addDefaultLocale(en); -function App() { - const [nodesOpened, setNodesOpened] = useState(() => []); +export const nodesOpenedAtom = atom([]); +function App() { + const [nodesOpened, setNodesOpened] = useAtom(nodesOpenedAtom); + + // Open today's journal entry if it's not already opened useEffect(() => { (async () => { console.log("ndoes", nodesOpened); diff --git a/app/src/components/Header.tsx b/app/src/components/Header.tsx index 1f48b50..cb85a18 100644 --- a/app/src/components/Header.tsx +++ b/app/src/components/Header.tsx @@ -5,11 +5,34 @@ import { getVersion } from "@tauri-apps/api/app"; import ListIcon from "@mui/icons-material/List"; import { useSetAtom } from "jotai"; import { sidebarExpandedAtom } from "./Sidebar"; +import { nodesOpenedAtom } from "../App"; +import { useCallback } from "react"; const version = await getVersion(); export default function Header() { + const setNodesOpened = useSetAtom(nodesOpenedAtom); 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 ( <>
@@ -23,7 +46,7 @@ export default function Header() { Panorama v{version}
- diff --git a/app/src/components/NodeDisplay.module.scss b/app/src/components/NodeDisplay.module.scss index f3c269f..941e8ae 100644 --- a/app/src/components/NodeDisplay.module.scss +++ b/app/src/components/NodeDisplay.module.scss @@ -9,6 +9,8 @@ display: flex; align-items: stretch; flex-direction: column; + + resize: horizontal; } .header { @@ -20,6 +22,11 @@ font-size: 0.6rem; } +.footer { + padding: 2px 12px; + font-size: 0.6rem; +} + .body { flex-grow: 1; diff --git a/app/src/components/NodeDisplay.tsx b/app/src/components/NodeDisplay.tsx index 0524454..8d4fa12 100644 --- a/app/src/components/NodeDisplay.tsx +++ b/app/src/components/NodeDisplay.tsx @@ -85,6 +85,8 @@ export default function NodeDisplay({ id }: NodeDisplayProps) { <>Status: {status} )} + +
{id}
); } @@ -93,7 +95,7 @@ function NodeDisplayHeaderLoaded({ id, data }) { return ( <> Type {data.type} · Last updated{" "} - · {id} + ); } diff --git a/crates/panorama-daemon/Cargo.toml b/crates/panorama-daemon/Cargo.toml index cfd3f68..c9bc6dc 100644 --- a/crates/panorama-daemon/Cargo.toml +++ b/crates/panorama-daemon/Cargo.toml @@ -13,6 +13,7 @@ cozo = { version = "0.7.6", features = ["storage-rocksdb"] } csv = "1.3.0" dirs = "5.0.1" futures = "0.3.30" +itertools = "0.13.0" miette = "5.5.0" serde = { version = "1.0.202", features = ["derive"] } serde_json = "1.0.117" diff --git a/crates/panorama-daemon/src/main.rs b/crates/panorama-daemon/src/main.rs index e0502e0..e8bd0a4 100644 --- a/crates/panorama-daemon/src/main.rs +++ b/crates/panorama-daemon/src/main.rs @@ -59,7 +59,7 @@ async fn main() -> Result<()> { let state = AppState { db }; let cors = CorsLayer::new() - .allow_methods([Method::GET, Method::POST]) + .allow_methods([Method::GET, Method::POST, Method::PUT]) .allow_headers(cors::Any) .allow_origin(cors::Any); diff --git a/crates/panorama-daemon/src/node.rs b/crates/panorama-daemon/src/node.rs index 43c9f15..147e0e2 100644 --- a/crates/panorama-daemon/src/node.rs +++ b/crates/panorama-daemon/src/node.rs @@ -1,12 +1,14 @@ -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use axum::{ extract::{Path, Query, State}, http::StatusCode, Json, }; -use cozo::{DataValue, ScriptMutability, Vector}; +use cozo::{DataValue, DbInstance, MultiTransaction, ScriptMutability, Vector}; +use itertools::Itertools; use serde_json::Value; +use uuid::Uuid; use crate::{error::AppResult, AppState}; @@ -60,7 +62,7 @@ pub async fn get_node( #[derive(Deserialize, Debug)] pub struct UpdateData { title: Option, - extra_data: Option>, + extra_data: Option, } pub async fn update_node( @@ -92,28 +94,7 @@ pub async fn update_node( } if let Some(extra_data) = update_data.extra_data { - 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::>() - ), - }, - )?; - - 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::>(); + let result = get_rows_for_extra_keys(&tx, &extra_data)?; for (key, (relation, field_name, ty)) in result.iter() { let new_value = extra_data.get(key).unwrap(); @@ -127,7 +108,7 @@ pub async fn update_node( :update {relation} {{ node_id, {field_name} }} " ); - println!("QUERY: {query:?}"); + let result = tx.run_script( &query, btmap! { @@ -135,8 +116,6 @@ pub async fn update_node( "input_data".to_owned() => new_value, }, )?; - - println!("RESULT: {result:?}"); } } @@ -164,8 +143,85 @@ pub async fn node_types() -> AppResult> { }))) } -pub async fn create_node() -> AppResult<()> { - Ok(()) +#[derive(Debug, Deserialize)] +pub struct CreateNodeOpts { + // TODO: Allow submitting a string + // id: Option, + #[serde(rename = "type")] + ty: String, + extra_data: Option, +} + +pub async fn create_node( + State(state): State, + Json(opts): Json, +) -> AppResult> { + 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::>(); + + let keys = fields_mapping.keys().collect::>(); + 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)] @@ -179,14 +235,18 @@ pub async fn search_nodes( ) -> AppResult> { let results = state.db.run_script( " - ?[node_id, content, score] := ~journal:text_index {node_id, content, | - query: $q, - k: 10, - score_kind: 'tf_idf', - bind_score: score - } + results[node_id, content, score] := ~journal:text_index {node_id, content, | + query: $q, + k: 10, + score_kind: 'tf_idf', + bind_score: score + } - :order -score + ?[node_id, content, title, score] := + results[node_id, content, score], + *node{ id: node_id, title } + + :order -score ", btmap! { "q".to_owned() => DataValue::from(query.query), @@ -201,7 +261,8 @@ pub async fn search_nodes( json!({ "node_id": row[0].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::>(); @@ -210,3 +271,36 @@ pub async fn search_nodes( "results": results }))) } + +type ExtraData = HashMap; + +fn get_rows_for_extra_keys( + tx: &MultiTransaction, + extra_data: &ExtraData, +) -> AppResult> { + 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::>() + ), + }, + )?; + + 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::>(), + ) +}