search endpoint

This commit is contained in:
Michael Zhang 2024-05-27 00:43:09 -05:00
parent 7307f92f55
commit 508a7cbf5e
17 changed files with 237 additions and 20 deletions

View file

@ -15,11 +15,13 @@
"@fontsource/inter": "^5.0.18", "@fontsource/inter": "^5.0.18",
"@mui/icons-material": "^5.15.18", "@mui/icons-material": "^5.15.18",
"@mui/material": "^5.15.18", "@mui/material": "^5.15.18",
"@remark-embedder/core": "^3.0.3",
"@tanstack/react-query": "^5.37.1", "@tanstack/react-query": "^5.37.1",
"@tauri-apps/api": "^1", "@tauri-apps/api": "^1",
"@uidotdev/usehooks": "^2.4.1", "@uidotdev/usehooks": "^2.4.1",
"@uiw/react-md-editor": "^4.0.4", "@uiw/react-md-editor": "^4.0.4",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"date-fns": "^3.6.0",
"hast-util-to-jsx-runtime": "^2.3.0", "hast-util-to-jsx-runtime": "^2.3.0",
"hast-util-to-mdast": "^10.1.0", "hast-util-to-mdast": "^10.1.0",
"javascript-time-ago": "^2.5.10", "javascript-time-ago": "^2.5.10",

View file

