search endpoint
This commit is contained in:
parent
7307f92f55
commit
508a7cbf5e
17 changed files with 237 additions and 20 deletions
|
@ -15,11 +15,13 @@
|
|||
"@fontsource/inter": "^5.0.18",
|
||||
"@mui/icons-material": "^5.15.18",
|
||||
"@mui/material": "^5.15.18",
|
||||
"@remark-embedder/core": "^3.0.3",
|
||||
"@tanstack/react-query": "^5.37.1",
|
||||
"@tauri-apps/api": "^1",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@uiw/react-md-editor": "^4.0.4",
|
||||
"classnames": "^2.5.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"hast-util-to-jsx-runtime": "^2.3.0",
|
||||
"hast-util-to-mdast": "^10.1.0",
|
||||
"javascript-time-ago": "^2.5.10",
|
||||
|
|
|
@ -11,7 +11,7 @@ edition = "2021"
|
|||
tauri-build = { version = "1", features = [] }
|
||||
|
||||
[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_json = "1"
|
||||
|
||||
|
|
|
@ -7,11 +7,16 @@
|
|||
},
|
||||
"package": {
|
||||
"productName": "panorama",
|
||||
"version": "0.0.0"
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"all": false,
|
||||
"app": {
|
||||
"all": true,
|
||||
"hide": true,
|
||||
"show": true
|
||||
},
|
||||
"shell": {
|
||||
"all": false,
|
||||
"open": true
|
||||
|
|
|
@ -8,9 +8,12 @@
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
.nodeContainer {
|
||||
.main {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nodeContainer {
|
||||
display: flex;
|
||||
justify-items: stretch;
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import Header from "./components/Header";
|
|||
import styles from "./App.module.scss";
|
||||
|
||||
import "@fontsource/inter";
|
||||
import "@fontsource/inter/700.css";
|
||||
import "./global.scss";
|
||||
import "katex/dist/katex.min.css";
|
||||
import { useEffect, useState } from "react";
|
||||
|
@ -9,6 +10,7 @@ import NodeDisplay from "./components/NodeDisplay";
|
|||
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";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
|
@ -41,7 +43,10 @@ function App() {
|
|||
<div className={styles.container}>
|
||||
<Header />
|
||||
|
||||
<div className={styles.nodeContainer}>{nodes}</div>
|
||||
<div className={styles.main}>
|
||||
<Sidebar />
|
||||
<div className={styles.nodeContainer}>{nodes}</div>
|
||||
</div>
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
|
|
@ -7,4 +7,26 @@
|
|||
|
||||
background: rgb(204, 201, 255);
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -1,15 +1,34 @@
|
|||
import styles from "./Header.module.scss";
|
||||
import NoteAddIcon from "@mui/icons-material/NoteAdd";
|
||||
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() {
|
||||
const setSidebarExpanded = useSetAtom(sidebarExpandedAtom);
|
||||
return (
|
||||
<div className={styles.Header}>
|
||||
<span>Panorama</span>
|
||||
<button type="button">
|
||||
<NoteAddIcon />
|
||||
</button>
|
||||
<SearchBar />
|
||||
</div>
|
||||
<>
|
||||
<div className={styles.Header}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSidebarExpanded((prev) => !prev)}
|
||||
>
|
||||
<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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -26,7 +26,9 @@ export default function NodeDisplay({ id }: NodeDisplayProps) {
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTitle(data.title);
|
||||
if (data) {
|
||||
setTitle(data.title);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const saveChangedTitle = useCallback(() => {
|
||||
|
|
29
app/src/components/Sidebar.module.scss
Normal file
29
app/src/components/Sidebar.module.scss
Normal 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;
|
||||
}
|
||||
}
|
32
app/src/components/Sidebar.tsx
Normal file
32
app/src/components/Sidebar.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -2,7 +2,6 @@
|
|||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mdContent {
|
||||
|
@ -17,6 +16,12 @@
|
|||
border-radius: 0;
|
||||
}
|
||||
|
||||
.dayIndicator {
|
||||
background-color: lavender;
|
||||
padding: 2px 12px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.block {
|
||||
padding: 12px;
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import MDEditor from "@uiw/react-md-editor";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import MDEditor, { PreviewType } from "@uiw/react-md-editor";
|
||||
import { usePrevious, useDebounce } from "@uidotdev/usehooks";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import styles from "./JournalPage.module.scss";
|
||||
import remarkMath from "remark-math";
|
||||
import rehypeKatex from "rehype-katex";
|
||||
import remarkEmbedder from "@remark-embedder/core";
|
||||
import { parse as parseDate, format as formatDate } from "date-fns";
|
||||
|
||||
export interface JournalPageProps {
|
||||
id: string;
|
||||
|
@ -14,11 +16,13 @@ export interface JournalPageProps {
|
|||
}
|
||||
|
||||
export default function JournalPage({ id, data }: JournalPageProps) {
|
||||
const { day } = data;
|
||||
const queryClient = useQueryClient();
|
||||
const [value, setValue] = useState(() => data.content);
|
||||
const valueToSave = useDebounce(value, 1000);
|
||||
const previous = usePrevious(valueToSave);
|
||||
const changed = valueToSave !== previous;
|
||||
const [mode, setMode] = useState<PreviewType>("preview");
|
||||
|
||||
useEffect(() => {
|
||||
if (changed) {
|
||||
|
@ -45,12 +49,15 @@ export default function JournalPage({ id, data }: JournalPageProps) {
|
|||
|
||||
return (
|
||||
<div data-color-mode="light" className={styles.container}>
|
||||
{day && <DayIndicator day={day} />}
|
||||
|
||||
<MDEditor
|
||||
value={value}
|
||||
className={styles.mdEditor}
|
||||
onChange={(newValue) => newValue && setValue(newValue)}
|
||||
preview="preview"
|
||||
onChange={(newValue) => newValue !== undefined && setValue(newValue)}
|
||||
preview={mode}
|
||||
visibleDragbar={false}
|
||||
onDoubleClick={() => setMode("live")}
|
||||
previewOptions={{
|
||||
remarkPlugins: [remarkMath],
|
||||
rehypePlugins: [rehypeKatex],
|
||||
|
@ -59,3 +66,13 @@ export default function JournalPage({ id, data }: JournalPageProps) {
|
|||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,4 +12,8 @@ html,
|
|||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
0
crates/panorama-daemon/src/mail.rs
Normal file
0
crates/panorama-daemon/src/mail.rs
Normal file
|
@ -19,7 +19,7 @@ use std::fs;
|
|||
use anyhow::Result;
|
||||
use axum::{
|
||||
http::Method,
|
||||
routing::{get, post},
|
||||
routing::{get, post, put},
|
||||
Router,
|
||||
};
|
||||
use cozo::DbInstance;
|
||||
|
@ -32,7 +32,7 @@ use crate::{
|
|||
export::export,
|
||||
journal::get_todays_journal_id,
|
||||
migrations::run_migrations,
|
||||
node::{get_node, node_types, update_node},
|
||||
node::{create_node, get_node, node_types, search_nodes, update_node},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -67,6 +67,8 @@ async fn main() -> Result<()> {
|
|||
let app = Router::new()
|
||||
.route("/", get(|| async { "Hello, World!" }))
|
||||
.route("/export", get(export))
|
||||
.route("/node", put(create_node))
|
||||
.route("/node/search", get(search_nodes))
|
||||
.route("/node/:id", get(get_node))
|
||||
.route("/node/:id", post(update_node))
|
||||
.route("/node/types", get(node_types))
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
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
|
||||
})))
|
||||
}
|
||||
|
|
|
@ -26,6 +26,9 @@ importers:
|
|||
'@mui/material':
|
||||
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)
|
||||
'@remark-embedder/core':
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3
|
||||
'@tanstack/react-query':
|
||||
specifier: ^5.37.1
|
||||
version: 5.39.0(react@18.3.1)
|
||||
|
@ -41,6 +44,9 @@ importers:
|
|||
classnames:
|
||||
specifier: ^2.5.1
|
||||
version: 2.5.1
|
||||
date-fns:
|
||||
specifier: ^3.6.0
|
||||
version: 3.6.0
|
||||
hast-util-to-jsx-runtime:
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
|
@ -926,6 +932,19 @@ packages:
|
|||
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
||||
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:
|
||||
resolution: {integrity: sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==}
|
||||
cpu: [arm]
|
||||
|
@ -1524,6 +1543,10 @@ packages:
|
|||
/csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
/date-fns@3.6.0:
|
||||
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
|
||||
dev: false
|
||||
|
||||
/debug@4.3.4:
|
||||
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
|
|
Loading…
Reference in a new issue