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",
|
||||
"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",
|
||||
|
|
|
@ -18,4 +18,5 @@
|
|||
justify-items: stretch;
|
||||
|
||||
padding: 12px;
|
||||
gap: 12px;
|
||||
}
|
|
@ -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<string[]>(() => []);
|
||||
export const nodesOpenedAtom = atom<string[]>([]);
|
||||
|
||||
function App() {
|
||||
const [nodesOpened, setNodesOpened] = useAtom(nodesOpenedAtom);
|
||||
|
||||
// Open today's journal entry if it's not already opened
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
console.log("ndoes", nodesOpened);
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<div className={styles.Header}>
|
||||
|
@ -23,7 +46,7 @@ export default function Header() {
|
|||
<span className={styles.title}>Panorama</span>
|
||||
<span className={styles.version}>v{version}</span>
|
||||
</div>
|
||||
<button type="button">
|
||||
<button type="button" onClick={createNewJournalPage}>
|
||||
<NoteAddIcon />
|
||||
</button>
|
||||
<SearchBar />
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -85,6 +85,8 @@ export default function NodeDisplay({ id }: NodeDisplayProps) {
|
|||
<>Status: {status}</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.footer}>{id}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -93,7 +95,7 @@ function NodeDisplayHeaderLoaded({ id, data }) {
|
|||
return (
|
||||
<>
|
||||
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"
|
||||
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"
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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<String>,
|
||||
extra_data: Option<HashMap<String, Value>>,
|
||||
extra_data: Option<ExtraData>,
|
||||
}
|
||||
|
||||
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::<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<_, _>>();
|
||||
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<Json<Value>> {
|
|||
})))
|
||||
}
|
||||
|
||||
pub async fn create_node() -> AppResult<()> {
|
||||
Ok(())
|
||||
#[derive(Debug, Deserialize)]
|
||||
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)]
|
||||
|
@ -179,14 +235,18 @@ pub async fn search_nodes(
|
|||
) -> AppResult<Json<Value>> {
|
||||
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::<Vec<_>>();
|
||||
|
@ -210,3 +271,36 @@ pub async fn search_nodes(
|
|||
"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