@ -11,7 +11,7 @@ edition = "2021"
tauri-build = { version = "1", features = [] } tauri-build = { version = "1", features = [] }
[dependencies] [dependencies]
tauri = { version = "1", features = [ "http-request", "shell-open"] } tauri = { version = "1", features = [ "app-all", "http-request", "shell-open"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"

View file

@ -7,11 +7,16 @@
}, },
"package": { "package": {
"productName": "panorama", "productName": "panorama",
"version": "0.0.0" "version": "0.1.0"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {
"all": false, "all": false,
"app": {
"all": true,
"hide": true,
"show": true
},
"shell": { "shell": {
"all": false, "all": false,
"open": true "open": true

View file

@ -8,9 +8,12 @@
height: 100%; height: 100%;
} }
.nodeContainer { .main {
flex-grow: 1; flex-grow: 1;
display: flex;
}
.nodeContainer {
display: flex; display: flex;
justify-items: stretch; justify-items: stretch;

View file

@ -2,6 +2,7 @@ import Header from "./components/Header";
import styles from "./App.module.scss"; import styles from "./App.module.scss";
import "@fontsource/inter"; import "@fontsource/inter";
import "@fontsource/inter/700.css";
import "./global.scss"; import "./global.scss";
import "katex/dist/katex.min.css"; import "katex/dist/katex.min.css";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@ -9,6 +10,7 @@ import NodeDisplay from "./components/NodeDisplay";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 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";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@ -41,7 +43,10 @@ function App() {
<div className={styles.container}> <div className={styles.container}>
<Header /> <Header />
<div className={styles.nodeContainer}>{nodes}</div> <div className={styles.main}>
<Sidebar />
<div className={styles.nodeContainer}>{nodes}</div>
</div>
</div> </div>
</QueryClientProvider> </QueryClientProvider>
); );

View file

@ -7,4 +7,26 @@
background: rgb(204, 201, 255); background: rgb(204, 201, 255);
background: linear-gradient(90deg, rgba(204, 201, 255, 1) 0%, rgba(255, 255, 255, 1) 100%); background: linear-gradient(90deg, rgba(204, 201, 255, 1) 0%, rgba(255, 255, 255, 1) 100%);
}
.headerBorder {
height: 1px;
background: rgb(170, 166, 255);
background: linear-gradient(90deg, rgba(170, 166, 255, 1) 0%, rgba(255, 255, 255, 1) 100%);
}
.brand {
display: flex;
flex-direction: column;
.title {
font-size: 1.2em;
margin: 0;
}
.version {
font-size: .6em;
color: rgb(0, 0, 0, 0.5);
margin: 0;
}
} }

View file

@ -1,15 +1,34 @@
import styles from "./Header.module.scss"; import styles from "./Header.module.scss";
import NoteAddIcon from "@mui/icons-material/NoteAdd"; import NoteAddIcon from "@mui/icons-material/NoteAdd";
import SearchBar from "./SearchBar"; import SearchBar from "./SearchBar";
import { getVersion } from "@tauri-apps/api/app";
import ListIcon from "@mui/icons-material/List";
import { useSetAtom } from "jotai";
import { sidebarExpandedAtom } from "./Sidebar";
const version = await getVersion();
export default function Header() { export default function Header() {
const setSidebarExpanded = useSetAtom(sidebarExpandedAtom);
return ( return (
<div className={styles.Header}> <>
<span>Panorama</span> <div className={styles.Header}>
<button type="button"> <button
<NoteAddIcon /> type="button"
</button> onClick={() => setSidebarExpanded((prev) => !prev)}
<SearchBar /> >
</div> <ListIcon />
</button>
<div className={styles.brand}>
<span className={styles.title}>Panorama</span>
<span className={styles.version}>v{version}</span>
</div>
<button type="button">
<NoteAddIcon />
</button>
<SearchBar />
</div>
<div className={styles.headerBorder} />
</>
); );
} }

View file

@ -26,7 +26,9 @@ export default function NodeDisplay({ id }: NodeDisplayProps) {
); );
useEffect(() => { useEffect(() => {
setTitle(data.title); if (data) {
setTitle(data.title);
}
}, [data]); }, [data]);
const saveChangedTitle = useCallback(() => { const saveChangedTitle = useCallback(() => {

View file

@ -0,0 +1,29 @@
.sidebar {
display: flex;
flex-direction: column;
gap: 6px;
background-color: rgba(204, 201, 255);
padding: 6px;
.item {
display: flex;
align-items: center;
padding: 6px;
font-size: 0.95em;
border-radius: 4px;
&:hover {
background-color: rgba(255, 255, 255, 0.2);
cursor: pointer;
}
}
&.expanded .item {
gap: 6px;
}
&.collapsed .item .label {
display: none;
}
}

View file

@ -0,0 +1,32 @@
import { atom, useAtomValue } from "jotai";
import styles from "./Sidebar.module.scss";
import classNames from "classnames";
import EmailIcon from "@mui/icons-material/Email";
import SettingsIcon from "@mui/icons-material/Settings";
export const sidebarExpandedAtom = atom(false);
export default function Sidebar() {
const sidebarExpanded = useAtomValue(sidebarExpandedAtom);
return (
<div
className={classNames(
styles.sidebar,
sidebarExpanded ? styles.expanded : styles.collapsed,
)}
>
<div className={styles.item}>
<EmailIcon />
<span className={styles.label}>Email</span>
</div>
<div className="spacer" />
<div className={styles.item}>
<SettingsIcon />
<span className={styles.label}>Settings</span>
</div>
</div>
);
}

View file

@ -2,7 +2,6 @@
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px;
} }
.mdContent { .mdContent {
@ -17,6 +16,12 @@
border-radius: 0; border-radius: 0;
} }
.dayIndicator {
background-color: lavender;
padding: 2px 12px;
font-size: 0.8em;
}
.block { .block {
padding: 12px; padding: 12px;

View file

@ -1,10 +1,12 @@
import { useEffect, useState } from "react"; import { useEffect, useRef, useState } from "react";
import MDEditor from "@uiw/react-md-editor"; import MDEditor, { PreviewType } from "@uiw/react-md-editor";
import { usePrevious, useDebounce } from "@uidotdev/usehooks"; import { usePrevious, useDebounce } from "@uidotdev/usehooks";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import styles from "./JournalPage.module.scss"; import styles from "./JournalPage.module.scss";
import remarkMath from "remark-math"; import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex"; import rehypeKatex from "rehype-katex";
import remarkEmbedder from "@remark-embedder/core";
import { parse as parseDate, format as formatDate } from "date-fns";
export interface JournalPageProps { export interface JournalPageProps {
id: string; id: string;
@ -14,11 +16,13 @@ export interface JournalPageProps {
} }
export default function JournalPage({ id, data }: JournalPageProps) { export default function JournalPage({ id, data }: JournalPageProps) {
const { day } = data;
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [value, setValue] = useState(() => data.content); const [value, setValue] = useState(() => data.content);
const valueToSave = useDebounce(value, 1000); const valueToSave = useDebounce(value, 1000);
const previous = usePrevious(valueToSave); const previous = usePrevious(valueToSave);
const changed = valueToSave !== previous; const changed = valueToSave !== previous;
const [mode, setMode] = useState<PreviewType>("preview");
useEffect(() => { useEffect(() => {
if (changed) { if (changed) {
@ -45,12 +49,15 @@ export default function JournalPage({ id, data }: JournalPageProps) {
return ( return (
<div data-color-mode="light" className={styles.container}> <div data-color-mode="light" className={styles.container}>
{day && <DayIndicator day={day} />}
<MDEditor <MDEditor
value={value} value={value}
className={styles.mdEditor} className={styles.mdEditor}
onChange={(newValue) => newValue && setValue(newValue)} onChange={(newValue) => newValue !== undefined && setValue(newValue)}
preview="preview" preview={mode}
visibleDragbar={false} visibleDragbar={false}
onDoubleClick={() => setMode("live")}
previewOptions={{ previewOptions={{
remarkPlugins: [remarkMath], remarkPlugins: [remarkMath],
rehypePlugins: [rehypeKatex], rehypePlugins: [rehypeKatex],
@ -59,3 +66,13 @@ export default function JournalPage({ id, data }: JournalPageProps) {
</div> </div>
); );
} }
function DayIndicator({ day }) {
const parsedDate = parseDate(day, "yyyy-MM-dd", new Date());
const formattedDate = formatDate(parsedDate, "PPPP");
return (
<div className={styles.dayIndicator}>
Journal entry for <b>{formattedDate}</b>
</div>
);
}

View file

@ -12,4 +12,8 @@ html,
#root { #root {
width: 100%; width: 100%;
height: 100%; height: 100%;
}
.spacer {
flex-grow: 1;
} }

View file

View file

@ -19,7 +19,7 @@ use std::fs;
use anyhow::Result; use anyhow::Result;
use axum::{ use axum::{
http::Method, http::Method,
routing::{get, post}, routing::{get, post, put},
Router, Router,
}; };
use cozo::DbInstance; use cozo::DbInstance;
@ -32,7 +32,7 @@ use crate::{
export::export, export::export,
journal::get_todays_journal_id, journal::get_todays_journal_id,
migrations::run_migrations, migrations::run_migrations,
node::{get_node, node_types, update_node}, node::{create_node, get_node, node_types, search_nodes, update_node},
}; };
#[derive(Clone)] #[derive(Clone)]
@ -67,6 +67,8 @@ async fn main() -> Result<()> {
let app = Router::new() let app = Router::new()
.route("/", get(|| async { "Hello, World!" })) .route("/", get(|| async { "Hello, World!" }))
.route("/export", get(export)) .route("/export", get(export))
.route("/node", put(create_node))
.route("/node/search", get(search_nodes))
.route("/node/:id", get(get_node)) .route("/node/:id", get(get_node))
.route("/node/:id", post(update_node)) .route("/node/:id", post(update_node))
.route("/node/types", get(node_types)) .route("/node/types", get(node_types))

View file

@ -1,7 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, Query, State},
http::StatusCode, http::StatusCode,
Json, Json,
}; };
@ -163,3 +163,50 @@ pub async fn node_types() -> AppResult<Json<Value>> {
] ]
}))) })))
} }
pub async fn create_node() -> AppResult<()> {
Ok(())
}
#[derive(Deserialize)]
pub struct SearchQuery {
query: String,
}
pub async fn search_nodes(
State(state): State<AppState>,
Query(query): Query<SearchQuery>,
) -> 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
}
:order -score
",
btmap! {
"q".to_owned() => DataValue::from(query.query),
},
ScriptMutability::Immutable,
)?;
let results = results
.rows
.into_iter()
.map(|row| {
json!({
"node_id": row[0].get_str().unwrap(),
"content": row[1].get_str().unwrap(),
"score": row[2].get_float().unwrap(),
})
})
.collect::<Vec<_>>();
Ok(Json(json!({
"results": results
})))
}

