@powerhousedao/knowledge-note 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +359 -122
- package/dist/editors/knowledge-vault/components/DriveExplorer.d.ts.map +1 -1
- package/dist/editors/knowledge-vault/components/DriveExplorer.js +8 -2
- package/dist/editors/knowledge-vault/components/SearchView.d.ts +2 -0
- package/dist/editors/knowledge-vault/components/SearchView.d.ts.map +1 -0
- package/dist/editors/knowledge-vault/components/SearchView.js +96 -0
- package/dist/editors/knowledge-vault/hooks/use-graph-search.d.ts +25 -0
- package/dist/editors/knowledge-vault/hooks/use-graph-search.d.ts.map +1 -0
- package/dist/editors/knowledge-vault/hooks/use-graph-search.js +198 -0
- package/dist/package.json +2 -1
- package/dist/processors/graph-indexer/embedder.d.ts +5 -0
- package/dist/processors/graph-indexer/embedder.d.ts.map +1 -0
- package/dist/processors/graph-indexer/embedder.js +27 -0
- package/dist/processors/graph-indexer/embedding-store.d.ts +11 -0
- package/dist/processors/graph-indexer/embedding-store.d.ts.map +1 -0
- package/dist/processors/graph-indexer/embedding-store.js +70 -0
- package/dist/processors/graph-indexer/index.d.ts.map +1 -1
- package/dist/processors/graph-indexer/index.js +48 -0
- package/dist/processors/graph-indexer/migrations.d.ts.map +1 -1
- package/dist/processors/graph-indexer/migrations.js +35 -0
- package/dist/processors/graph-indexer/query.d.ts +21 -0
- package/dist/processors/graph-indexer/query.d.ts.map +1 -1
- package/dist/processors/graph-indexer/query.js +154 -13
- package/dist/processors/graph-indexer/schema.d.ts +11 -0
- package/dist/processors/graph-indexer/schema.d.ts.map +1 -1
- package/dist/style.css +63 -0
- package/dist/subgraphs/knowledge-graph/subgraph.d.ts +321 -12
- package/dist/subgraphs/knowledge-graph/subgraph.d.ts.map +1 -1
- package/dist/subgraphs/knowledge-graph/subgraph.js +237 -15
- package/package.json +2 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { setSelectedNode } from "@powerhousedao/reactor-browser";
|
|
4
|
+
import { useGraphSearch, } from "../hooks/use-graph-search.js";
|
|
5
|
+
/* ------------------------------------------------------------------ */
|
|
6
|
+
/* Constants */
|
|
7
|
+
/* ------------------------------------------------------------------ */
|
|
8
|
+
const STATUS_COLORS = {
|
|
9
|
+
DRAFT: {
|
|
10
|
+
background: "rgba(245, 158, 11, 0.2)",
|
|
11
|
+
color: "rgba(252, 211, 77, 1)",
|
|
12
|
+
borderColor: "rgba(245, 158, 11, 0.3)",
|
|
13
|
+
},
|
|
14
|
+
IN_REVIEW: {
|
|
15
|
+
background: "rgba(59, 130, 246, 0.2)",
|
|
16
|
+
color: "rgba(147, 197, 253, 1)",
|
|
17
|
+
borderColor: "rgba(59, 130, 246, 0.3)",
|
|
18
|
+
},
|
|
19
|
+
CANONICAL: {
|
|
20
|
+
background: "rgba(16, 185, 129, 0.2)",
|
|
21
|
+
color: "rgba(110, 231, 183, 1)",
|
|
22
|
+
borderColor: "rgba(16, 185, 129, 0.3)",
|
|
23
|
+
},
|
|
24
|
+
ARCHIVED: {
|
|
25
|
+
background: "var(--bai-hover)",
|
|
26
|
+
color: "var(--bai-text-muted)",
|
|
27
|
+
borderColor: "var(--bai-border)",
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
function similarityColor(score) {
|
|
31
|
+
if (score >= 0.8)
|
|
32
|
+
return "#10b981";
|
|
33
|
+
if (score >= 0.6)
|
|
34
|
+
return "#f59e0b";
|
|
35
|
+
return "#6b7280";
|
|
36
|
+
}
|
|
37
|
+
/* ------------------------------------------------------------------ */
|
|
38
|
+
/* Component */
|
|
39
|
+
/* ------------------------------------------------------------------ */
|
|
40
|
+
export function SearchView() {
|
|
41
|
+
const { query, setQuery, results, topics, loading, error, searchMode, setSearchMode, } = useGraphSearch();
|
|
42
|
+
return (_jsxs("div", { className: "flex h-full flex-col p-4", children: [_jsxs("div", { className: "mb-4 flex items-center gap-3", children: [_jsxs("div", { className: "relative flex-1", style: { color: "var(--bai-text-muted)" }, children: [_jsxs("svg", { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("circle", { cx: "11", cy: "11", r: "8" }), _jsx("path", { d: "m21 21-4.35-4.35" })] }), _jsx("input", { type: "text", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search knowledge vault...", className: "w-full rounded-lg border py-2.5 pl-10 pr-10 text-sm outline-none transition-colors", style: {
|
|
43
|
+
backgroundColor: "var(--bai-surface)",
|
|
44
|
+
borderColor: "var(--bai-border)",
|
|
45
|
+
color: "var(--bai-text)",
|
|
46
|
+
} }), query && (_jsx("button", { type: "button", onClick: () => setQuery(""), className: "absolute right-3 top-1/2 -translate-y-1/2 rounded p-0.5 transition-colors hover:opacity-80", style: { color: "var(--bai-text-muted)" }, children: _jsx("svg", { className: "h-4 w-4", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: _jsx("path", { d: "M18 6 6 18M6 6l12 12" }) }) }))] }), _jsx(ModeToggle, { mode: searchMode, onChange: setSearchMode })] }), query.trim() && (_jsxs("div", { className: "mb-3 flex items-center gap-2 text-xs", style: { color: "var(--bai-text-muted)" }, children: [loading ? (_jsx("span", { children: "Searching..." })) : (_jsxs(_Fragment, { children: [_jsxs("span", { children: [results.length, " result", results.length !== 1 ? "s" : ""] }), searchMode === "semantic" && results.length > 0 && (_jsx("span", { style: { opacity: 0.6 }, children: "\u2014 match % shows how closely each note relates to your query by meaning" }))] })), error && _jsx("span", { style: { color: "#ef4444" }, children: error })] })), _jsx("div", { className: "flex-1 overflow-auto", children: !query.trim() ? (_jsx(EmptyState, { topics: topics, onTopicClick: setQuery })) : (_jsx(ResultList, { results: results, searchMode: searchMode })) })] }));
|
|
47
|
+
}
|
|
48
|
+
/* ------------------------------------------------------------------ */
|
|
49
|
+
/* Mode toggle */
|
|
50
|
+
/* ------------------------------------------------------------------ */
|
|
51
|
+
function ModeToggle({ mode, onChange, }) {
|
|
52
|
+
return (_jsxs("div", { className: "flex shrink-0 overflow-hidden rounded-lg border text-xs", style: { borderColor: "var(--bai-border)" }, children: [_jsx("button", { type: "button", onClick: () => onChange("semantic"), className: "px-3 py-2 transition-colors", style: {
|
|
53
|
+
backgroundColor: mode === "semantic" ? "var(--bai-accent)" : "var(--bai-surface)",
|
|
54
|
+
color: mode === "semantic" ? "#fff" : "var(--bai-text-muted)",
|
|
55
|
+
}, children: "Semantic" }), _jsx("button", { type: "button", onClick: () => onChange("keyword"), className: "px-3 py-2 transition-colors", style: {
|
|
56
|
+
backgroundColor: mode === "keyword" ? "var(--bai-accent)" : "var(--bai-surface)",
|
|
57
|
+
color: mode === "keyword" ? "#fff" : "var(--bai-text-muted)",
|
|
58
|
+
}, children: "Keyword" })] }));
|
|
59
|
+
}
|
|
60
|
+
/* ------------------------------------------------------------------ */
|
|
61
|
+
/* Empty state */
|
|
62
|
+
/* ------------------------------------------------------------------ */
|
|
63
|
+
function EmptyState({ topics, onTopicClick, }) {
|
|
64
|
+
const [showTopics, setShowTopics] = useState(false);
|
|
65
|
+
return (_jsxs("div", { className: "flex flex-col items-center pt-16", children: [_jsxs("svg", { className: "mb-4 h-12 w-12", style: { color: "var(--bai-text-faint)", opacity: 0.5 }, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: [_jsx("circle", { cx: "11", cy: "11", r: "8" }), _jsx("path", { d: "m21 21-4.35-4.35" })] }), _jsx("h2", { className: "mb-2 text-lg font-semibold", style: { color: "var(--bai-text)" }, children: "Search the Knowledge Vault" }), _jsx("p", { className: "mb-6 max-w-md text-center text-sm leading-relaxed", style: { color: "var(--bai-text-muted)" }, children: "Ask a question in natural language or type keywords. Semantic search understands meaning \u2014 try \"how does storage work?\" or \"legal setup for organizations\"." }), topics.length > 0 && (_jsxs("div", { className: "flex flex-col items-center", children: [_jsx("button", { type: "button", onClick: () => setShowTopics((s) => !s), className: "mb-4 rounded-lg border px-5 py-2.5 text-sm font-medium transition-colors", style: {
|
|
66
|
+
borderColor: "var(--bai-border)",
|
|
67
|
+
color: "var(--bai-accent)",
|
|
68
|
+
backgroundColor: "var(--bai-surface)",
|
|
69
|
+
}, children: showTopics ? "Hide Topics" : `Explore ${topics.length} Topics` }), showTopics && (_jsx("div", { className: "max-w-2xl", children: _jsx("div", { className: "flex flex-wrap justify-center gap-2", children: topics.map((t) => (_jsxs("button", { type: "button", onClick: () => onTopicClick(t.name), className: "rounded-full px-3 py-1.5 text-xs transition-colors", style: {
|
|
70
|
+
backgroundColor: "var(--bai-hover)",
|
|
71
|
+
color: "var(--bai-accent)",
|
|
72
|
+
}, children: [t.name, _jsx("span", { className: "ml-1.5 opacity-50", style: { color: "var(--bai-text-muted)" }, children: t.noteCount })] }, t.name))) }) }))] }))] }));
|
|
73
|
+
}
|
|
74
|
+
/* ------------------------------------------------------------------ */
|
|
75
|
+
/* Result list */
|
|
76
|
+
/* ------------------------------------------------------------------ */
|
|
77
|
+
function ResultList({ results, searchMode, }) {
|
|
78
|
+
if (results.length === 0) {
|
|
79
|
+
return (_jsx("div", { className: "py-12 text-center text-sm", style: { color: "var(--bai-text-muted)" }, children: "No results found" }));
|
|
80
|
+
}
|
|
81
|
+
return (_jsx("div", { className: "space-y-2", children: results.map((result) => (_jsx(ResultCard, { result: result, showSimilarity: searchMode === "semantic" }, result.documentId))) }));
|
|
82
|
+
}
|
|
83
|
+
/* ------------------------------------------------------------------ */
|
|
84
|
+
/* Result card */
|
|
85
|
+
/* ------------------------------------------------------------------ */
|
|
86
|
+
function ResultCard({ result, showSimilarity, }) {
|
|
87
|
+
const status = result.status ?? "DRAFT";
|
|
88
|
+
const badgeStyle = STATUS_COLORS[status] ?? STATUS_COLORS.DRAFT;
|
|
89
|
+
return (_jsxs("button", { type: "button", onClick: () => setSelectedNode(result.documentId), className: "group flex w-full items-start gap-3 rounded-xl border p-4 text-left transition-all border-[var(--bai-border)] bg-[var(--bai-surface)] hover:border-[var(--bai-accent)] hover:bg-[var(--bai-hover)]", children: [showSimilarity && result.similarity != null && (_jsxs("div", { className: "shrink-0 rounded-md px-2 py-1 text-center font-mono text-xs font-bold", title: "Semantic similarity \u2014 how closely this note's meaning matches your query (higher is better)", style: {
|
|
90
|
+
color: similarityColor(result.similarity),
|
|
91
|
+
backgroundColor: `${similarityColor(result.similarity)}15`,
|
|
92
|
+
}, children: [(result.similarity * 100).toFixed(0), "%"] })), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "mb-1 flex items-start justify-between gap-2", children: [_jsx("h4", { className: "truncate text-sm font-semibold text-[var(--bai-text)] group-hover:text-[var(--bai-accent)]", children: result.title ?? "Untitled" }), _jsx("span", { className: "shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium", style: badgeStyle, children: status.replace("_", " ") })] }), result.description && (_jsx("p", { className: "mb-2 line-clamp-2 text-xs leading-relaxed", style: { color: "var(--bai-text-muted)" }, children: result.description })), _jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [result.noteType && (_jsx("span", { className: "rounded px-1.5 py-0.5 text-[10px] font-medium", style: {
|
|
93
|
+
background: "var(--bai-hover)",
|
|
94
|
+
color: "var(--bai-text-muted)",
|
|
95
|
+
}, children: result.noteType })), result.topics?.slice(0, 4).map((topic) => (_jsxs("span", { className: "text-[10px]", style: { color: "var(--bai-accent)", opacity: 0.6 }, children: ["#", topic] }, topic)))] })] }), _jsx("svg", { className: "mt-1 h-4 w-4 shrink-0 opacity-0 transition-opacity group-hover:opacity-60", style: { color: "var(--bai-text-muted)" }, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: _jsx("path", { d: "M9 18l6-6-6-6" }) })] }));
|
|
96
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type SearchResult = {
|
|
2
|
+
documentId: string;
|
|
3
|
+
title: string | null;
|
|
4
|
+
description: string | null;
|
|
5
|
+
noteType: string | null;
|
|
6
|
+
status: string | null;
|
|
7
|
+
topics: string[];
|
|
8
|
+
similarity?: number;
|
|
9
|
+
};
|
|
10
|
+
export type TopicInfo = {
|
|
11
|
+
name: string;
|
|
12
|
+
noteCount: number;
|
|
13
|
+
};
|
|
14
|
+
export type SearchMode = "semantic" | "keyword";
|
|
15
|
+
export declare function useGraphSearch(): {
|
|
16
|
+
query: string;
|
|
17
|
+
setQuery: (q: string) => void;
|
|
18
|
+
results: SearchResult[];
|
|
19
|
+
topics: TopicInfo[];
|
|
20
|
+
loading: boolean;
|
|
21
|
+
error: string | null;
|
|
22
|
+
searchMode: SearchMode;
|
|
23
|
+
setSearchMode: (m: SearchMode) => void;
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=use-graph-search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-graph-search.d.ts","sourceRoot":"","sources":["../../../../editors/knowledge-vault/hooks/use-graph-search.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,YAAY,GAAG;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,SAAS,CAAC;AAsHhD,wBAAgB,cAAc;;kBAgBtB,MAAM;;;;;;uBAQN,UAAU;EAoGjB"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
|
2
|
+
import { useSelectedDriveId } from "@powerhousedao/reactor-browser";
|
|
3
|
+
/* ------------------------------------------------------------------ */
|
|
4
|
+
/* GraphQL helpers */
|
|
5
|
+
/* ------------------------------------------------------------------ */
|
|
6
|
+
const DEBOUNCE_MS = 300;
|
|
7
|
+
const SUBGRAPH_PATH = "/graphql/knowledgeGraph";
|
|
8
|
+
/**
|
|
9
|
+
* Derive the subgraph endpoint.
|
|
10
|
+
*
|
|
11
|
+
* Priority:
|
|
12
|
+
* 1. VITE_SUBGRAPH_URL env var (explicit override for deployed environments)
|
|
13
|
+
* 2. Vite dev (port 3000/3001) → localhost:4001
|
|
14
|
+
* 3. Same-origin relative path (Connect production — app and reactor share origin)
|
|
15
|
+
*
|
|
16
|
+
* For deployed environments where Connect and Switchboard are on different
|
|
17
|
+
* domains (e.g. connect.example.com vs switchboard-dev.powerhouse.xyz),
|
|
18
|
+
* set VITE_SUBGRAPH_URL=https://switchboard-dev.powerhouse.xyz/graphql/knowledgeGraph
|
|
19
|
+
* in your .env file.
|
|
20
|
+
*/
|
|
21
|
+
function resolveEndpoint() {
|
|
22
|
+
// Explicit override via env var
|
|
23
|
+
const envUrl = typeof import.meta !== "undefined" &&
|
|
24
|
+
import.meta.env?.VITE_SUBGRAPH_URL;
|
|
25
|
+
if (envUrl)
|
|
26
|
+
return envUrl;
|
|
27
|
+
// Vite dev server proxying to local reactor
|
|
28
|
+
const port = globalThis.window?.location?.port;
|
|
29
|
+
if (port === "3000" || port === "3001") {
|
|
30
|
+
return `http://localhost:4001${SUBGRAPH_PATH}`;
|
|
31
|
+
}
|
|
32
|
+
// Same-origin (Connect production)
|
|
33
|
+
return SUBGRAPH_PATH;
|
|
34
|
+
}
|
|
35
|
+
async function graphqlFetch(endpoint, query, variables) {
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(endpoint, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: { "Content-Type": "application/json" },
|
|
40
|
+
body: JSON.stringify({ query, variables }),
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok)
|
|
43
|
+
return null;
|
|
44
|
+
const json = (await res.json());
|
|
45
|
+
if (json.errors) {
|
|
46
|
+
console.warn("[useGraphSearch] GraphQL errors:", json.errors);
|
|
47
|
+
}
|
|
48
|
+
return json.data ?? null;
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
console.warn("[useGraphSearch] Fetch failed:", err);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/* ------------------------------------------------------------------ */
|
|
56
|
+
/* Queries */
|
|
57
|
+
/* ------------------------------------------------------------------ */
|
|
58
|
+
const SEMANTIC_SEARCH_QUERY = `
|
|
59
|
+
query SemanticSearch($driveId: ID!, $query: String!, $limit: Int) {
|
|
60
|
+
knowledgeGraphSemanticSearch(driveId: $driveId, query: $query, limit: $limit) {
|
|
61
|
+
node { documentId title description noteType status topics }
|
|
62
|
+
similarity
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
const KEYWORD_SEARCH_QUERY = `
|
|
67
|
+
query FullSearch($driveId: ID!, $query: String!, $limit: Int) {
|
|
68
|
+
knowledgeGraphFullSearch(driveId: $driveId, query: $query, limit: $limit) {
|
|
69
|
+
documentId title description noteType status topics
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
const TOPICS_QUERY = `
|
|
74
|
+
query Topics($driveId: ID!) {
|
|
75
|
+
knowledgeGraphTopics(driveId: $driveId) { name noteCount }
|
|
76
|
+
}
|
|
77
|
+
`;
|
|
78
|
+
/* ------------------------------------------------------------------ */
|
|
79
|
+
/* Hook */
|
|
80
|
+
/* ------------------------------------------------------------------ */
|
|
81
|
+
const STORAGE_KEY = "bai-search-state";
|
|
82
|
+
function loadSearchState() {
|
|
83
|
+
try {
|
|
84
|
+
const raw = sessionStorage.getItem(STORAGE_KEY);
|
|
85
|
+
if (raw) {
|
|
86
|
+
const parsed = JSON.parse(raw);
|
|
87
|
+
return {
|
|
88
|
+
query: parsed.query ?? "",
|
|
89
|
+
mode: parsed.mode === "keyword" ? "keyword" : "semantic",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// ignore
|
|
95
|
+
}
|
|
96
|
+
return { query: "", mode: "semantic" };
|
|
97
|
+
}
|
|
98
|
+
function saveSearchState(query, mode) {
|
|
99
|
+
try {
|
|
100
|
+
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ query, mode }));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// ignore
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function useGraphSearch() {
|
|
107
|
+
const driveId = useSelectedDriveId();
|
|
108
|
+
const saved = useRef(loadSearchState());
|
|
109
|
+
const [query, setQueryRaw] = useState(saved.current.query);
|
|
110
|
+
const [results, setResults] = useState([]);
|
|
111
|
+
const [topics, setTopics] = useState([]);
|
|
112
|
+
const [loading, setLoading] = useState(false);
|
|
113
|
+
const [error, setError] = useState(null);
|
|
114
|
+
const [searchMode, setSearchModeRaw] = useState(saved.current.mode);
|
|
115
|
+
const debounceRef = useRef(null);
|
|
116
|
+
const endpoint = useMemo(() => resolveEndpoint(), []);
|
|
117
|
+
// Persist query and mode to sessionStorage
|
|
118
|
+
const setQuery = useCallback((q) => {
|
|
119
|
+
setQueryRaw(q);
|
|
120
|
+
saveSearchState(q, searchMode);
|
|
121
|
+
}, [searchMode]);
|
|
122
|
+
const setSearchMode = useCallback((m) => {
|
|
123
|
+
setSearchModeRaw(m);
|
|
124
|
+
saveSearchState(query, m);
|
|
125
|
+
}, [query]);
|
|
126
|
+
// Fetch topics on mount for empty-state overview
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (!driveId)
|
|
129
|
+
return;
|
|
130
|
+
graphqlFetch(endpoint, TOPICS_QUERY, { driveId }).then((data) => {
|
|
131
|
+
if (data?.knowledgeGraphTopics) {
|
|
132
|
+
setTopics(data.knowledgeGraphTopics);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}, [driveId, endpoint]);
|
|
136
|
+
// Debounced search
|
|
137
|
+
const executeSearch = useCallback(async (q, mode) => {
|
|
138
|
+
if (!driveId || !q.trim()) {
|
|
139
|
+
setResults([]);
|
|
140
|
+
setLoading(false);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
setLoading(true);
|
|
144
|
+
setError(null);
|
|
145
|
+
if (mode === "semantic") {
|
|
146
|
+
const data = await graphqlFetch(endpoint, SEMANTIC_SEARCH_QUERY, { driveId, query: q, limit: 20 });
|
|
147
|
+
if (data?.knowledgeGraphSemanticSearch) {
|
|
148
|
+
setResults(data.knowledgeGraphSemanticSearch.map((r) => ({
|
|
149
|
+
...r.node,
|
|
150
|
+
similarity: r.similarity,
|
|
151
|
+
})));
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
setResults([]);
|
|
155
|
+
setError("Semantic search unavailable. Try keyword mode.");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
const data = await graphqlFetch(endpoint, KEYWORD_SEARCH_QUERY, { driveId, query: q, limit: 20 });
|
|
160
|
+
if (data?.knowledgeGraphFullSearch) {
|
|
161
|
+
setResults(data.knowledgeGraphFullSearch);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
setResults([]);
|
|
165
|
+
setError("Search failed.");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
setLoading(false);
|
|
169
|
+
}, [driveId, endpoint]);
|
|
170
|
+
// Trigger debounced search on query or mode change
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (debounceRef.current)
|
|
173
|
+
clearTimeout(debounceRef.current);
|
|
174
|
+
if (!query.trim()) {
|
|
175
|
+
setResults([]);
|
|
176
|
+
setLoading(false);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
setLoading(true);
|
|
180
|
+
debounceRef.current = setTimeout(() => {
|
|
181
|
+
void executeSearch(query, searchMode);
|
|
182
|
+
}, DEBOUNCE_MS);
|
|
183
|
+
return () => {
|
|
184
|
+
if (debounceRef.current)
|
|
185
|
+
clearTimeout(debounceRef.current);
|
|
186
|
+
};
|
|
187
|
+
}, [query, searchMode, executeSearch]);
|
|
188
|
+
return {
|
|
189
|
+
query,
|
|
190
|
+
setQuery,
|
|
191
|
+
results,
|
|
192
|
+
topics,
|
|
193
|
+
loading,
|
|
194
|
+
error,
|
|
195
|
+
searchMode,
|
|
196
|
+
setSearchMode,
|
|
197
|
+
};
|
|
198
|
+
}
|
package/dist/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powerhousedao/knowledge-note",
|
|
3
3
|
"description": "Knowledge Note document model package for Powerhouse",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.4",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"files": [
|
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
},
|
|
108
108
|
"dependencies": {
|
|
109
109
|
"@electric-sql/pglite": "0.3.15",
|
|
110
|
+
"@huggingface/transformers": "^4.0.1",
|
|
110
111
|
"@powerhousedao/builder-tools": "6.0.0-dev.105",
|
|
111
112
|
"@powerhousedao/common": "6.0.0-dev.105",
|
|
112
113
|
"@powerhousedao/design-system": "6.0.0-dev.105",
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type FeatureExtractionPipeline } from "@huggingface/transformers";
|
|
2
|
+
export declare function getExtractor(): Promise<FeatureExtractionPipeline>;
|
|
3
|
+
export declare function generateEmbedding(text: string): Promise<number[]>;
|
|
4
|
+
export declare function isEmbedderReady(): boolean;
|
|
5
|
+
//# sourceMappingURL=embedder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embedder.d.ts","sourceRoot":"","sources":["../../../processors/graph-indexer/embedder.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,yBAAyB,EAC/B,MAAM,2BAA2B,CAAC;AAKnC,wBAAsB,YAAY,IAAI,OAAO,CAAC,yBAAyB,CAAC,CAgBvE;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAIvE;AAED,wBAAgB,eAAe,IAAI,OAAO,CAEzC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { pipeline, } from "@huggingface/transformers";
|
|
2
|
+
let extractor = null;
|
|
3
|
+
let loading = null;
|
|
4
|
+
export async function getExtractor() {
|
|
5
|
+
if (extractor)
|
|
6
|
+
return extractor;
|
|
7
|
+
// Prevent concurrent loads — share the same promise
|
|
8
|
+
if (!loading) {
|
|
9
|
+
loading = pipeline("feature-extraction", "Supabase/gte-small", {
|
|
10
|
+
dtype: "q8",
|
|
11
|
+
}).then((ext) => {
|
|
12
|
+
extractor = ext;
|
|
13
|
+
loading = null;
|
|
14
|
+
console.log("[Embedder] Model loaded: Supabase/gte-small (q8)");
|
|
15
|
+
return extractor;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return loading;
|
|
19
|
+
}
|
|
20
|
+
export async function generateEmbedding(text) {
|
|
21
|
+
const ext = await getExtractor();
|
|
22
|
+
const output = await ext(text, { pooling: "mean", normalize: true });
|
|
23
|
+
return Array.from(output.data);
|
|
24
|
+
}
|
|
25
|
+
export function isEmbedderReady() {
|
|
26
|
+
return extractor !== null;
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
2
|
+
export declare function getEmbeddingDb(): Promise<PGlite>;
|
|
3
|
+
export declare function upsertEmbedding(documentId: string, embedding: number[]): Promise<void>;
|
|
4
|
+
export declare function searchSimilar(queryEmbedding: number[], limit?: number): Promise<Array<{
|
|
5
|
+
documentId: string;
|
|
6
|
+
similarity: number;
|
|
7
|
+
}>>;
|
|
8
|
+
export declare function getEmbedding(documentId: string): Promise<number[] | null>;
|
|
9
|
+
export declare function deleteEmbedding(documentId: string): Promise<void>;
|
|
10
|
+
export declare function closeEmbeddingDb(): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=embedding-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embedding-store.d.ts","sourceRoot":"","sources":["../../../processors/graph-indexer/embedding-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAK9C,wBAAsB,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,CA2BtD;AAED,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EAAE,GAClB,OAAO,CAAC,IAAI,CAAC,CAaf;AAED,wBAAsB,aAAa,CACjC,cAAc,EAAE,MAAM,EAAE,EACxB,KAAK,SAAK,GACT,OAAO,CAAC,KAAK,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAmB5D;AAED,wBAAsB,YAAY,CAChC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAa1B;AAED,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKvE;AAED,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAKtD"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
2
|
+
import { vector } from "@electric-sql/pglite/vector";
|
|
3
|
+
let db = null;
|
|
4
|
+
export async function getEmbeddingDb() {
|
|
5
|
+
if (db)
|
|
6
|
+
return db;
|
|
7
|
+
const dataDir = typeof window !== "undefined"
|
|
8
|
+
? "idb://knowledge-embeddings"
|
|
9
|
+
: "./.ph/knowledge-embeddings";
|
|
10
|
+
db = new PGlite({
|
|
11
|
+
dataDir,
|
|
12
|
+
extensions: { vector },
|
|
13
|
+
});
|
|
14
|
+
await db.exec("CREATE EXTENSION IF NOT EXISTS vector;");
|
|
15
|
+
await db.exec(`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS note_embeddings (
|
|
17
|
+
document_id VARCHAR(255) PRIMARY KEY,
|
|
18
|
+
embedding vector(384) NOT NULL,
|
|
19
|
+
updated_at VARCHAR(50) NOT NULL
|
|
20
|
+
);
|
|
21
|
+
`);
|
|
22
|
+
await db.exec(`
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_note_embeddings_hnsw
|
|
24
|
+
ON note_embeddings USING hnsw (embedding vector_cosine_ops);
|
|
25
|
+
`);
|
|
26
|
+
return db;
|
|
27
|
+
}
|
|
28
|
+
export async function upsertEmbedding(documentId, embedding) {
|
|
29
|
+
const store = await getEmbeddingDb();
|
|
30
|
+
const now = new Date().toISOString();
|
|
31
|
+
const embeddingStr = `[${embedding.join(",")}]`;
|
|
32
|
+
await store.query(`INSERT INTO note_embeddings (document_id, embedding, updated_at)
|
|
33
|
+
VALUES ($1, $2::vector, $3)
|
|
34
|
+
ON CONFLICT (document_id) DO UPDATE SET
|
|
35
|
+
embedding = EXCLUDED.embedding,
|
|
36
|
+
updated_at = EXCLUDED.updated_at`, [documentId, embeddingStr, now]);
|
|
37
|
+
}
|
|
38
|
+
export async function searchSimilar(queryEmbedding, limit = 10) {
|
|
39
|
+
const store = await getEmbeddingDb();
|
|
40
|
+
const embeddingStr = `[${queryEmbedding.join(",")}]`;
|
|
41
|
+
const result = await store.query(`SELECT document_id, embedding <=> $1::vector AS distance
|
|
42
|
+
FROM note_embeddings
|
|
43
|
+
ORDER BY distance ASC
|
|
44
|
+
LIMIT $2`, [embeddingStr, limit]);
|
|
45
|
+
return (result.rows ?? []).map((row) => ({
|
|
46
|
+
documentId: row.document_id,
|
|
47
|
+
similarity: 1 - row.distance,
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
export async function getEmbedding(documentId) {
|
|
51
|
+
const store = await getEmbeddingDb();
|
|
52
|
+
const result = await store.query(`SELECT embedding::text FROM note_embeddings WHERE document_id = $1`, [documentId]);
|
|
53
|
+
if (!result.rows || result.rows.length === 0)
|
|
54
|
+
return null;
|
|
55
|
+
// Parse "[0.1,0.2,...]" string back to number[]
|
|
56
|
+
const raw = result.rows[0].embedding;
|
|
57
|
+
return JSON.parse(raw);
|
|
58
|
+
}
|
|
59
|
+
export async function deleteEmbedding(documentId) {
|
|
60
|
+
const store = await getEmbeddingDb();
|
|
61
|
+
await store.query(`DELETE FROM note_embeddings WHERE document_id = $1`, [
|
|
62
|
+
documentId,
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
export async function closeEmbeddingDb() {
|
|
66
|
+
if (db) {
|
|
67
|
+
await db.close();
|
|
68
|
+
db = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../processors/graph-indexer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAEjF,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../processors/graph-indexer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAEjF,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAItC,qBAAa,qBAAsB,SAAQ,qBAAqB,CAAC,EAAE,CAAC;WAClD,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAItC,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAI/B,YAAY,CACzB,UAAU,EAAE,oBAAoB,EAAE,GACjC,OAAO,CAAC,IAAI,CAAC;IAmKV,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;YAQrB,UAAU;CA8BzB"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { RelationalDbProcessor } from "@powerhousedao/shared/processors";
|
|
2
2
|
import { up } from "./migrations.js";
|
|
3
|
+
import { generateEmbedding } from "./embedder.js";
|
|
4
|
+
import { upsertEmbedding, deleteEmbedding } from "./embedding-store.js";
|
|
3
5
|
export class GraphIndexerProcessor extends RelationalDbProcessor {
|
|
4
6
|
static getNamespace(driveId) {
|
|
5
7
|
return super.getNamespace(driveId);
|
|
@@ -41,6 +43,8 @@ export class GraphIndexerProcessor extends RelationalDbProcessor {
|
|
|
41
43
|
// resultingState may be wrapped in { global: ... } or be the global state directly
|
|
42
44
|
const global = (parsed.global ?? parsed);
|
|
43
45
|
const now = new Date().toISOString();
|
|
46
|
+
// Extract provenance
|
|
47
|
+
const provenance = global.provenance;
|
|
44
48
|
// Upsert node
|
|
45
49
|
await this.relationalDb
|
|
46
50
|
.insertInto("graph_nodes")
|
|
@@ -51,6 +55,10 @@ export class GraphIndexerProcessor extends RelationalDbProcessor {
|
|
|
51
55
|
description: global.description ?? null,
|
|
52
56
|
note_type: global.noteType ?? null,
|
|
53
57
|
status: global.status ?? "DRAFT",
|
|
58
|
+
content: global.content ?? null,
|
|
59
|
+
author: provenance?.author ?? null,
|
|
60
|
+
source_origin: provenance?.sourceOrigin ?? null,
|
|
61
|
+
created_at: provenance?.createdAt ?? null,
|
|
54
62
|
updated_at: now,
|
|
55
63
|
})
|
|
56
64
|
.onConflict((oc) => oc.column("document_id").doUpdateSet({
|
|
@@ -58,9 +66,35 @@ export class GraphIndexerProcessor extends RelationalDbProcessor {
|
|
|
58
66
|
description: global.description ?? null,
|
|
59
67
|
note_type: global.noteType ?? null,
|
|
60
68
|
status: global.status ?? "DRAFT",
|
|
69
|
+
content: global.content ?? null,
|
|
70
|
+
author: provenance?.author ?? null,
|
|
71
|
+
source_origin: provenance?.sourceOrigin ?? null,
|
|
72
|
+
created_at: provenance?.createdAt ?? null,
|
|
61
73
|
updated_at: now,
|
|
62
74
|
}))
|
|
63
75
|
.execute();
|
|
76
|
+
// Reconcile topics: delete old, insert new
|
|
77
|
+
await this.relationalDb
|
|
78
|
+
.deleteFrom("graph_topics")
|
|
79
|
+
.where("document_id", "=", documentId)
|
|
80
|
+
.execute();
|
|
81
|
+
const topics = global.topics ?? [];
|
|
82
|
+
if (topics.length > 0) {
|
|
83
|
+
await this.relationalDb
|
|
84
|
+
.insertInto("graph_topics")
|
|
85
|
+
.values(topics.map((topic, idx) => {
|
|
86
|
+
const name = typeof topic === "string"
|
|
87
|
+
? topic
|
|
88
|
+
: (topic.name ?? "");
|
|
89
|
+
return {
|
|
90
|
+
id: `${documentId}-topic-${idx}`,
|
|
91
|
+
document_id: documentId,
|
|
92
|
+
name,
|
|
93
|
+
updated_at: now,
|
|
94
|
+
};
|
|
95
|
+
}))
|
|
96
|
+
.execute();
|
|
97
|
+
}
|
|
64
98
|
// Reconcile edges: delete old, insert new
|
|
65
99
|
await this.relationalDb
|
|
66
100
|
.deleteFrom("graph_edges")
|
|
@@ -82,6 +116,15 @@ export class GraphIndexerProcessor extends RelationalDbProcessor {
|
|
|
82
116
|
.execute();
|
|
83
117
|
}
|
|
84
118
|
console.log(`[GraphIndexer] Reconciled ${documentId}: ${links.length} edges`);
|
|
119
|
+
// Fire-and-forget embedding generation (don't block operation processing)
|
|
120
|
+
const text = [global.title, global.description, global.content]
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
.join(" ");
|
|
123
|
+
if (text.length > 0) {
|
|
124
|
+
generateEmbedding(text)
|
|
125
|
+
.then((emb) => upsertEmbedding(documentId, emb))
|
|
126
|
+
.catch((err) => console.warn(`[GraphIndexer] Embedding failed for ${documentId}:`, err));
|
|
127
|
+
}
|
|
85
128
|
}
|
|
86
129
|
catch (err) {
|
|
87
130
|
console.error(`[GraphIndexer] Error reconciling document ${documentId}:`, err);
|
|
@@ -97,6 +140,10 @@ export class GraphIndexerProcessor extends RelationalDbProcessor {
|
|
|
97
140
|
}
|
|
98
141
|
async deleteNode(documentId) {
|
|
99
142
|
try {
|
|
143
|
+
await this.relationalDb
|
|
144
|
+
.deleteFrom("graph_topics")
|
|
145
|
+
.where("document_id", "=", documentId)
|
|
146
|
+
.execute();
|
|
100
147
|
await this.relationalDb
|
|
101
148
|
.deleteFrom("graph_edges")
|
|
102
149
|
.where((eb) => eb.or([
|
|
@@ -108,6 +155,7 @@ export class GraphIndexerProcessor extends RelationalDbProcessor {
|
|
|
108
155
|
.deleteFrom("graph_nodes")
|
|
109
156
|
.where("document_id", "=", documentId)
|
|
110
157
|
.execute();
|
|
158
|
+
deleteEmbedding(documentId).catch((err) => console.warn(`[GraphIndexer] Embedding delete failed for ${documentId}:`, err));
|
|
111
159
|
console.log(`[GraphIndexer] Deleted node ${documentId}`);
|
|
112
160
|
}
|
|
113
161
|
catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../../processors/graph-indexer/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAEtE,wBAAsB,EAAE,CAAC,EAAE,EAAE,aAAa,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../../processors/graph-indexer/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAEtE,wBAAsB,EAAE,CAAC,EAAE,EAAE,aAAa,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAmF9D;AAED,wBAAsB,IAAI,CAAC,EAAE,EAAE,aAAa,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhE"}
|
|
@@ -39,8 +39,43 @@ export async function up(db) {
|
|
|
39
39
|
.column("status")
|
|
40
40
|
.ifNotExists()
|
|
41
41
|
.execute();
|
|
42
|
+
// --- Phase 1: topics, content, provenance ---
|
|
43
|
+
// Add new columns to graph_nodes (wrap each in try/catch for idempotency)
|
|
44
|
+
for (const col of ["content", "author", "source_origin", "created_at"]) {
|
|
45
|
+
try {
|
|
46
|
+
await db.schema
|
|
47
|
+
.alterTable("graph_nodes")
|
|
48
|
+
.addColumn(col, col === "content" ? "text" : "varchar(1024)")
|
|
49
|
+
.execute();
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// column likely already exists — ignore
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Create graph_topics table
|
|
56
|
+
await db.schema
|
|
57
|
+
.createTable("graph_topics")
|
|
58
|
+
.addColumn("id", "varchar(255)", (col) => col.primaryKey())
|
|
59
|
+
.addColumn("document_id", "varchar(255)", (col) => col.notNull())
|
|
60
|
+
.addColumn("name", "varchar(512)", (col) => col.notNull())
|
|
61
|
+
.addColumn("updated_at", "varchar(50)", (col) => col.notNull())
|
|
62
|
+
.ifNotExists()
|
|
63
|
+
.execute();
|
|
64
|
+
await db.schema
|
|
65
|
+
.createIndex("idx_graph_topics_document_id")
|
|
66
|
+
.on("graph_topics")
|
|
67
|
+
.column("document_id")
|
|
68
|
+
.ifNotExists()
|
|
69
|
+
.execute();
|
|
70
|
+
await db.schema
|
|
71
|
+
.createIndex("idx_graph_topics_name")
|
|
72
|
+
.on("graph_topics")
|
|
73
|
+
.column("name")
|
|
74
|
+
.ifNotExists()
|
|
75
|
+
.execute();
|
|
42
76
|
}
|
|
43
77
|
export async function down(db) {
|
|
78
|
+
await db.schema.dropTable("graph_topics").ifExists().execute();
|
|
44
79
|
await db.schema.dropTable("graph_edges").ifExists().execute();
|
|
45
80
|
await db.schema.dropTable("graph_nodes").ifExists().execute();
|
|
46
81
|
}
|