mop-agent 0.1.15 โ 0.1.16
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 +13 -5
- package/apps/web/app/api/apps/route.ts +23 -0
- package/apps/web/app/api/chat/route.ts +26 -4
- package/apps/web/app/api/graph/route.ts +8 -3
- package/apps/web/app/assistant/page.tsx +14 -19
- package/apps/web/app/brain/graph/page.tsx +144 -28
- package/apps/web/app/brain/page.tsx +106 -104
- package/apps/web/app/globals.css +418 -2
- package/apps/web/app/settings/page.tsx +102 -1
- package/apps/web/components/AppShell.tsx +75 -17
- package/apps/web/components/ui/chatgpt-prompt-input.tsx +290 -0
- package/apps/web/components.json +18 -0
- package/apps/web/lib/channels/config.ts +48 -0
- package/apps/web/lib/channels/index.ts +8 -4
- package/apps/web/lib/db/migrate.ts +9 -0
- package/apps/web/lib/providers/anthropic.ts +20 -1
- package/apps/web/lib/providers/openrouter.ts +11 -1
- package/apps/web/lib/providers/types.ts +2 -1
- package/apps/web/package.json +6 -0
- package/apps/web/postcss.config.mjs +5 -0
- package/npm-shrinkwrap.json +1646 -222
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -5,17 +5,25 @@ through MOP-FLOW. It stores project memory, performs semantic recall and
|
|
|
5
5
|
consolidation, serves grounded chat, and can request approved actions from a
|
|
6
6
|
linked FLOW node.
|
|
7
7
|
|
|
8
|
-
> **Release status:** release candidate `mop-agent@0.1.
|
|
9
|
-
> installer, one-time Admin setup/login flow,
|
|
10
|
-
>
|
|
8
|
+
> **Release status:** release candidate `mop-agent@0.1.16` contains the corrected VPS
|
|
9
|
+
> installer, one-time Admin setup/login flow, rich Assistant composer, Main Brain
|
|
10
|
+
> workspace, Obsidian-inspired Graph View, and encrypted Apps settings.
|
|
11
11
|
> The canonical installation command is exactly `npx mop-agent`.
|
|
12
12
|
|
|
13
13
|
## Current status
|
|
14
14
|
|
|
15
15
|
The application core through Fasa 7 foundation is implemented: reverse-WSS
|
|
16
16
|
project links, SQLite + sqlite-vec storage, Better Auth, semantic recall,
|
|
17
|
-
admin-only provider/user settings, consolidation, approval-based write-back,
|
|
18
|
-
Telegram and Discord adapters, skills, graph UI, execution backends, and user
|
|
17
|
+
admin-only provider/user/app settings, consolidation, approval-based write-back,
|
|
18
|
+
Telegram and Discord adapters, skills, graph UI, execution backends, and user accounts.
|
|
19
|
+
|
|
20
|
+
The Assistant supports an autosizing prompt, image attachment/preview, voice input
|
|
21
|
+
(when the browser exposes Web Speech), and focused tool modes. Anthropic and
|
|
22
|
+
OpenRouter receive attached images as multimodal input. Brain treats Main Brain as
|
|
23
|
+
the primary knowledge layer and provides an interactive, searchable Graph View.
|
|
24
|
+
Telegram and Discord credentials can be stored encrypted under **Settings โ Apps**;
|
|
25
|
+
their adapters become active after the service restarts. WhatsApp, Slack, and generic
|
|
26
|
+
webhook configuration can be stored there now while their runtime adapters remain planned.
|
|
19
27
|
|
|
20
28
|
The npm bootstrap stages the packaged application durably at `/opt/mop-agent`,
|
|
21
29
|
uses the proven SQLite + sqlite-vec backend, and asks for sudo only for specific
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { requireRole } from "@/lib/authz";
|
|
2
|
+
import { listAppConfigs, saveAppConfig, type AppId } from "@/lib/channels/config";
|
|
3
|
+
|
|
4
|
+
const APP_IDS = new Set<AppId>(["telegram", "discord", "whatsapp", "slack", "webhook"]);
|
|
5
|
+
|
|
6
|
+
export async function GET(req: Request): Promise<Response> {
|
|
7
|
+
const auth = await requireRole(req, ["owner"]);
|
|
8
|
+
if (!auth.ok) return auth.response;
|
|
9
|
+
return Response.json({ apps: listAppConfigs(auth.userId) });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function POST(req: Request): Promise<Response> {
|
|
13
|
+
const auth = await requireRole(req, ["owner"]);
|
|
14
|
+
if (!auth.ok) return auth.response;
|
|
15
|
+
const body = (await req.json()) as { appId?: AppId; secret?: string; fields?: Record<string, string>; enabled?: boolean };
|
|
16
|
+
if (!body.appId || !APP_IDS.has(body.appId)) return Response.json({ error: "unknown_app" }, { status: 400 });
|
|
17
|
+
try {
|
|
18
|
+
saveAppConfig(auth.userId, { appId: body.appId, secret: body.secret, fields: body.fields, enabled: body.enabled !== false });
|
|
19
|
+
return Response.json({ apps: listAppConfigs(auth.userId), restartRequired: true });
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return Response.json({ error: error instanceof Error ? error.message : "save_failed" }, { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -6,23 +6,44 @@
|
|
|
6
6
|
import { auth } from "@/lib/auth";
|
|
7
7
|
import { recall } from "@/lib/brain/broker";
|
|
8
8
|
import { resolveProvider } from "@/lib/providers";
|
|
9
|
+
import type { ChatImage } from "@/lib/providers/types";
|
|
10
|
+
|
|
11
|
+
type ChatTool = "image" | "web" | "code" | "research" | "think";
|
|
12
|
+
|
|
13
|
+
const TOOL_INSTRUCTIONS: Record<ChatTool, string> = {
|
|
14
|
+
image: "The user selected image creation. Produce a precise image-generation brief or image-oriented response.",
|
|
15
|
+
web: "The user selected web search. Clearly separate recalled knowledge from facts that require a live web source; never invent browsing results.",
|
|
16
|
+
code: "The user selected writing/code. Give implementation-ready, technically precise output.",
|
|
17
|
+
research: "The user selected deep research. Analyze methodically, compare alternatives, and state uncertainties.",
|
|
18
|
+
think: "The user selected extended thinking. Check assumptions carefully before answering.",
|
|
19
|
+
};
|
|
9
20
|
|
|
10
21
|
export async function POST(req: Request): Promise<Response> {
|
|
11
22
|
const session = await auth.api.getSession({ headers: req.headers });
|
|
12
23
|
if (!session) return Response.json({ error: "unauthorized" }, { status: 401 });
|
|
13
24
|
|
|
14
|
-
const { projectId, message, allowCrossProject } = (await req.json()) as {
|
|
25
|
+
const { projectId, message, allowCrossProject, tool, image } = (await req.json()) as {
|
|
15
26
|
projectId?: string;
|
|
16
27
|
message: string;
|
|
17
28
|
allowCrossProject?: boolean;
|
|
29
|
+
tool?: ChatTool | null;
|
|
30
|
+
image?: ChatImage | null;
|
|
18
31
|
};
|
|
19
|
-
if (typeof message !== "string" || !message.trim()) {
|
|
32
|
+
if ((typeof message !== "string" || !message.trim()) && !image) {
|
|
20
33
|
return Response.json({ error: "missing_message" }, { status: 400 });
|
|
21
34
|
}
|
|
35
|
+
if (tool && !Object.hasOwn(TOOL_INSTRUCTIONS, tool)) {
|
|
36
|
+
return Response.json({ error: "unknown_tool" }, { status: 400 });
|
|
37
|
+
}
|
|
38
|
+
if (image && (!image.dataUrl?.startsWith("data:image/") || image.dataUrl.length > 7_000_000)) {
|
|
39
|
+
return Response.json({ error: "invalid_or_oversized_image" }, { status: 400 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const userMessage = message.trim() || "Help me understand this attached image.";
|
|
22
43
|
|
|
23
44
|
const centralAssistant = !projectId;
|
|
24
45
|
const pack = await recall({
|
|
25
|
-
query:
|
|
46
|
+
query: userMessage,
|
|
26
47
|
projectId,
|
|
27
48
|
// The main assistant is the authenticated, cross-project surface.
|
|
28
49
|
allowCrossProject: centralAssistant || !!allowCrossProject,
|
|
@@ -34,6 +55,7 @@ export async function POST(req: Request): Promise<Response> {
|
|
|
34
55
|
centralAssistant
|
|
35
56
|
? "Help the user directly. Use the available cross-project memory when relevant; a linked project is not required."
|
|
36
57
|
: "Help the user with the selected project and use its memory when relevant.",
|
|
58
|
+
tool ? TOOL_INSTRUCTIONS[tool] : "",
|
|
37
59
|
"",
|
|
38
60
|
pack.toPromptString(),
|
|
39
61
|
].join("\n");
|
|
@@ -42,7 +64,7 @@ export async function POST(req: Request): Promise<Response> {
|
|
|
42
64
|
const stream = new ReadableStream<Uint8Array>({
|
|
43
65
|
async start(controller) {
|
|
44
66
|
try {
|
|
45
|
-
for await (const delta of provider.chat({ system, messages: [{ role: "user", content:
|
|
67
|
+
for await (const delta of provider.chat({ system, messages: [{ role: "user", content: userMessage, image: image ?? undefined }] })) {
|
|
46
68
|
controller.enqueue(encoder.encode(delta));
|
|
47
69
|
}
|
|
48
70
|
} catch (e) {
|
|
@@ -8,25 +8,30 @@ import { listProjects } from "@/lib/link/store";
|
|
|
8
8
|
import { listSemanticNotes } from "@/lib/brain/consolidate";
|
|
9
9
|
import { listSkills } from "@/lib/brain/skills";
|
|
10
10
|
|
|
11
|
-
export type GraphNode = { id: string; label: string; type: "project" | "pattern" | "skill" };
|
|
11
|
+
export type GraphNode = { id: string; label: string; type: "main" | "project" | "pattern" | "skill" };
|
|
12
12
|
export type GraphEdge = { from: string; to: string };
|
|
13
13
|
|
|
14
14
|
export async function GET(req: Request): Promise<Response> {
|
|
15
15
|
const session = await auth.api.getSession({ headers: req.headers });
|
|
16
16
|
if (!session) return Response.json({ error: "unauthorized" }, { status: 401 });
|
|
17
17
|
|
|
18
|
-
const nodes: GraphNode[] = [];
|
|
18
|
+
const nodes: GraphNode[] = [{ id: "main-brain", label: "Main Brain", type: "main" }];
|
|
19
19
|
const edges: GraphEdge[] = [];
|
|
20
20
|
|
|
21
|
-
for (const p of listProjects())
|
|
21
|
+
for (const p of listProjects()) {
|
|
22
|
+
nodes.push({ id: `project:${p.id}`, label: p.name, type: "project" });
|
|
23
|
+
edges.push({ from: "main-brain", to: `project:${p.id}` });
|
|
24
|
+
}
|
|
22
25
|
|
|
23
26
|
for (const n of listSemanticNotes()) {
|
|
24
27
|
nodes.push({ id: `pattern:${n.id}`, label: n.title, type: "pattern" });
|
|
28
|
+
edges.push({ from: "main-brain", to: `pattern:${n.id}` });
|
|
25
29
|
for (const pid of n.sourceProjects ?? []) edges.push({ from: `pattern:${n.id}`, to: `project:${pid}` });
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
for (const s of listSkills()) {
|
|
29
33
|
nodes.push({ id: `skill:${s.id}`, label: s.name, type: "skill" });
|
|
34
|
+
edges.push({ from: "main-brain", to: `skill:${s.id}` });
|
|
30
35
|
for (const pid of s.sourceProjects ?? []) edges.push({ from: `skill:${s.id}`, to: `project:${pid}` });
|
|
31
36
|
}
|
|
32
37
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { CSSProperties } from "react";
|
|
4
4
|
import { useEffect, useRef, useState } from "react";
|
|
5
5
|
import { useMemoryCore } from "@/components/AppShell";
|
|
6
|
+
import { PromptBox, type PromptSubmit } from "@/components/ui/chatgpt-prompt-input";
|
|
6
7
|
|
|
7
8
|
type Turn = { role: "user" | "assistant"; content: string };
|
|
8
9
|
type SavedChat = { id: string; title: string; turns: Turn[]; updatedAt: number };
|
|
@@ -87,13 +88,17 @@ export default function AssistantPage() {
|
|
|
87
88
|
});
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
async function send(
|
|
91
|
-
const
|
|
92
|
-
|
|
91
|
+
async function send(request?: string | PromptSubmit) {
|
|
92
|
+
const payload: PromptSubmit = typeof request === "string"
|
|
93
|
+
? { message: request, tool: null, image: null }
|
|
94
|
+
: request ?? { message: input, tool: null, image: null };
|
|
95
|
+
const message = payload.message.trim();
|
|
96
|
+
if ((!message && !payload.image) || busy) return false;
|
|
97
|
+
const visibleMessage = [message, payload.image ? `๐ ${payload.image.name}` : ""].filter(Boolean).join("\n");
|
|
93
98
|
if (!activeChatId) setActiveChatId(`chat-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
94
99
|
setInput("");
|
|
95
100
|
setBusy(true);
|
|
96
|
-
setTurns((current) => [...current, { role: "user", content:
|
|
101
|
+
setTurns((current) => [...current, { role: "user", content: visibleMessage }, { role: "assistant", content: "" }]);
|
|
97
102
|
|
|
98
103
|
const res = await fetch("/api/chat", {
|
|
99
104
|
method: "POST",
|
|
@@ -101,6 +106,8 @@ export default function AssistantPage() {
|
|
|
101
106
|
body: JSON.stringify({
|
|
102
107
|
message,
|
|
103
108
|
allowCrossProject: true,
|
|
109
|
+
tool: payload.tool,
|
|
110
|
+
image: payload.image,
|
|
104
111
|
}),
|
|
105
112
|
});
|
|
106
113
|
|
|
@@ -112,7 +119,7 @@ export default function AssistantPage() {
|
|
|
112
119
|
return next;
|
|
113
120
|
});
|
|
114
121
|
setBusy(false);
|
|
115
|
-
return;
|
|
122
|
+
return true;
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
const reader = res.body.getReader();
|
|
@@ -130,6 +137,7 @@ export default function AssistantPage() {
|
|
|
130
137
|
endRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
131
138
|
}
|
|
132
139
|
setBusy(false);
|
|
140
|
+
return true;
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
return (
|
|
@@ -174,17 +182,7 @@ export default function AssistantPage() {
|
|
|
174
182
|
</div>
|
|
175
183
|
|
|
176
184
|
<div className="mop-assistant-composer-wrap">
|
|
177
|
-
<
|
|
178
|
-
<textarea
|
|
179
|
-
value={input}
|
|
180
|
-
onChange={(e) => setInput(e.target.value)}
|
|
181
|
-
onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }}
|
|
182
|
-
placeholder="Message MOP-AGENTโฆ"
|
|
183
|
-
rows={1}
|
|
184
|
-
style={textarea}
|
|
185
|
-
/>
|
|
186
|
-
<button onClick={() => send()} disabled={busy || !input.trim()} style={{ ...sendButton, opacity: busy || !input.trim() ? .45 : 1 }}>โ</button>
|
|
187
|
-
</div>
|
|
185
|
+
<PromptBox value={input} onValueChange={setInput} onSubmit={send} busy={busy} />
|
|
188
186
|
<div style={{ textAlign: "center", color: "rgba(45,74,62,.62)", fontSize: 11, marginTop: 8 }}>
|
|
189
187
|
{providerUsed ? `Answered by ${providerUsed} ยท ` : ""}Cross-project memory
|
|
190
188
|
</div>
|
|
@@ -237,6 +235,3 @@ const promptGrid: CSSProperties = { width: "min(100%, 650px)", display: "grid",
|
|
|
237
235
|
const promptCard: CSSProperties = { display: "flex", justifyContent: "space-between", padding: "14px 15px", border: "1px solid rgba(45,74,62,.38)", borderBottomWidth: 3, background: "#fffdf2", color: "#2d4a3e", cursor: "pointer", textAlign: "left" };
|
|
238
236
|
const botAvatar: CSSProperties = { width: 32, height: 32, display: "grid", placeItems: "center", background: "#742220", color: "#fef9e1" };
|
|
239
237
|
const userAvatar: CSSProperties = { ...botAvatar, background: "#2d4a3e", fontSize: 12 };
|
|
240
|
-
const composer: CSSProperties = { width: "min(calc(100% - 32px), 800px)", margin: "0 auto", display: "flex", alignItems: "flex-end", gap: 10, padding: "10px 10px 10px 15px", border: "1px solid rgba(45,74,62,.48)", borderBottomWidth: 4, background: "#fffdf2", boxShadow: "4px 4px 0 rgba(45,74,62,.13)" };
|
|
241
|
-
const textarea: CSSProperties = { flex: 1, resize: "none", border: 0, outline: 0, boxShadow: "none", background: "transparent", color: "#2d4a3e", font: "inherit", lineHeight: 1.55, padding: "5px 0" };
|
|
242
|
-
const sendButton: CSSProperties = { width: 34, height: 34, border: 0, background: "#742220", color: "#fef9e1", fontSize: 18, cursor: "pointer" };
|
|
@@ -1,54 +1,170 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import { useEffect, useMemo, useState } from "react";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
Background,
|
|
6
|
+
Controls,
|
|
7
|
+
Handle,
|
|
8
|
+
MiniMap,
|
|
9
|
+
Position,
|
|
10
|
+
ReactFlow,
|
|
11
|
+
type Edge,
|
|
12
|
+
type Node,
|
|
13
|
+
type NodeMouseHandler,
|
|
14
|
+
type NodeProps,
|
|
15
|
+
} from "reactflow";
|
|
5
16
|
import "reactflow/dist/style.css";
|
|
6
17
|
|
|
7
|
-
type
|
|
18
|
+
type MemoryNodeType = "main" | "project" | "pattern" | "skill";
|
|
19
|
+
type GNode = { id: string; label: string; type: MemoryNodeType };
|
|
8
20
|
type GEdge = { from: string; to: string };
|
|
21
|
+
type MemoryNodeData = { label: string; memoryType: MemoryNodeType };
|
|
22
|
+
|
|
23
|
+
const COLORS: Record<MemoryNodeType, string> = {
|
|
24
|
+
main: "#e4b83f",
|
|
25
|
+
project: "#d74c45",
|
|
26
|
+
pattern: "#4fa676",
|
|
27
|
+
skill: "#4d91c9",
|
|
28
|
+
};
|
|
9
29
|
|
|
10
|
-
const
|
|
30
|
+
const nodeTypes = { memory: MemoryGraphNode };
|
|
11
31
|
|
|
12
32
|
export default function GraphPage() {
|
|
13
33
|
const [data, setData] = useState<{ nodes: GNode[]; edges: GEdge[] }>({ nodes: [], edges: [] });
|
|
34
|
+
const [query, setQuery] = useState("");
|
|
35
|
+
const [filters, setFilters] = useState<Record<MemoryNodeType, boolean>>({ main: true, project: true, pattern: true, skill: true });
|
|
36
|
+
const [selected, setSelected] = useState<GNode | null>(null);
|
|
37
|
+
const [labelsVisible, setLabelsVisible] = useState(true);
|
|
14
38
|
|
|
15
39
|
useEffect(() => {
|
|
16
|
-
fetch("/api/graph")
|
|
40
|
+
fetch("/api/graph")
|
|
41
|
+
.then((response) => response.json())
|
|
42
|
+
.then((result) => setData({ nodes: result.nodes ?? [], edges: result.edges ?? [] }))
|
|
43
|
+
.catch(() => {});
|
|
17
44
|
}, []);
|
|
18
45
|
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
46
|
+
const visibleData = useMemo(() => {
|
|
47
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
48
|
+
const directMatches = new Set(
|
|
49
|
+
data.nodes
|
|
50
|
+
.filter((node) => filters[node.type] && (!normalizedQuery || node.label.toLowerCase().includes(normalizedQuery)))
|
|
51
|
+
.map((node) => node.id),
|
|
52
|
+
);
|
|
53
|
+
if (normalizedQuery && directMatches.size) directMatches.add("main-brain");
|
|
54
|
+
return data.nodes.filter((node) => filters[node.type] && (!normalizedQuery || directMatches.has(node.id)));
|
|
55
|
+
}, [data.nodes, filters, query]);
|
|
56
|
+
|
|
57
|
+
const nodes: Node<MemoryNodeData>[] = useMemo(() => {
|
|
58
|
+
const grouped = new Map<MemoryNodeType, GNode[]>();
|
|
59
|
+
for (const type of ["main", "project", "pattern", "skill"] as const) {
|
|
60
|
+
grouped.set(type, visibleData.filter((node) => node.type === type));
|
|
61
|
+
}
|
|
62
|
+
return visibleData.map((node) => {
|
|
63
|
+
if (node.type === "main") {
|
|
64
|
+
return { id: node.id, type: "memory", position: { x: 0, y: 0 }, data: { label: node.label, memoryType: node.type } };
|
|
65
|
+
}
|
|
66
|
+
const peers = grouped.get(node.type) ?? [];
|
|
67
|
+
const index = peers.findIndex((peer) => peer.id === node.id);
|
|
68
|
+
const radius = node.type === "project" ? 250 : node.type === "pattern" ? 430 : 590;
|
|
69
|
+
const phase = node.type === "project" ? 0 : node.type === "pattern" ? 0.35 : 0.7;
|
|
70
|
+
const angle = phase + (Math.PI * 2 * Math.max(index, 0)) / Math.max(peers.length, 1);
|
|
25
71
|
return {
|
|
26
|
-
id:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
72
|
+
id: node.id,
|
|
73
|
+
type: "memory",
|
|
74
|
+
position: { x: Math.cos(angle) * radius, y: Math.sin(angle) * radius },
|
|
75
|
+
data: { label: node.label, memoryType: node.type },
|
|
30
76
|
};
|
|
31
77
|
});
|
|
32
|
-
}, [
|
|
78
|
+
}, [visibleData]);
|
|
33
79
|
|
|
80
|
+
const nodeIds = useMemo(() => new Set(nodes.map((node) => node.id)), [nodes]);
|
|
34
81
|
const edges: Edge[] = useMemo(
|
|
35
|
-
() => data.edges
|
|
36
|
-
|
|
82
|
+
() => data.edges
|
|
83
|
+
.filter((edge) => nodeIds.has(edge.from) && nodeIds.has(edge.to))
|
|
84
|
+
.map((edge, index) => ({
|
|
85
|
+
id: `edge-${index}-${edge.from}-${edge.to}`,
|
|
86
|
+
source: edge.from,
|
|
87
|
+
target: edge.to,
|
|
88
|
+
style: { stroke: "rgba(215, 210, 190, .24)", strokeWidth: edge.from === "main-brain" ? 1.25 : .75 },
|
|
89
|
+
})),
|
|
90
|
+
[data.edges, nodeIds],
|
|
37
91
|
);
|
|
38
92
|
|
|
93
|
+
const onNodeClick: NodeMouseHandler = (_, node) => {
|
|
94
|
+
setSelected(data.nodes.find((item) => item.id === node.id) ?? null);
|
|
95
|
+
};
|
|
96
|
+
|
|
39
97
|
return (
|
|
40
|
-
<main
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
<
|
|
98
|
+
<main className={`mop-graph-view${labelsVisible ? " show-labels" : " hide-labels"}`}>
|
|
99
|
+
<header className="mop-graph-toolbar">
|
|
100
|
+
<div className="mop-graph-toolbar-left">
|
|
101
|
+
<a href="/brain" aria-label="Back to Brain">โ</a>
|
|
102
|
+
<strong>Graph view</strong>
|
|
103
|
+
<span>{nodes.length} nodes ยท {edges.length} links</span>
|
|
104
|
+
</div>
|
|
105
|
+
<label className="mop-graph-search">
|
|
106
|
+
<span>โ</span>
|
|
107
|
+
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search memory graph" />
|
|
108
|
+
</label>
|
|
109
|
+
<button type="button" onClick={() => setLabelsVisible((visible) => !visible)}>{labelsVisible ? "Hide labels" : "Show labels"}</button>
|
|
110
|
+
</header>
|
|
111
|
+
|
|
112
|
+
<div className="mop-graph-canvas">
|
|
113
|
+
<ReactFlow
|
|
114
|
+
nodes={nodes}
|
|
115
|
+
edges={edges}
|
|
116
|
+
nodeTypes={nodeTypes}
|
|
117
|
+
onNodeClick={onNodeClick}
|
|
118
|
+
fitView
|
|
119
|
+
fitViewOptions={{ padding: .22 }}
|
|
120
|
+
minZoom={0.08}
|
|
121
|
+
maxZoom={2.4}
|
|
122
|
+
nodesConnectable={false}
|
|
123
|
+
proOptions={{ hideAttribution: true }}
|
|
124
|
+
>
|
|
125
|
+
<Background color="rgba(255,255,255,.08)" gap={28} size={1} />
|
|
126
|
+
<MiniMap
|
|
127
|
+
pannable
|
|
128
|
+
zoomable
|
|
129
|
+
nodeColor={(node) => COLORS[(node.data as MemoryNodeData).memoryType]}
|
|
130
|
+
maskColor="rgba(14,15,15,.72)"
|
|
131
|
+
style={{ background: "#171918", border: "1px solid rgba(255,255,255,.12)" }}
|
|
132
|
+
/>
|
|
133
|
+
<Controls showInteractive={false} />
|
|
50
134
|
</ReactFlow>
|
|
135
|
+
|
|
136
|
+
<aside className="mop-graph-filters">
|
|
137
|
+
<strong>Graph filters</strong>
|
|
138
|
+
{(["main", "project", "pattern", "skill"] as const).map((type) => (
|
|
139
|
+
<label key={type}>
|
|
140
|
+
<input type="checkbox" checked={filters[type]} onChange={() => setFilters((current) => ({ ...current, [type]: !current[type] }))} />
|
|
141
|
+
<span style={{ background: COLORS[type] }} />
|
|
142
|
+
{type === "main" ? "Main Brain" : `${type.charAt(0).toUpperCase()}${type.slice(1)}s`}
|
|
143
|
+
</label>
|
|
144
|
+
))}
|
|
145
|
+
</aside>
|
|
146
|
+
|
|
147
|
+
{selected && (
|
|
148
|
+
<aside className="mop-graph-inspector">
|
|
149
|
+
<button type="button" aria-label="Close inspector" onClick={() => setSelected(null)}>ร</button>
|
|
150
|
+
<span style={{ background: COLORS[selected.type] }} />
|
|
151
|
+
<small>{selected.type === "main" ? "CENTRAL MEMORY" : selected.type.toUpperCase()}</small>
|
|
152
|
+
<strong>{selected.label}</strong>
|
|
153
|
+
<p>{selected.type === "main" ? "Shared semantic memory and the root of every linked project." : "Connected knowledge within MOP MemoryCore."}</p>
|
|
154
|
+
</aside>
|
|
155
|
+
)}
|
|
51
156
|
</div>
|
|
52
157
|
</main>
|
|
53
158
|
);
|
|
54
159
|
}
|
|
160
|
+
|
|
161
|
+
function MemoryGraphNode({ data, selected }: NodeProps<MemoryNodeData>) {
|
|
162
|
+
return (
|
|
163
|
+
<div className={`mop-memory-graph-node is-${data.memoryType}${selected ? " is-selected" : ""}`}>
|
|
164
|
+
<Handle type="target" position={Position.Top} className="mop-graph-handle" />
|
|
165
|
+
<span className="mop-memory-node-dot" style={{ background: COLORS[data.memoryType] }} />
|
|
166
|
+
<span className="mop-memory-node-label">{data.label}</span>
|
|
167
|
+
<Handle type="source" position={Position.Bottom} className="mop-graph-handle" />
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|