View file

@ -26,6 +26,9 @@ importers:
'@mui/material': '@mui/material':
specifier: ^5.15.18 specifier: ^5.15.18
version: 5.15.18(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) version: 5.15.18(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@remark-embedder/core':
specifier: ^3.0.3
version: 3.0.3
'@tanstack/react-query': '@tanstack/react-query':
specifier: ^5.37.1 specifier: ^5.37.1
version: 5.39.0(react@18.3.1) version: 5.39.0(react@18.3.1)
@ -41,6 +44,9 @@ importers:
classnames: classnames:
specifier: ^2.5.1 specifier: ^2.5.1
version: 2.5.1 version: 2.5.1
date-fns:
specifier: ^3.6.0
version: 3.6.0
hast-util-to-jsx-runtime: hast-util-to-jsx-runtime:
specifier: ^2.3.0 specifier: ^2.3.0
version: 2.3.0 version: 2.3.0
@ -926,6 +932,19 @@ packages:
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
dev: false dev: false
/@remark-embedder/core@3.0.3:
resolution: {integrity: sha512-izeW4GT5A/NgArjATndg1KKumL7IHLPZFQODJ07vSEHgCOBq2caMlnuvFGD+7ZmF4NCZks/HhZsYhfF46TOK5w==}
engines: {node: '>=12', npm: '>=6'}
dependencies:
'@babel/runtime': 7.24.6
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
hast-util-from-parse5: 8.0.1
parse5: 7.1.2
unified: 11.0.4
unist-util-visit: 5.0.0
dev: false
/@rollup/rollup-android-arm-eabi@4.18.0: /@rollup/rollup-android-arm-eabi@4.18.0:
resolution: {integrity: sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==} resolution: {integrity: sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==}
cpu: [arm] cpu: [arm]
@ -1524,6 +1543,10 @@ packages:
/csstype@3.1.3: /csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
/date-fns@3.6.0:
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
dev: false
/debug@4.3.4: /debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}