@cryptiklemur/lattice 0.0.0
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/.editorconfig +12 -0
- package/.github/workflows/release.yml +44 -0
- package/.impeccable.md +66 -0
- package/.releaserc.json +32 -0
- package/.serena/project.yml +138 -0
- package/CLAUDE.md +35 -0
- package/CONTRIBUTING.md +93 -0
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/bun.lock +1459 -0
- package/bunfig.toml +2 -0
- package/client/index.html +32 -0
- package/client/package.json +37 -0
- package/client/public/icons/icon-192.svg +11 -0
- package/client/public/icons/icon-512.svg +11 -0
- package/client/public/manifest.json +24 -0
- package/client/public/sw.js +61 -0
- package/client/src/App.tsx +28 -0
- package/client/src/components/auth/PassphrasePrompt.tsx +70 -0
- package/client/src/components/chat/ChatInput.tsx +241 -0
- package/client/src/components/chat/ChatView.tsx +727 -0
- package/client/src/components/chat/Message.tsx +362 -0
- package/client/src/components/chat/ModelSelector.tsx +87 -0
- package/client/src/components/chat/PermissionModeSelector.tsx +41 -0
- package/client/src/components/chat/StatusBar.tsx +50 -0
- package/client/src/components/chat/ToolGroup.tsx +129 -0
- package/client/src/components/chat/ToolResultRenderer.tsx +343 -0
- package/client/src/components/chat/toolSummary.ts +41 -0
- package/client/src/components/dashboard/DashboardView.tsx +219 -0
- package/client/src/components/dashboard/ProjectDashboardView.tsx +168 -0
- package/client/src/components/mesh/NodeBadge.tsx +24 -0
- package/client/src/components/mesh/PairingDialog.tsx +281 -0
- package/client/src/components/panels/FileBrowser.tsx +241 -0
- package/client/src/components/panels/StickyNotes.tsx +187 -0
- package/client/src/components/panels/Terminal.tsx +128 -0
- package/client/src/components/project-settings/ProjectClaude.tsx +304 -0
- package/client/src/components/project-settings/ProjectEnvironment.tsx +235 -0
- package/client/src/components/project-settings/ProjectGeneral.tsx +76 -0
- package/client/src/components/project-settings/ProjectMcp.tsx +232 -0
- package/client/src/components/project-settings/ProjectPermissions.tsx +209 -0
- package/client/src/components/project-settings/ProjectRules.tsx +277 -0
- package/client/src/components/project-settings/ProjectSettingsView.tsx +99 -0
- package/client/src/components/project-settings/ProjectSkills.tsx +91 -0
- package/client/src/components/settings/Appearance.tsx +151 -0
- package/client/src/components/settings/ClaudeSettings.tsx +151 -0
- package/client/src/components/settings/Environment.tsx +185 -0
- package/client/src/components/settings/GlobalMcp.tsx +207 -0
- package/client/src/components/settings/GlobalSkills.tsx +125 -0
- package/client/src/components/settings/MeshStatus.tsx +145 -0
- package/client/src/components/settings/SettingsView.tsx +57 -0
- package/client/src/components/settings/SkillMarketplace.tsx +175 -0
- package/client/src/components/settings/mcp-shared.tsx +194 -0
- package/client/src/components/settings/skill-shared.tsx +177 -0
- package/client/src/components/setup/SetupWizard.tsx +750 -0
- package/client/src/components/sidebar/NodeSettingsModal.tsx +180 -0
- package/client/src/components/sidebar/ProjectDropdown.tsx +43 -0
- package/client/src/components/sidebar/ProjectRail.tsx +291 -0
- package/client/src/components/sidebar/SearchFilter.tsx +52 -0
- package/client/src/components/sidebar/SessionList.tsx +384 -0
- package/client/src/components/sidebar/SettingsSidebar.tsx +128 -0
- package/client/src/components/sidebar/Sidebar.tsx +209 -0
- package/client/src/components/sidebar/UserIsland.tsx +59 -0
- package/client/src/components/sidebar/UserMenu.tsx +101 -0
- package/client/src/components/ui/CommandPalette.tsx +321 -0
- package/client/src/components/ui/ErrorBoundary.tsx +56 -0
- package/client/src/components/ui/IconPicker.tsx +209 -0
- package/client/src/components/ui/LatticeLogomark.tsx +19 -0
- package/client/src/components/ui/PopupMenu.tsx +98 -0
- package/client/src/components/ui/SaveFooter.tsx +38 -0
- package/client/src/components/ui/Toast.tsx +112 -0
- package/client/src/hooks/useMesh.ts +89 -0
- package/client/src/hooks/useProjectSettings.ts +56 -0
- package/client/src/hooks/useProjects.ts +66 -0
- package/client/src/hooks/useSaveState.ts +59 -0
- package/client/src/hooks/useSession.ts +317 -0
- package/client/src/hooks/useSidebar.ts +74 -0
- package/client/src/hooks/useSkills.ts +30 -0
- package/client/src/hooks/useTheme.ts +114 -0
- package/client/src/hooks/useWebSocket.ts +26 -0
- package/client/src/main.tsx +10 -0
- package/client/src/providers/WebSocketProvider.tsx +146 -0
- package/client/src/router.tsx +391 -0
- package/client/src/stores/mesh.ts +78 -0
- package/client/src/stores/session.ts +322 -0
- package/client/src/stores/sidebar.ts +336 -0
- package/client/src/stores/theme.ts +44 -0
- package/client/src/styles/global.css +167 -0
- package/client/src/styles/theme-vars.css +18 -0
- package/client/src/themes/index.ts +79 -0
- package/client/src/utils/findDuplicateKeys.ts +12 -0
- package/client/tsconfig.json +14 -0
- package/client/vite.config.ts +20 -0
- package/package.json +46 -0
- package/server/package.json +22 -0
- package/server/src/auth/passphrase.ts +48 -0
- package/server/src/config.ts +55 -0
- package/server/src/daemon.ts +338 -0
- package/server/src/features/ralph-loop.ts +173 -0
- package/server/src/features/scheduler.ts +281 -0
- package/server/src/features/sticky-notes.ts +102 -0
- package/server/src/handlers/chat.ts +194 -0
- package/server/src/handlers/fs.ts +84 -0
- package/server/src/handlers/loop.ts +37 -0
- package/server/src/handlers/mesh.ts +125 -0
- package/server/src/handlers/notes.ts +45 -0
- package/server/src/handlers/project-settings.ts +174 -0
- package/server/src/handlers/scheduler.ts +47 -0
- package/server/src/handlers/session.ts +159 -0
- package/server/src/handlers/settings.ts +109 -0
- package/server/src/handlers/skills.ts +380 -0
- package/server/src/handlers/terminal.ts +70 -0
- package/server/src/identity.ts +26 -0
- package/server/src/index.ts +190 -0
- package/server/src/mesh/connector.ts +209 -0
- package/server/src/mesh/discovery.ts +123 -0
- package/server/src/mesh/pairing.ts +94 -0
- package/server/src/mesh/peers.ts +52 -0
- package/server/src/mesh/proxy.ts +103 -0
- package/server/src/mesh/session-sync.ts +107 -0
- package/server/src/project/context-breakdown.ts +289 -0
- package/server/src/project/file-browser.ts +106 -0
- package/server/src/project/project-files.ts +267 -0
- package/server/src/project/registry.ts +57 -0
- package/server/src/project/sdk-bridge.ts +566 -0
- package/server/src/project/session.ts +432 -0
- package/server/src/project/terminal.ts +69 -0
- package/server/src/tls.ts +51 -0
- package/server/src/ws/broadcast.ts +31 -0
- package/server/src/ws/router.ts +104 -0
- package/server/src/ws/server.ts +2 -0
- package/server/tsconfig.json +16 -0
- package/shared/package.json +11 -0
- package/shared/src/constants.ts +7 -0
- package/shared/src/index.ts +4 -0
- package/shared/src/messages.ts +638 -0
- package/shared/src/models.ts +136 -0
- package/shared/src/project-settings.ts +45 -0
- package/shared/tsconfig.json +11 -0
- package/themes/amoled.json +20 -0
- package/themes/ayu-light.json +9 -0
- package/themes/catppuccin-latte.json +9 -0
- package/themes/catppuccin-mocha.json +9 -0
- package/themes/clay-light.json +10 -0
- package/themes/clay.json +10 -0
- package/themes/dracula.json +9 -0
- package/themes/everforest-light.json +9 -0
- package/themes/everforest.json +9 -0
- package/themes/github-light.json +9 -0
- package/themes/gruvbox-dark.json +9 -0
- package/themes/gruvbox-light.json +9 -0
- package/themes/monokai.json +9 -0
- package/themes/nord-light.json +9 -0
- package/themes/nord.json +9 -0
- package/themes/one-dark.json +9 -0
- package/themes/one-light.json +9 -0
- package/themes/rose-pine-dawn.json +9 -0
- package/themes/rose-pine.json +9 -0
- package/themes/solarized-dark.json +9 -0
- package/themes/solarized-light.json +9 -0
- package/themes/tokyo-night-light.json +9 -0
- package/themes/tokyo-night.json +9 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { ClientMessage, ServerMessage } from "@lattice/shared";
|
|
4
|
+
import { WebSocketContext, getWebSocketUrl } from "../hooks/useWebSocket";
|
|
5
|
+
import type { WebSocketStatus } from "../hooks/useWebSocket";
|
|
6
|
+
import { showToast } from "../components/ui/Toast";
|
|
7
|
+
import { getSessionStore } from "../stores/session";
|
|
8
|
+
|
|
9
|
+
interface WebSocketProviderProps {
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
var MAX_BACKOFF = 30000;
|
|
14
|
+
|
|
15
|
+
export function WebSocketProvider(props: WebSocketProviderProps) {
|
|
16
|
+
var [status, setStatus] = useState<WebSocketStatus>("connecting");
|
|
17
|
+
var wsRef = useRef<WebSocket | null>(null);
|
|
18
|
+
var backoffRef = useRef<number>(1000);
|
|
19
|
+
var retryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
20
|
+
var unmountedRef = useRef<boolean>(false);
|
|
21
|
+
var listenersRef = useRef<Map<string, Set<(msg: ServerMessage) => void>>>(new Map());
|
|
22
|
+
var hasConnectedRef = useRef<boolean>(false);
|
|
23
|
+
|
|
24
|
+
function connect() {
|
|
25
|
+
if (unmountedRef.current) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var url = getWebSocketUrl();
|
|
30
|
+
var ws = new WebSocket(url);
|
|
31
|
+
wsRef.current = ws;
|
|
32
|
+
setStatus("connecting");
|
|
33
|
+
|
|
34
|
+
ws.onopen = function () {
|
|
35
|
+
if (unmountedRef.current) {
|
|
36
|
+
ws.close();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
setStatus("connected");
|
|
40
|
+
backoffRef.current = 1000;
|
|
41
|
+
if (hasConnectedRef.current) {
|
|
42
|
+
showToast("Reconnected to daemon", "info");
|
|
43
|
+
ws.send(JSON.stringify({ type: "settings:get" }));
|
|
44
|
+
|
|
45
|
+
var sessionState = getSessionStore().state;
|
|
46
|
+
if (sessionState.activeProjectSlug && sessionState.activeSessionId) {
|
|
47
|
+
ws.send(JSON.stringify({
|
|
48
|
+
type: "session:activate",
|
|
49
|
+
projectSlug: sessionState.activeProjectSlug,
|
|
50
|
+
sessionId: sessionState.activeSessionId,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
hasConnectedRef.current = true;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
ws.onmessage = function (event: MessageEvent) {
|
|
58
|
+
try {
|
|
59
|
+
var msg = JSON.parse(event.data as string) as ServerMessage;
|
|
60
|
+
var listeners = listenersRef.current.get(msg.type);
|
|
61
|
+
if (listeners) {
|
|
62
|
+
listeners.forEach(function (cb) {
|
|
63
|
+
cb(msg);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
ws.onclose = function () {
|
|
71
|
+
if (unmountedRef.current) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
setStatus("disconnected");
|
|
75
|
+
wsRef.current = null;
|
|
76
|
+
if (hasConnectedRef.current) {
|
|
77
|
+
showToast("Disconnected from daemon. Reconnecting...", "warning");
|
|
78
|
+
}
|
|
79
|
+
scheduleReconnect();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
ws.onerror = function () {
|
|
83
|
+
ws.close();
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function scheduleReconnect() {
|
|
88
|
+
if (unmountedRef.current) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
var delay = backoffRef.current;
|
|
92
|
+
retryTimerRef.current = setTimeout(function () {
|
|
93
|
+
backoffRef.current = Math.min(backoffRef.current * 2, MAX_BACKOFF);
|
|
94
|
+
connect();
|
|
95
|
+
}, delay);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function send(msg: ClientMessage) {
|
|
99
|
+
var ws = wsRef.current;
|
|
100
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
101
|
+
ws.send(JSON.stringify(msg));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function subscribe(type: string, callback: (msg: ServerMessage) => void) {
|
|
106
|
+
var listeners = listenersRef.current.get(type);
|
|
107
|
+
if (!listeners) {
|
|
108
|
+
listeners = new Set();
|
|
109
|
+
listenersRef.current.set(type, listeners);
|
|
110
|
+
}
|
|
111
|
+
listeners.add(callback);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function unsubscribe(type: string, callback: (msg: ServerMessage) => void) {
|
|
115
|
+
var listeners = listenersRef.current.get(type);
|
|
116
|
+
if (listeners) {
|
|
117
|
+
listeners.delete(callback);
|
|
118
|
+
if (listeners.size === 0) {
|
|
119
|
+
listenersRef.current.delete(type);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
useEffect(function () {
|
|
125
|
+
unmountedRef.current = false;
|
|
126
|
+
connect();
|
|
127
|
+
|
|
128
|
+
return function () {
|
|
129
|
+
unmountedRef.current = true;
|
|
130
|
+
if (retryTimerRef.current !== null) {
|
|
131
|
+
clearTimeout(retryTimerRef.current);
|
|
132
|
+
retryTimerRef.current = null;
|
|
133
|
+
}
|
|
134
|
+
if (wsRef.current) {
|
|
135
|
+
wsRef.current.close();
|
|
136
|
+
wsRef.current = null;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}, []);
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<WebSocketContext.Provider value={{ status, send, subscribe, unsubscribe }}>
|
|
143
|
+
{props.children}
|
|
144
|
+
</WebSocketContext.Provider>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { createRouter, createRootRoute, createRoute, createMemoryHistory } from "@tanstack/react-router";
|
|
2
|
+
import { Outlet } from "@tanstack/react-router";
|
|
3
|
+
import { useState, useEffect, useRef } from "react";
|
|
4
|
+
import { Sidebar } from "./components/sidebar/Sidebar";
|
|
5
|
+
import { ChatView } from "./components/chat/ChatView";
|
|
6
|
+
import { SetupWizard } from "./components/setup/SetupWizard";
|
|
7
|
+
import { SettingsView } from "./components/settings/SettingsView";
|
|
8
|
+
import { ProjectSettingsView } from "./components/project-settings/ProjectSettingsView";
|
|
9
|
+
import { DashboardView } from "./components/dashboard/DashboardView";
|
|
10
|
+
import { ProjectDashboardView } from "./components/dashboard/ProjectDashboardView";
|
|
11
|
+
import { NodeSettingsModal } from "./components/sidebar/NodeSettingsModal";
|
|
12
|
+
import { useSidebar } from "./hooks/useSidebar";
|
|
13
|
+
import { useWebSocket } from "./hooks/useWebSocket";
|
|
14
|
+
import { exitSettings, getSidebarStore, handlePopState, closeDrawer } from "./stores/sidebar";
|
|
15
|
+
|
|
16
|
+
function LoadingScreen() {
|
|
17
|
+
var ws = useWebSocket();
|
|
18
|
+
var [dataReceived, setDataReceived] = useState(false);
|
|
19
|
+
var [minTimeElapsed, setMinTimeElapsed] = useState(false);
|
|
20
|
+
var canvasRef = useRef<HTMLCanvasElement>(null);
|
|
21
|
+
var frameRef = useRef<number>(0);
|
|
22
|
+
|
|
23
|
+
useEffect(function () {
|
|
24
|
+
var timer = setTimeout(function () {
|
|
25
|
+
setMinTimeElapsed(true);
|
|
26
|
+
}, 600);
|
|
27
|
+
return function () { clearTimeout(timer); };
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
useEffect(function () {
|
|
31
|
+
if (ws.status !== "connected") return;
|
|
32
|
+
function handleProjects() {
|
|
33
|
+
setDataReceived(true);
|
|
34
|
+
}
|
|
35
|
+
ws.subscribe("projects:list", handleProjects);
|
|
36
|
+
return function () { ws.unsubscribe("projects:list", handleProjects); };
|
|
37
|
+
}, [ws]);
|
|
38
|
+
|
|
39
|
+
var ready = dataReceived && minTimeElapsed;
|
|
40
|
+
var [visible, setVisible] = useState(true);
|
|
41
|
+
|
|
42
|
+
useEffect(function () {
|
|
43
|
+
if (!ready) return;
|
|
44
|
+
var timer = setTimeout(function () {
|
|
45
|
+
setVisible(false);
|
|
46
|
+
}, 300);
|
|
47
|
+
return function () { clearTimeout(timer); };
|
|
48
|
+
}, [ready]);
|
|
49
|
+
|
|
50
|
+
useEffect(function () {
|
|
51
|
+
var canvas = canvasRef.current;
|
|
52
|
+
if (!canvas) return;
|
|
53
|
+
var ctx = canvas.getContext("2d");
|
|
54
|
+
if (!ctx) return;
|
|
55
|
+
|
|
56
|
+
var gridSize = 9;
|
|
57
|
+
var spacing = 26;
|
|
58
|
+
var dotRadius = 3.5;
|
|
59
|
+
var padding = 20;
|
|
60
|
+
var canvasSize = padding * 2 + (gridSize - 1) * spacing;
|
|
61
|
+
var dpr = window.devicePixelRatio || 1;
|
|
62
|
+
|
|
63
|
+
canvas.width = canvasSize * dpr;
|
|
64
|
+
canvas.height = canvasSize * dpr;
|
|
65
|
+
canvas.style.width = canvasSize + "px";
|
|
66
|
+
canvas.style.height = canvasSize + "px";
|
|
67
|
+
ctx.scale(dpr, dpr);
|
|
68
|
+
|
|
69
|
+
var centerX = canvasSize / 2;
|
|
70
|
+
var centerY = canvasSize / 2;
|
|
71
|
+
var logoSquare = 14;
|
|
72
|
+
var logoGap = 4;
|
|
73
|
+
var logoHalf = (logoSquare * 2 + logoGap) / 2;
|
|
74
|
+
var logoLeft = centerX - logoHalf;
|
|
75
|
+
var logoTop = centerY - logoHalf;
|
|
76
|
+
var logoRight = centerX + logoHalf;
|
|
77
|
+
var logoBottom = centerY + logoHalf;
|
|
78
|
+
|
|
79
|
+
var primary = "oklch(55% 0.25 280)";
|
|
80
|
+
|
|
81
|
+
type Dot = { x: number; y: number; col: number; row: number; hidden: boolean; brightness: number };
|
|
82
|
+
var dots: Dot[] = [];
|
|
83
|
+
for (var row = 0; row < gridSize; row++) {
|
|
84
|
+
for (var col = 0; col < gridSize; col++) {
|
|
85
|
+
var x = padding + col * spacing;
|
|
86
|
+
var y = padding + row * spacing;
|
|
87
|
+
var hidden = x + dotRadius > logoLeft && x - dotRadius < logoRight &&
|
|
88
|
+
y + dotRadius > logoTop && y - dotRadius < logoBottom;
|
|
89
|
+
dots.push({ x: x, y: y, col: col, row: row, hidden: hidden, brightness: 0.08 });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type Connection = {
|
|
94
|
+
a: number; b: number;
|
|
95
|
+
birth: number; duration: number;
|
|
96
|
+
fadeIn: number; fadeOut: number;
|
|
97
|
+
};
|
|
98
|
+
var connections: Connection[] = [];
|
|
99
|
+
var maxConnections = 10;
|
|
100
|
+
var now = performance.now();
|
|
101
|
+
|
|
102
|
+
function getNeighbors(idx: number): number[] {
|
|
103
|
+
var d = dots[idx];
|
|
104
|
+
var result: number[] = [];
|
|
105
|
+
for (var dr = -1; dr <= 1; dr++) {
|
|
106
|
+
for (var dc = -1; dc <= 1; dc++) {
|
|
107
|
+
if (dr === 0 && dc === 0) continue;
|
|
108
|
+
var nr = d.row + dr;
|
|
109
|
+
var nc = d.col + dc;
|
|
110
|
+
if (nr >= 0 && nr < gridSize && nc >= 0 && nc < gridSize) {
|
|
111
|
+
var ni = nr * gridSize + nc;
|
|
112
|
+
if (!dots[ni].hidden) result.push(ni);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function animate() {
|
|
120
|
+
now = performance.now();
|
|
121
|
+
ctx!.clearRect(0, 0, canvasSize, canvasSize);
|
|
122
|
+
|
|
123
|
+
if (connections.length < maxConnections && Math.random() < 0.04) {
|
|
124
|
+
var candidates: number[] = [];
|
|
125
|
+
for (var i = 0; i < dots.length; i++) {
|
|
126
|
+
if (!dots[i].hidden) candidates.push(i);
|
|
127
|
+
}
|
|
128
|
+
var attempts = 0;
|
|
129
|
+
while (attempts < 5 && connections.length < maxConnections) {
|
|
130
|
+
var ai = candidates[Math.floor(Math.random() * candidates.length)];
|
|
131
|
+
var neighbors = getNeighbors(ai);
|
|
132
|
+
if (neighbors.length > 0) {
|
|
133
|
+
var bi = neighbors[Math.floor(Math.random() * neighbors.length)];
|
|
134
|
+
var exists = false;
|
|
135
|
+
for (var j = 0; j < connections.length; j++) {
|
|
136
|
+
if ((connections[j].a === ai && connections[j].b === bi) ||
|
|
137
|
+
(connections[j].a === bi && connections[j].b === ai)) {
|
|
138
|
+
exists = true;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!exists) {
|
|
143
|
+
connections.push({
|
|
144
|
+
a: ai, b: bi,
|
|
145
|
+
birth: now,
|
|
146
|
+
duration: 2000 + Math.random() * 3000,
|
|
147
|
+
fadeIn: 400,
|
|
148
|
+
fadeOut: 400,
|
|
149
|
+
});
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
attempts++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
var connectedSet = new Set<number>();
|
|
158
|
+
var keep: Connection[] = [];
|
|
159
|
+
for (var ci = 0; ci < connections.length; ci++) {
|
|
160
|
+
var c = connections[ci];
|
|
161
|
+
var age = now - c.birth;
|
|
162
|
+
if (age > c.duration) continue;
|
|
163
|
+
keep.push(c);
|
|
164
|
+
|
|
165
|
+
var alpha = 1.0;
|
|
166
|
+
if (age < c.fadeIn) {
|
|
167
|
+
alpha = age / c.fadeIn;
|
|
168
|
+
} else if (age > c.duration - c.fadeOut) {
|
|
169
|
+
alpha = (c.duration - age) / c.fadeOut;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
connectedSet.add(c.a);
|
|
173
|
+
connectedSet.add(c.b);
|
|
174
|
+
|
|
175
|
+
var da = dots[c.a];
|
|
176
|
+
var db = dots[c.b];
|
|
177
|
+
ctx!.beginPath();
|
|
178
|
+
ctx!.moveTo(da.x, da.y);
|
|
179
|
+
ctx!.lineTo(db.x, db.y);
|
|
180
|
+
ctx!.strokeStyle = primary;
|
|
181
|
+
ctx!.globalAlpha = alpha * 0.35;
|
|
182
|
+
ctx!.lineWidth = 1.5;
|
|
183
|
+
ctx!.stroke();
|
|
184
|
+
ctx!.globalAlpha = 1.0;
|
|
185
|
+
}
|
|
186
|
+
connections = keep;
|
|
187
|
+
|
|
188
|
+
for (var di = 0; di < dots.length; di++) {
|
|
189
|
+
var dot = dots[di];
|
|
190
|
+
if (dot.hidden) continue;
|
|
191
|
+
var targetBrightness = connectedSet.has(di) ? 0.7 : 0.08;
|
|
192
|
+
dot.brightness += (targetBrightness - dot.brightness) * 0.08;
|
|
193
|
+
ctx!.beginPath();
|
|
194
|
+
ctx!.arc(dot.x, dot.y, dotRadius, 0, Math.PI * 2);
|
|
195
|
+
ctx!.fillStyle = primary;
|
|
196
|
+
ctx!.globalAlpha = dot.brightness;
|
|
197
|
+
ctx!.fill();
|
|
198
|
+
ctx!.globalAlpha = 1.0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
var pulse = 0.4 + 0.2 * Math.sin(now / 800);
|
|
202
|
+
var squares = [
|
|
203
|
+
[logoLeft, logoTop],
|
|
204
|
+
[logoLeft + logoSquare + logoGap, logoTop],
|
|
205
|
+
[logoLeft, logoTop + logoSquare + logoGap],
|
|
206
|
+
[logoLeft + logoSquare + logoGap, logoTop + logoSquare + logoGap],
|
|
207
|
+
];
|
|
208
|
+
for (var si = 0; si < squares.length; si++) {
|
|
209
|
+
var sx = squares[si][0];
|
|
210
|
+
var sy = squares[si][1];
|
|
211
|
+
ctx!.save();
|
|
212
|
+
ctx!.shadowColor = primary;
|
|
213
|
+
ctx!.shadowBlur = 8 + pulse * 6;
|
|
214
|
+
ctx!.fillStyle = primary;
|
|
215
|
+
ctx!.globalAlpha = 0.85 + pulse * 0.15;
|
|
216
|
+
ctx!.fillRect(sx, sy, logoSquare, logoSquare);
|
|
217
|
+
ctx!.restore();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
frameRef.current = requestAnimationFrame(animate);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
frameRef.current = requestAnimationFrame(animate);
|
|
224
|
+
|
|
225
|
+
return function () {
|
|
226
|
+
cancelAnimationFrame(frameRef.current);
|
|
227
|
+
};
|
|
228
|
+
}, []);
|
|
229
|
+
|
|
230
|
+
if (!visible) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
var statusText = ws.status === "connecting" ? "Connecting..."
|
|
235
|
+
: ws.status === "disconnected" ? "Reconnecting..."
|
|
236
|
+
: "Loading projects...";
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<div
|
|
240
|
+
className="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-base-100"
|
|
241
|
+
style={{ opacity: ready ? 0 : 1, transition: "opacity 300ms ease-out", pointerEvents: ready ? "none" : "auto" }}
|
|
242
|
+
>
|
|
243
|
+
<div className="flex flex-col items-center gap-7">
|
|
244
|
+
<canvas ref={canvasRef} />
|
|
245
|
+
<div className="flex flex-col items-center gap-2">
|
|
246
|
+
<span className="text-[16px] font-mono font-bold text-base-content tracking-tight">Lattice</span>
|
|
247
|
+
<span className="text-[12px] text-base-content/40">{statusText}</span>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function RootLayout() {
|
|
255
|
+
var [setupComplete, setSetupComplete] = useState(function () {
|
|
256
|
+
return localStorage.getItem("lattice-setup-complete") === "1";
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
var sidebar = useSidebar();
|
|
260
|
+
|
|
261
|
+
useEffect(function () {
|
|
262
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
263
|
+
if (e.key === "Escape") {
|
|
264
|
+
var state = getSidebarStore().state;
|
|
265
|
+
if (state.sidebarMode === "settings") {
|
|
266
|
+
exitSettings();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
271
|
+
window.addEventListener("popstate", handlePopState);
|
|
272
|
+
return function () {
|
|
273
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
274
|
+
window.removeEventListener("popstate", handlePopState);
|
|
275
|
+
};
|
|
276
|
+
}, []);
|
|
277
|
+
|
|
278
|
+
if (!setupComplete) {
|
|
279
|
+
return (
|
|
280
|
+
<SetupWizard onComplete={function () { setSetupComplete(true); }} />
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<div className="flex w-full h-full overflow-hidden bg-base-100">
|
|
286
|
+
<LoadingScreen />
|
|
287
|
+
<div className="drawer lg:drawer-open h-full w-full">
|
|
288
|
+
<input
|
|
289
|
+
id="sidebar-drawer"
|
|
290
|
+
type="checkbox"
|
|
291
|
+
className="drawer-toggle"
|
|
292
|
+
checked={sidebar.drawerOpen}
|
|
293
|
+
onChange={function () {}}
|
|
294
|
+
/>
|
|
295
|
+
|
|
296
|
+
<div className="drawer-content flex flex-col h-full min-w-0 overflow-hidden">
|
|
297
|
+
<Outlet />
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<div className="drawer-side z-50 h-full">
|
|
301
|
+
<label
|
|
302
|
+
htmlFor="sidebar-drawer"
|
|
303
|
+
aria-label="close sidebar"
|
|
304
|
+
className="drawer-overlay"
|
|
305
|
+
onClick={closeDrawer}
|
|
306
|
+
/>
|
|
307
|
+
<div className="h-full w-[284px] flex flex-col overflow-hidden">
|
|
308
|
+
<Sidebar onSessionSelect={closeDrawer} />
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
<NodeSettingsModal
|
|
313
|
+
isOpen={sidebar.nodeSettingsOpen}
|
|
314
|
+
onClose={sidebar.closeNodeSettings}
|
|
315
|
+
/>
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function IndexPage() {
|
|
321
|
+
var sidebar = useSidebar();
|
|
322
|
+
if (sidebar.activeView.type === "dashboard") {
|
|
323
|
+
return <DashboardView />;
|
|
324
|
+
}
|
|
325
|
+
if (sidebar.activeView.type === "settings") {
|
|
326
|
+
return <SettingsView />;
|
|
327
|
+
}
|
|
328
|
+
if (sidebar.activeView.type === "project-settings") {
|
|
329
|
+
return <ProjectSettingsView />;
|
|
330
|
+
}
|
|
331
|
+
if (sidebar.activeView.type === "project-dashboard") {
|
|
332
|
+
return <ProjectDashboardView />;
|
|
333
|
+
}
|
|
334
|
+
return <ChatView />;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
var rootRoute = createRootRoute({
|
|
338
|
+
component: RootLayout,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
var indexRoute = createRoute({
|
|
342
|
+
getParentRoute: function () { return rootRoute; },
|
|
343
|
+
path: "/",
|
|
344
|
+
component: IndexPage,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
var projectRoute = createRoute({
|
|
348
|
+
getParentRoute: function () { return rootRoute; },
|
|
349
|
+
path: "/$projectSlug",
|
|
350
|
+
component: IndexPage,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
var sessionRoute = createRoute({
|
|
354
|
+
getParentRoute: function () { return rootRoute; },
|
|
355
|
+
path: "/$projectSlug/$sessionId",
|
|
356
|
+
component: IndexPage,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
var settingsRoute = createRoute({
|
|
360
|
+
getParentRoute: function () { return rootRoute; },
|
|
361
|
+
path: "/settings/$section",
|
|
362
|
+
component: IndexPage,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
var settingsIndexRoute = createRoute({
|
|
366
|
+
getParentRoute: function () { return rootRoute; },
|
|
367
|
+
path: "/settings",
|
|
368
|
+
component: IndexPage,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
var projectSettingsRoute = createRoute({
|
|
372
|
+
getParentRoute: function () { return rootRoute; },
|
|
373
|
+
path: "/$projectSlug/settings/$section",
|
|
374
|
+
component: IndexPage,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
var projectSettingsIndexRoute = createRoute({
|
|
378
|
+
getParentRoute: function () { return rootRoute; },
|
|
379
|
+
path: "/$projectSlug/settings",
|
|
380
|
+
component: IndexPage,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
var routeTree = rootRoute.addChildren([indexRoute, settingsIndexRoute, settingsRoute, projectSettingsIndexRoute, projectSettingsRoute, projectRoute, sessionRoute]);
|
|
384
|
+
|
|
385
|
+
export var router = createRouter({ routeTree });
|
|
386
|
+
|
|
387
|
+
declare module "@tanstack/react-router" {
|
|
388
|
+
interface Register {
|
|
389
|
+
router: typeof router;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Store } from "@tanstack/react-store";
|
|
2
|
+
import type { NodeInfo } from "@lattice/shared";
|
|
3
|
+
|
|
4
|
+
export interface MeshState {
|
|
5
|
+
nodes: NodeInfo[];
|
|
6
|
+
selectedNodeId: string | null;
|
|
7
|
+
inviteCode: string | null;
|
|
8
|
+
inviteQr: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
var meshStore = new Store<MeshState>({
|
|
12
|
+
nodes: [],
|
|
13
|
+
selectedNodeId: null,
|
|
14
|
+
inviteCode: null,
|
|
15
|
+
inviteQr: null,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export function getMeshStore(): Store<MeshState> {
|
|
19
|
+
return meshStore;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function setNodes(nodes: NodeInfo[]): void {
|
|
23
|
+
meshStore.setState(function (state) {
|
|
24
|
+
return { ...state, nodes };
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function setNodeOnline(nodeId: string): void {
|
|
29
|
+
meshStore.setState(function (state) {
|
|
30
|
+
var nodes = state.nodes.map(function (n) {
|
|
31
|
+
if (n.id === nodeId) {
|
|
32
|
+
return { ...n, online: true };
|
|
33
|
+
}
|
|
34
|
+
return n;
|
|
35
|
+
});
|
|
36
|
+
return { ...state, nodes };
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function setNodeOffline(nodeId: string): void {
|
|
41
|
+
meshStore.setState(function (state) {
|
|
42
|
+
var nodes = state.nodes.map(function (n) {
|
|
43
|
+
if (n.id === nodeId) {
|
|
44
|
+
return { ...n, online: false };
|
|
45
|
+
}
|
|
46
|
+
return n;
|
|
47
|
+
});
|
|
48
|
+
return { ...state, nodes };
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function addOrUpdateNode(node: NodeInfo): void {
|
|
53
|
+
meshStore.setState(function (state) {
|
|
54
|
+
var exists = state.nodes.some(function (n) { return n.id === node.id; });
|
|
55
|
+
var nodes = exists
|
|
56
|
+
? state.nodes.map(function (n) { return n.id === node.id ? node : n; })
|
|
57
|
+
: [...state.nodes, node];
|
|
58
|
+
return { ...state, nodes };
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function setSelectedNodeId(nodeId: string | null): void {
|
|
63
|
+
meshStore.setState(function (state) {
|
|
64
|
+
return { ...state, selectedNodeId: nodeId };
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function setInvite(code: string, qr: string): void {
|
|
69
|
+
meshStore.setState(function (state) {
|
|
70
|
+
return { ...state, inviteCode: code, inviteQr: qr };
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function clearInvite(): void {
|
|
75
|
+
meshStore.setState(function (state) {
|
|
76
|
+
return { ...state, inviteCode: null, inviteQr: null };
|
|
77
|
+
});
|
|
78
|
+
}
|