create new nodes

This commit is contained in:
Michael Zhang 2024-05-27 01:27:17 -05:00
parent 508a7cbf5e
commit a7e4e82765
9 changed files with 186 additions and 44 deletions

12
Cargo.lock generated
View file

@ -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",

View file

@ -18,4 +18,5 @@
justify-items: stretch; justify-items: stretch;
padding: 12px; padding: 12px;
gap: 12px;
} }

View file

@ -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);

View file

@ -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 />

View file

@ -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;

View file

@ -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} &middot; Last updated{" "} Type {data.type} &middot; Last updated{" "}
<ReactTimeAgo date={data.created_at * 1000} /> &middot; {id} <ReactTimeAgo date={data.created_at * 1000} />
</> </>
); );
} }

View file

@ -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"

View file

@ -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);

View file

@ -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<_, _>>(),
)
}