@cryptiklemur/lattice 1.14.2 → 1.16.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/.github/workflows/ci.yml +121 -0
- package/bun.lock +14 -1
- package/client/src/App.tsx +2 -0
- package/client/src/components/analytics/ChartCard.tsx +6 -10
- package/client/src/components/analytics/QuickStats.tsx +3 -3
- package/client/src/components/analytics/charts/NodeFleetOverview.tsx +1 -1
- package/client/src/components/chat/ChatView.tsx +119 -7
- package/client/src/components/chat/Message.tsx +41 -6
- package/client/src/components/chat/PromptQuestion.tsx +4 -4
- package/client/src/components/chat/TodoCard.tsx +2 -2
- package/client/src/components/chat/ToolResultRenderer.tsx +6 -2
- package/client/src/components/dashboard/DashboardView.tsx +2 -0
- package/client/src/components/mesh/PairingDialog.tsx +6 -17
- package/client/src/components/project-settings/ProjectEnvironment.tsx +1 -1
- package/client/src/components/project-settings/ProjectMemory.tsx +10 -19
- package/client/src/components/project-settings/ProjectRules.tsx +3 -3
- package/client/src/components/project-settings/ProjectSkills.tsx +2 -2
- package/client/src/components/settings/BudgetSettings.tsx +161 -0
- package/client/src/components/settings/Environment.tsx +1 -1
- package/client/src/components/settings/SettingsView.tsx +3 -0
- package/client/src/components/sidebar/AddProjectModal.tsx +7 -11
- package/client/src/components/sidebar/NodeSettingsModal.tsx +6 -11
- package/client/src/components/sidebar/ProjectRail.tsx +11 -1
- package/client/src/components/sidebar/SessionList.tsx +33 -12
- package/client/src/components/sidebar/SettingsSidebar.tsx +2 -1
- package/client/src/components/sidebar/Sidebar.tsx +152 -2
- package/client/src/components/sidebar/UserIsland.tsx +76 -37
- package/client/src/components/ui/IconPicker.tsx +9 -36
- package/client/src/components/ui/KeyboardShortcuts.tsx +129 -0
- package/client/src/components/ui/Toast.tsx +22 -2
- package/client/src/components/workspace/BookmarksView.tsx +156 -0
- package/client/src/components/workspace/TabBar.tsx +34 -5
- package/client/src/components/workspace/TaskEditModal.tsx +7 -2
- package/client/src/components/workspace/WorkspaceView.tsx +29 -6
- package/client/src/hooks/useBookmarks.ts +57 -0
- package/client/src/hooks/useFocusTrap.ts +72 -0
- package/client/src/hooks/useProjects.ts +1 -1
- package/client/src/hooks/useSession.ts +38 -1
- package/client/src/hooks/useTimeTick.ts +35 -0
- package/client/src/hooks/useVoiceRecorder.ts +17 -3
- package/client/src/hooks/useWorkspace.ts +10 -1
- package/client/src/router.tsx +6 -11
- package/client/src/stores/bookmarks.ts +45 -0
- package/client/src/stores/session.ts +24 -0
- package/client/src/stores/sidebar.ts +2 -2
- package/client/src/stores/workspace.ts +114 -3
- package/client/src/vite-env.d.ts +6 -0
- package/client/tsconfig.json +4 -0
- package/package.json +2 -1
- package/playwright.config.ts +19 -0
- package/server/package.json +2 -0
- package/server/src/analytics/engine.ts +43 -9
- package/server/src/daemon.ts +11 -7
- package/server/src/handlers/bookmarks.ts +50 -0
- package/server/src/handlers/chat.ts +64 -0
- package/server/src/handlers/fs.ts +1 -1
- package/server/src/handlers/memory.ts +1 -1
- package/server/src/handlers/mesh.ts +1 -1
- package/server/src/handlers/project-settings.ts +2 -2
- package/server/src/handlers/session.ts +12 -11
- package/server/src/handlers/settings.ts +5 -2
- package/server/src/handlers/skills.ts +1 -1
- package/server/src/logger.ts +12 -0
- package/server/src/mesh/connector.ts +7 -6
- package/server/src/project/bookmarks.ts +83 -0
- package/server/src/project/context-breakdown.ts +1 -1
- package/server/src/project/registry.ts +5 -5
- package/server/src/project/sdk-bridge.ts +77 -6
- package/server/src/project/session.ts +6 -5
- package/server/src/ws/router.ts +5 -4
- package/server/tsconfig.json +4 -0
- package/shared/src/messages.ts +53 -2
- package/shared/src/models.ts +17 -1
- package/shared/src/project-settings.ts +0 -1
- package/shared/tsconfig.json +4 -0
- package/tests/accessibility.spec.ts +77 -0
- package/tests/keyboard-shortcuts.spec.ts +74 -0
- package/tests/message-actions.spec.ts +112 -0
- package/tests/onboarding.spec.ts +72 -0
- package/tests/session-flow.spec.ts +117 -0
- package/tests/session-preview.spec.ts +83 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
branches:
|
|
9
|
+
- main
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
typecheck:
|
|
13
|
+
name: Typecheck
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout
|
|
17
|
+
uses: actions/checkout@v5
|
|
18
|
+
|
|
19
|
+
- name: Setup Bun
|
|
20
|
+
uses: oven-sh/setup-bun@v2
|
|
21
|
+
with:
|
|
22
|
+
bun-version: latest
|
|
23
|
+
|
|
24
|
+
- name: Setup Node.js
|
|
25
|
+
uses: actions/setup-node@v4
|
|
26
|
+
with:
|
|
27
|
+
node-version: 22
|
|
28
|
+
|
|
29
|
+
- name: Cache bun install
|
|
30
|
+
uses: actions/cache@v4
|
|
31
|
+
with:
|
|
32
|
+
path: ~/.bun/install/cache
|
|
33
|
+
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
|
|
34
|
+
restore-keys: |
|
|
35
|
+
${{ runner.os }}-bun-
|
|
36
|
+
|
|
37
|
+
- name: Install dependencies
|
|
38
|
+
run: bun install --frozen-lockfile
|
|
39
|
+
|
|
40
|
+
- name: Typecheck shared
|
|
41
|
+
run: bunx tsc --noEmit -p shared/tsconfig.json
|
|
42
|
+
|
|
43
|
+
- name: Typecheck server
|
|
44
|
+
run: bunx tsc --noEmit -p server/tsconfig.json
|
|
45
|
+
|
|
46
|
+
- name: Typecheck client
|
|
47
|
+
run: bunx tsc --noEmit -p client/tsconfig.json
|
|
48
|
+
|
|
49
|
+
build:
|
|
50
|
+
name: Build
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
steps:
|
|
53
|
+
- name: Checkout
|
|
54
|
+
uses: actions/checkout@v5
|
|
55
|
+
|
|
56
|
+
- name: Setup Bun
|
|
57
|
+
uses: oven-sh/setup-bun@v2
|
|
58
|
+
with:
|
|
59
|
+
bun-version: latest
|
|
60
|
+
|
|
61
|
+
- name: Setup Node.js
|
|
62
|
+
uses: actions/setup-node@v4
|
|
63
|
+
with:
|
|
64
|
+
node-version: 22
|
|
65
|
+
|
|
66
|
+
- name: Cache bun install
|
|
67
|
+
uses: actions/cache@v4
|
|
68
|
+
with:
|
|
69
|
+
path: ~/.bun/install/cache
|
|
70
|
+
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
|
|
71
|
+
restore-keys: |
|
|
72
|
+
${{ runner.os }}-bun-
|
|
73
|
+
|
|
74
|
+
- name: Install dependencies
|
|
75
|
+
run: bun install --frozen-lockfile
|
|
76
|
+
|
|
77
|
+
- name: Build client
|
|
78
|
+
run: cd client && npx vite build
|
|
79
|
+
|
|
80
|
+
playwright:
|
|
81
|
+
name: Playwright Tests
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
continue-on-error: true
|
|
84
|
+
steps:
|
|
85
|
+
- name: Checkout
|
|
86
|
+
uses: actions/checkout@v5
|
|
87
|
+
|
|
88
|
+
- name: Setup Bun
|
|
89
|
+
uses: oven-sh/setup-bun@v2
|
|
90
|
+
with:
|
|
91
|
+
bun-version: latest
|
|
92
|
+
|
|
93
|
+
- name: Setup Node.js
|
|
94
|
+
uses: actions/setup-node@v4
|
|
95
|
+
with:
|
|
96
|
+
node-version: 22
|
|
97
|
+
|
|
98
|
+
- name: Cache bun install
|
|
99
|
+
uses: actions/cache@v4
|
|
100
|
+
with:
|
|
101
|
+
path: ~/.bun/install/cache
|
|
102
|
+
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
|
|
103
|
+
restore-keys: |
|
|
104
|
+
${{ runner.os }}-bun-
|
|
105
|
+
|
|
106
|
+
- name: Install dependencies
|
|
107
|
+
run: bun install --frozen-lockfile
|
|
108
|
+
|
|
109
|
+
- name: Install Playwright browsers
|
|
110
|
+
run: bunx playwright install --with-deps chromium
|
|
111
|
+
|
|
112
|
+
- name: Run Playwright tests
|
|
113
|
+
run: bunx playwright test
|
|
114
|
+
|
|
115
|
+
- name: Upload test results
|
|
116
|
+
if: always()
|
|
117
|
+
uses: actions/upload-artifact@v4
|
|
118
|
+
with:
|
|
119
|
+
name: playwright-results
|
|
120
|
+
path: test-results/
|
|
121
|
+
retention-days: 7
|
package/bun.lock
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"": {
|
|
6
6
|
"name": "@lattice/root",
|
|
7
7
|
"devDependencies": {
|
|
8
|
+
"@playwright/test": "^1.52.0",
|
|
8
9
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
9
10
|
"@semantic-release/github": "^12.0.6",
|
|
10
11
|
"@semantic-release/npm": "^13.1.5",
|
|
@@ -55,11 +56,13 @@
|
|
|
55
56
|
"@anthropic-ai/claude-agent-sdk": "^0.2.79",
|
|
56
57
|
"@lattice/shared": "workspace:*",
|
|
57
58
|
"bonjour-service": "^1.3.0",
|
|
59
|
+
"debug": "^4.4.3",
|
|
58
60
|
"js-tiktoken": "^1.0.21",
|
|
59
61
|
"node-pty": "^1.1.0",
|
|
60
62
|
"qrcode": "^1.5.4",
|
|
61
63
|
},
|
|
62
64
|
"devDependencies": {
|
|
65
|
+
"@types/debug": "^4.1.13",
|
|
63
66
|
"@types/qrcode": "^1.5.6",
|
|
64
67
|
"bun-types": "latest",
|
|
65
68
|
},
|
|
@@ -350,6 +353,8 @@
|
|
|
350
353
|
|
|
351
354
|
"@oxc-project/types": ["@oxc-project/types@0.120.0", "", {}, "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg=="],
|
|
352
355
|
|
|
356
|
+
"@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="],
|
|
357
|
+
|
|
353
358
|
"@pnpm/config.env-replace": ["@pnpm/config.env-replace@1.1.0", "", {}, "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w=="],
|
|
354
359
|
|
|
355
360
|
"@pnpm/network.ca-file": ["@pnpm/network.ca-file@1.0.2", "", { "dependencies": { "graceful-fs": "4.2.10" } }, "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA=="],
|
|
@@ -508,7 +513,7 @@
|
|
|
508
513
|
|
|
509
514
|
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
|
|
510
515
|
|
|
511
|
-
"@types/debug": ["@types/debug@4.1.
|
|
516
|
+
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
|
512
517
|
|
|
513
518
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
|
514
519
|
|
|
@@ -1314,6 +1319,10 @@
|
|
|
1314
1319
|
|
|
1315
1320
|
"pkg-conf": ["pkg-conf@2.1.0", "", { "dependencies": { "find-up": "^2.0.0", "load-json-file": "^4.0.0" } }, "sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g=="],
|
|
1316
1321
|
|
|
1322
|
+
"playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="],
|
|
1323
|
+
|
|
1324
|
+
"playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="],
|
|
1325
|
+
|
|
1317
1326
|
"pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="],
|
|
1318
1327
|
|
|
1319
1328
|
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
|
@@ -1774,6 +1783,8 @@
|
|
|
1774
1783
|
|
|
1775
1784
|
"make-asynchronous/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
|
1776
1785
|
|
|
1786
|
+
"micromark/@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
|
1787
|
+
|
|
1777
1788
|
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
|
1778
1789
|
|
|
1779
1790
|
"npm/@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="],
|
|
@@ -2068,6 +2079,8 @@
|
|
|
2068
2079
|
|
|
2069
2080
|
"pkg-conf/find-up": ["find-up@2.1.0", "", { "dependencies": { "locate-path": "^2.0.0" } }, "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ=="],
|
|
2070
2081
|
|
|
2082
|
+
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
|
2083
|
+
|
|
2071
2084
|
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
|
2072
2085
|
|
|
2073
2086
|
"qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
|
package/client/src/App.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { WebSocketProvider } from "./providers/WebSocketProvider";
|
|
|
4
4
|
import { ErrorBoundary } from "./components/ui/ErrorBoundary";
|
|
5
5
|
import { Toast, useToastState } from "./components/ui/Toast";
|
|
6
6
|
import { CommandPalette } from "./components/ui/CommandPalette";
|
|
7
|
+
import { KeyboardShortcuts } from "./components/ui/KeyboardShortcuts";
|
|
7
8
|
import { UpdatePrompt } from "./components/ui/UpdatePrompt";
|
|
8
9
|
|
|
9
10
|
function AppInner() {
|
|
@@ -13,6 +14,7 @@ function AppInner() {
|
|
|
13
14
|
<>
|
|
14
15
|
<RouterProvider router={router} />
|
|
15
16
|
<CommandPalette />
|
|
17
|
+
<KeyboardShortcuts />
|
|
16
18
|
<Toast items={items} onDismiss={dismiss} />
|
|
17
19
|
<UpdatePrompt />
|
|
18
20
|
</>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useState, useEffect, useRef, createContext, useContext } from "react";
|
|
1
|
+
import { useState, useEffect, useRef, createContext, useContext, useCallback } from "react";
|
|
2
|
+
import { useFocusTrap } from "../../hooks/useFocusTrap";
|
|
2
3
|
import { Maximize2, Minimize2 } from "lucide-react";
|
|
3
4
|
import type { ReactNode } from "react";
|
|
4
5
|
|
|
@@ -31,6 +32,9 @@ export function ChartCard(props: ChartCardProps) {
|
|
|
31
32
|
var cardRef = useRef<HTMLDivElement>(null);
|
|
32
33
|
var [originRect, setOriginRect] = useState<DOMRect | null>(null);
|
|
33
34
|
var [animating, setAnimating] = useState(false);
|
|
35
|
+
var fullscreenModalRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
var closeFullscreenCb = useCallback(function () { closeFullscreen(); }, []);
|
|
37
|
+
useFocusTrap(fullscreenModalRef, closeFullscreenCb, isFullscreen);
|
|
34
38
|
|
|
35
39
|
function openFullscreen() {
|
|
36
40
|
if (cardRef.current) {
|
|
@@ -54,15 +58,6 @@ export function ChartCard(props: ChartCardProps) {
|
|
|
54
58
|
}, 250);
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
useEffect(function () {
|
|
58
|
-
if (!isFullscreen) return;
|
|
59
|
-
function handleKeyDown(e: KeyboardEvent) {
|
|
60
|
-
if (e.key === "Escape") closeFullscreen();
|
|
61
|
-
}
|
|
62
|
-
document.addEventListener("keydown", handleKeyDown);
|
|
63
|
-
return function () { document.removeEventListener("keydown", handleKeyDown); };
|
|
64
|
-
}, [isFullscreen]);
|
|
65
|
-
|
|
66
61
|
useEffect(function () {
|
|
67
62
|
if (isFullscreen) {
|
|
68
63
|
document.body.style.overflow = "hidden";
|
|
@@ -134,6 +129,7 @@ export function ChartCard(props: ChartCardProps) {
|
|
|
134
129
|
onClick={closeFullscreen}
|
|
135
130
|
/>
|
|
136
131
|
<div
|
|
132
|
+
ref={fullscreenModalRef}
|
|
137
133
|
className="fixed inset-0 z-[9999] flex items-center justify-center p-8"
|
|
138
134
|
style={{ pointerEvents: "none" }}
|
|
139
135
|
role="dialog"
|
|
@@ -51,9 +51,9 @@ export function QuickStats() {
|
|
|
51
51
|
var d = analytics.data;
|
|
52
52
|
var colors = getChartColors();
|
|
53
53
|
|
|
54
|
-
var costSparkData = d.costOverTime.slice(-7).map(function (e) { return { v: e.total }; });
|
|
55
|
-
var sessionsSparkData = d.sessionsOverTime.slice(-7).map(function (e) { return { v: e.count }; });
|
|
56
|
-
var tokensSparkData = d.tokensOverTime.slice(-7).map(function (e) { return { v: e.input + e.output }; });
|
|
54
|
+
var costSparkData = d.costOverTime.slice(-7).map(function (e: typeof d.costOverTime[number]) { return { v: e.total }; });
|
|
55
|
+
var sessionsSparkData = d.sessionsOverTime.slice(-7).map(function (e: typeof d.sessionsOverTime[number]) { return { v: e.count }; });
|
|
56
|
+
var tokensSparkData = d.tokensOverTime.slice(-7).map(function (e: typeof d.tokensOverTime[number]) { return { v: e.input + e.output }; });
|
|
57
57
|
|
|
58
58
|
var totalTokens = d.totalTokens.input + d.totalTokens.output;
|
|
59
59
|
var cacheHitPct = Math.round(d.cacheHitRate * 100);
|
|
@@ -67,7 +67,7 @@ export function NodeFleetOverview(props: NodeFleetOverviewProps) {
|
|
|
67
67
|
<div className="mt-2.5 pt-2 border-t border-base-content/5">
|
|
68
68
|
<div className="text-[9px] font-mono text-base-content/25 uppercase tracking-widest mb-1.5">Projects</div>
|
|
69
69
|
<div className="flex flex-wrap gap-1">
|
|
70
|
-
{node.projects.map(function (p) {
|
|
70
|
+
{node.projects.map(function (p: typeof node.projects[number]) {
|
|
71
71
|
return (
|
|
72
72
|
<span
|
|
73
73
|
key={p.slug}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { useEffect, useRef, useCallback, useState, useMemo } from "react";
|
|
2
|
-
import { Terminal, Info, ArrowDown, Pencil, Copy, Check, Menu, AlertTriangle, Zap, Square, X } from "lucide-react";
|
|
2
|
+
import { Terminal, Info, ArrowDown, Pencil, Copy, Check, Menu, AlertTriangle, Zap, Square, X, Bookmark } from "lucide-react";
|
|
3
3
|
import { LatticeLogomark } from "../ui/LatticeLogomark";
|
|
4
|
+
import { useFocusTrap } from "../../hooks/useFocusTrap";
|
|
4
5
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
5
6
|
import { useSession } from "../../hooks/useSession";
|
|
6
7
|
import { useProjects } from "../../hooks/useProjects";
|
|
7
8
|
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
8
9
|
import { setSessionTitle, setIsProcessing, setCurrentStatus, setWasInterrupted, setPendingPrefill } from "../../stores/session";
|
|
9
10
|
import { openSettings, openProjectSettings } from "../../stores/sidebar";
|
|
10
|
-
import { openTab } from "../../stores/workspace";
|
|
11
|
+
import { openTab, updateSessionTabTitle } from "../../stores/workspace";
|
|
11
12
|
import { builtinCommands } from "../../commands";
|
|
12
13
|
import { Message } from "./Message";
|
|
13
14
|
import { ToolGroup } from "./ToolGroup";
|
|
@@ -18,15 +19,26 @@ import { StatusBar } from "./StatusBar";
|
|
|
18
19
|
import { useSidebar } from "../../hooks/useSidebar";
|
|
19
20
|
import { useOnline } from "../../hooks/useOnline";
|
|
20
21
|
import { useSpinnerVerb } from "../../hooks/useSpinnerVerb";
|
|
22
|
+
import { useBookmarks } from "../../hooks/useBookmarks";
|
|
21
23
|
import { formatSessionTitle } from "../../utils/formatSessionTitle";
|
|
22
24
|
|
|
23
|
-
export function ChatView() {
|
|
24
|
-
var { messages, isProcessing, sendMessage, activeSessionId, activeSessionTitle, currentStatus, contextUsage, contextBreakdown, lastResponseCost, lastResponseDuration, historyLoading, wasInterrupted, promptSuggestion, failedInput, clearFailedInput, messageQueue, enqueueMessage, removeQueuedMessage, updateQueuedMessage, isBusy, isPlanMode, pendingPrefill } = useSession();
|
|
25
|
+
export function ChatView({ sessionId: tabSessionId, projectSlug: tabProjectSlug }: { sessionId?: string; projectSlug?: string } = {}) {
|
|
26
|
+
var { messages, isProcessing, sendMessage, activeSessionId, activeSessionTitle, currentStatus, contextUsage, contextBreakdown, lastResponseCost, lastResponseDuration, historyLoading, wasInterrupted, promptSuggestion, failedInput, clearFailedInput, messageQueue, enqueueMessage, removeQueuedMessage, updateQueuedMessage, isBusy, isPlanMode, pendingPrefill, activateSession, budgetStatus, budgetExceeded, sendBudgetOverride, dismissBudgetExceeded } = useSession();
|
|
25
27
|
var { activeProject } = useProjects();
|
|
26
28
|
var { toggleDrawer } = useSidebar();
|
|
29
|
+
|
|
30
|
+
useEffect(function () {
|
|
31
|
+
if (!tabSessionId || !tabProjectSlug) return;
|
|
32
|
+
if (activeSessionId === tabSessionId) return;
|
|
33
|
+
activateSession(tabProjectSlug, tabSessionId);
|
|
34
|
+
}, [tabSessionId, tabProjectSlug]);
|
|
27
35
|
var online = useOnline();
|
|
28
36
|
var ws = useWebSocket();
|
|
29
37
|
var spinnerVerb = useSpinnerVerb(isProcessing);
|
|
38
|
+
var { bookmarks, requestSessionBookmarks } = useBookmarks();
|
|
39
|
+
var [showBookmarkDropdown, setShowBookmarkDropdown] = useState<boolean>(false);
|
|
40
|
+
var bookmarkDropdownRef = useRef<HTMLDivElement>(null);
|
|
41
|
+
var bookmarkBtnRef = useRef<HTMLButtonElement>(null);
|
|
30
42
|
var scrollParentRef = useRef<HTMLDivElement>(null);
|
|
31
43
|
var prevLengthRef = useRef<number>(0);
|
|
32
44
|
var isLiveChatRef = useRef<boolean>(false);
|
|
@@ -38,6 +50,9 @@ export function ChatView() {
|
|
|
38
50
|
var [showInfo, setShowInfo] = useState<boolean>(false);
|
|
39
51
|
var [confirmStopExternal, setConfirmStopExternal] = useState<boolean>(false);
|
|
40
52
|
var [prefillText, setPrefillText] = useState<string | null>(null);
|
|
53
|
+
var stopExternalModalRef = useRef<HTMLDivElement>(null);
|
|
54
|
+
var closeStopExternal = useCallback(function () { setConfirmStopExternal(false); }, []);
|
|
55
|
+
useFocusTrap(stopExternalModalRef, closeStopExternal, confirmStopExternal);
|
|
41
56
|
|
|
42
57
|
useEffect(function () {
|
|
43
58
|
if (pendingPrefill && !historyLoading) {
|
|
@@ -46,6 +61,24 @@ export function ChatView() {
|
|
|
46
61
|
}
|
|
47
62
|
}, [pendingPrefill, historyLoading]);
|
|
48
63
|
|
|
64
|
+
useEffect(function () {
|
|
65
|
+
if (activeSessionId && !historyLoading) {
|
|
66
|
+
requestSessionBookmarks();
|
|
67
|
+
}
|
|
68
|
+
}, [activeSessionId, historyLoading]);
|
|
69
|
+
|
|
70
|
+
useEffect(function () {
|
|
71
|
+
if (!showBookmarkDropdown) return;
|
|
72
|
+
function handleClick(e: MouseEvent) {
|
|
73
|
+
var target = e.target as Node;
|
|
74
|
+
if (bookmarkDropdownRef.current && bookmarkDropdownRef.current.contains(target)) return;
|
|
75
|
+
if (bookmarkBtnRef.current && bookmarkBtnRef.current.contains(target)) return;
|
|
76
|
+
setShowBookmarkDropdown(false);
|
|
77
|
+
}
|
|
78
|
+
document.addEventListener("mousedown", handleClick);
|
|
79
|
+
return function () { document.removeEventListener("mousedown", handleClick); };
|
|
80
|
+
}, [showBookmarkDropdown]);
|
|
81
|
+
|
|
49
82
|
var [copiedField, setCopiedField] = useState<string | null>(null);
|
|
50
83
|
var [isRenaming, setIsRenaming] = useState<boolean>(false);
|
|
51
84
|
var [renameValue, setRenameValue] = useState<string>("");
|
|
@@ -198,6 +231,7 @@ export function ChatView() {
|
|
|
198
231
|
if (renameValue.trim() !== activeSessionTitle) {
|
|
199
232
|
ws.send({ type: "session:rename", sessionId: activeSessionId, title: renameValue.trim() });
|
|
200
233
|
setSessionTitle(renameValue.trim());
|
|
234
|
+
updateSessionTabTitle(activeSessionId, renameValue.trim());
|
|
201
235
|
}
|
|
202
236
|
}
|
|
203
237
|
setIsRenaming(false);
|
|
@@ -305,6 +339,7 @@ export function ChatView() {
|
|
|
305
339
|
if (args && activeSessionId) {
|
|
306
340
|
ws.send({ type: "session:rename", sessionId: activeSessionId, title: args });
|
|
307
341
|
setSessionTitle(args);
|
|
342
|
+
updateSessionTabTitle(activeSessionId, args);
|
|
308
343
|
} else {
|
|
309
344
|
handleRenameStart();
|
|
310
345
|
}
|
|
@@ -416,6 +451,53 @@ export function ChatView() {
|
|
|
416
451
|
<Pencil size={12} />
|
|
417
452
|
</button>
|
|
418
453
|
)}
|
|
454
|
+
{activeSessionId && bookmarks.length > 0 && (
|
|
455
|
+
<div className="relative">
|
|
456
|
+
<button
|
|
457
|
+
ref={bookmarkBtnRef}
|
|
458
|
+
onClick={function () { setShowBookmarkDropdown(!showBookmarkDropdown); }}
|
|
459
|
+
aria-label="View bookmarks"
|
|
460
|
+
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-mono text-warning/70 hover:text-warning hover:bg-warning/10 transition-colors"
|
|
461
|
+
>
|
|
462
|
+
<Bookmark size={11} />
|
|
463
|
+
<span>{bookmarks.length}</span>
|
|
464
|
+
</button>
|
|
465
|
+
{showBookmarkDropdown && (
|
|
466
|
+
<div
|
|
467
|
+
ref={bookmarkDropdownRef}
|
|
468
|
+
className="absolute top-full left-0 mt-1 z-50 bg-base-300 border border-base-content/15 rounded-lg shadow-xl py-1 w-[280px] max-h-[300px] overflow-y-auto"
|
|
469
|
+
>
|
|
470
|
+
<div className="px-2.5 py-1.5 text-[10px] uppercase tracking-widest text-base-content/40 font-mono font-bold">
|
|
471
|
+
Bookmarks
|
|
472
|
+
</div>
|
|
473
|
+
{bookmarks.map(function (bm) {
|
|
474
|
+
return (
|
|
475
|
+
<button
|
|
476
|
+
key={bm.id}
|
|
477
|
+
type="button"
|
|
478
|
+
onClick={function () {
|
|
479
|
+
setShowBookmarkDropdown(false);
|
|
480
|
+
var el = document.getElementById("msg-" + bm.messageUuid);
|
|
481
|
+
if (el) {
|
|
482
|
+
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
483
|
+
el.classList.add("ring-2", "ring-warning/40");
|
|
484
|
+
setTimeout(function () { el!.classList.remove("ring-2", "ring-warning/40"); }, 2000);
|
|
485
|
+
}
|
|
486
|
+
}}
|
|
487
|
+
className="flex items-start gap-2 w-full px-2.5 py-1.5 hover:bg-base-content/5 transition-colors text-left"
|
|
488
|
+
>
|
|
489
|
+
<Bookmark size={10} className="text-warning/60 mt-0.5 flex-shrink-0" />
|
|
490
|
+
<div className="min-w-0 flex-1">
|
|
491
|
+
<div className="text-[11px] text-base-content/60 truncate">{bm.messageText}</div>
|
|
492
|
+
<div className="text-[9px] text-base-content/30 font-mono">{bm.messageType}</div>
|
|
493
|
+
</div>
|
|
494
|
+
</button>
|
|
495
|
+
);
|
|
496
|
+
})}
|
|
497
|
+
</div>
|
|
498
|
+
)}
|
|
499
|
+
</div>
|
|
500
|
+
)}
|
|
419
501
|
</>
|
|
420
502
|
)}
|
|
421
503
|
</div>
|
|
@@ -885,7 +967,7 @@ export function ChatView() {
|
|
|
885
967
|
)}
|
|
886
968
|
|
|
887
969
|
{confirmStopExternal && (
|
|
888
|
-
<div className="fixed inset-0 z-[9999] flex items-center justify-center" role="dialog" aria-modal="true" aria-label="End External Process"
|
|
970
|
+
<div ref={stopExternalModalRef} className="fixed inset-0 z-[9999] flex items-center justify-center" role="dialog" aria-modal="true" aria-label="End External Process">
|
|
889
971
|
<div className="absolute inset-0 bg-black/50" onClick={function () { setConfirmStopExternal(false); }} />
|
|
890
972
|
<div className="relative bg-base-200 border border-base-content/15 rounded-2xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden">
|
|
891
973
|
<div className="px-5 py-4 border-b border-base-content/15">
|
|
@@ -950,12 +1032,42 @@ export function ChatView() {
|
|
|
950
1032
|
</div>
|
|
951
1033
|
)}
|
|
952
1034
|
|
|
1035
|
+
{budgetExceeded && budgetStatus && (
|
|
1036
|
+
<div className="flex-shrink-0 border-t border-base-300 bg-warning/10 px-4 py-3 flex items-center gap-3">
|
|
1037
|
+
<AlertTriangle size={16} className="text-warning flex-shrink-0" />
|
|
1038
|
+
<div className="flex-1 min-w-0">
|
|
1039
|
+
<div className="text-[13px] text-base-content font-medium">
|
|
1040
|
+
Daily budget exceeded (${budgetStatus.dailySpend.toFixed(2)} / ${budgetStatus.dailyLimit.toFixed(2)})
|
|
1041
|
+
</div>
|
|
1042
|
+
<div className="text-[11px] text-base-content/50">Send anyway?</div>
|
|
1043
|
+
</div>
|
|
1044
|
+
<button
|
|
1045
|
+
onClick={sendBudgetOverride}
|
|
1046
|
+
className="px-3 py-1.5 rounded-lg bg-warning text-warning-content text-[12px] font-semibold hover:bg-warning/80 transition-colors"
|
|
1047
|
+
>
|
|
1048
|
+
Continue
|
|
1049
|
+
</button>
|
|
1050
|
+
<button
|
|
1051
|
+
onClick={dismissBudgetExceeded}
|
|
1052
|
+
className="px-3 py-1.5 rounded-lg border border-base-content/15 text-base-content/60 text-[12px] hover:bg-base-content/5 transition-colors"
|
|
1053
|
+
>
|
|
1054
|
+
Cancel
|
|
1055
|
+
</button>
|
|
1056
|
+
</div>
|
|
1057
|
+
)}
|
|
1058
|
+
|
|
953
1059
|
<div className="flex-shrink-0 border-t border-base-300 bg-base-200 px-2 sm:px-4 pb-3 pt-2">
|
|
954
1060
|
<ChatInput
|
|
955
1061
|
sessionId={activeSessionId}
|
|
956
1062
|
onSend={handleSend}
|
|
957
|
-
disabled={!activeSessionId || !online || isBusy}
|
|
958
|
-
disabledPlaceholder={
|
|
1063
|
+
disabled={!activeSessionId || !online || isBusy || (budgetStatus !== null && budgetStatus.enforcement === "hard-block" && budgetStatus.dailySpend >= budgetStatus.dailyLimit)}
|
|
1064
|
+
disabledPlaceholder={
|
|
1065
|
+
isBusy
|
|
1066
|
+
? "Session in use by another client..."
|
|
1067
|
+
: (budgetStatus !== null && budgetStatus.enforcement === "hard-block" && budgetStatus.dailySpend >= budgetStatus.dailyLimit)
|
|
1068
|
+
? "Daily budget exceeded ($" + budgetStatus.dailySpend.toFixed(2) + " / $" + budgetStatus.dailyLimit.toFixed(2) + ")"
|
|
1069
|
+
: undefined
|
|
1070
|
+
}
|
|
959
1071
|
failedInput={failedInput}
|
|
960
1072
|
onFailedInputConsumed={clearFailedInput}
|
|
961
1073
|
prefillText={prefillText}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { useState, useRef, useEffect, memo } from "react";
|
|
1
|
+
import { useState, useRef, useEffect, memo, useMemo } from "react";
|
|
2
2
|
import Markdown from "react-markdown";
|
|
3
3
|
import remarkGfm from "remark-gfm";
|
|
4
|
-
import { Wrench, TriangleAlert, ChevronDown, ChevronRight, Check, X, Shield, Zap, Link, Copy, SquarePlus } from "lucide-react";
|
|
4
|
+
import { Wrench, TriangleAlert, ChevronDown, ChevronRight, Check, X, Shield, Zap, Link, Copy, SquarePlus, Bookmark, BookmarkCheck } from "lucide-react";
|
|
5
5
|
import type { HistoryMessage, ChatPermissionResponseMessage } from "@lattice/shared";
|
|
6
|
+
import { useStore } from "@tanstack/react-store";
|
|
6
7
|
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
7
8
|
import { getSessionStore, setPendingPrefill } from "../../stores/session";
|
|
9
|
+
import { getBookmarkStore, findBookmarkByUuid } from "../../stores/bookmarks";
|
|
8
10
|
import { ToolResultRenderer } from "./ToolResultRenderer";
|
|
9
11
|
import { formatToolSummary } from "./toolSummary";
|
|
10
12
|
import { PromptQuestion } from "./PromptQuestion";
|
|
@@ -104,9 +106,17 @@ function stripMarkdown(text: string): string {
|
|
|
104
106
|
.trim();
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
function MessageActions(props: { text: string; showNewSession?: boolean }) {
|
|
109
|
+
function MessageActions(props: { text: string; showNewSession?: boolean; messageUuid?: string; messageType?: "user" | "assistant" }) {
|
|
108
110
|
var [copied, setCopied] = useState(false);
|
|
109
111
|
var ws = useWebSocket();
|
|
112
|
+
var bookmarkState = useStore(getBookmarkStore(), function (s) { return s; });
|
|
113
|
+
var isBookmarked = useMemo(function () {
|
|
114
|
+
if (!props.messageUuid) return false;
|
|
115
|
+
for (var i = 0; i < bookmarkState.bookmarks.length; i++) {
|
|
116
|
+
if (bookmarkState.bookmarks[i].messageUuid === props.messageUuid) return true;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}, [props.messageUuid, bookmarkState.bookmarks]);
|
|
110
120
|
|
|
111
121
|
function handleCopy(e: React.MouseEvent) {
|
|
112
122
|
var content = e.shiftKey ? stripMarkdown(props.text) : props.text;
|
|
@@ -122,6 +132,26 @@ function MessageActions(props: { text: string; showNewSession?: boolean }) {
|
|
|
122
132
|
ws.send({ type: "session:create", projectSlug: state.activeProjectSlug });
|
|
123
133
|
}
|
|
124
134
|
|
|
135
|
+
function handleBookmarkToggle() {
|
|
136
|
+
var state = getSessionStore().state;
|
|
137
|
+
if (!state.activeSessionId || !state.activeProjectSlug || !props.messageUuid || !props.messageType) return;
|
|
138
|
+
if (isBookmarked) {
|
|
139
|
+
var bm = findBookmarkByUuid(props.messageUuid);
|
|
140
|
+
if (bm) {
|
|
141
|
+
ws.send({ type: "bookmark:remove", id: bm.id });
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
ws.send({
|
|
145
|
+
type: "bookmark:add",
|
|
146
|
+
sessionId: state.activeSessionId,
|
|
147
|
+
projectSlug: state.activeProjectSlug,
|
|
148
|
+
messageUuid: props.messageUuid,
|
|
149
|
+
messageText: props.text.slice(0, 100),
|
|
150
|
+
messageType: props.messageType,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
125
155
|
var btnClass = "opacity-0 group-hover/msg:opacity-100 transition-opacity duration-150 text-base-content/20 hover:text-base-content/50 cursor-pointer p-0.5 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:outline-none rounded";
|
|
126
156
|
|
|
127
157
|
return (
|
|
@@ -134,6 +164,11 @@ function MessageActions(props: { text: string; showNewSession?: boolean }) {
|
|
|
134
164
|
<SquarePlus size={11} />
|
|
135
165
|
</button>
|
|
136
166
|
)}
|
|
167
|
+
{props.messageUuid && props.messageType && (
|
|
168
|
+
<button type="button" onClick={handleBookmarkToggle} className={btnClass + (isBookmarked ? " !opacity-100 !text-warning" : "")} title={isBookmarked ? "Remove bookmark" : "Bookmark message"}>
|
|
169
|
+
{isBookmarked ? <BookmarkCheck size={11} /> : <Bookmark size={11} />}
|
|
170
|
+
</button>
|
|
171
|
+
)}
|
|
137
172
|
</>
|
|
138
173
|
);
|
|
139
174
|
}
|
|
@@ -179,7 +214,7 @@ function SkillMessage(props: { skillName: string; content: string; time: string
|
|
|
179
214
|
<div className="chat-footer text-[10px] text-base-content/30 mt-0.5 flex items-center gap-1">
|
|
180
215
|
{props.time}
|
|
181
216
|
<MessageAnchor id={props.uuid} />
|
|
182
|
-
<MessageActions text={"/" + props.skillName} showNewSession />
|
|
217
|
+
<MessageActions text={"/" + props.skillName} showNewSession messageUuid={props.uuid} messageType="user" />
|
|
183
218
|
</div>
|
|
184
219
|
)}
|
|
185
220
|
</div>
|
|
@@ -205,7 +240,7 @@ function UserMessage(props: { message: HistoryMessage }) {
|
|
|
205
240
|
<div className="chat-footer text-[10px] text-base-content/30 mt-0.5 flex items-center gap-1">
|
|
206
241
|
{time}
|
|
207
242
|
<MessageAnchor id={msg.uuid} />
|
|
208
|
-
<MessageActions text={text} showNewSession />
|
|
243
|
+
<MessageActions text={text} showNewSession messageUuid={msg.uuid} messageType="user" />
|
|
209
244
|
</div>
|
|
210
245
|
)}
|
|
211
246
|
</div>
|
|
@@ -256,7 +291,7 @@ function AssistantMessage(props: { message: HistoryMessage; responseCost?: numbe
|
|
|
256
291
|
<span className="text-base-content/15">{formatTokenCount(msg.outputTokens)} out</span>
|
|
257
292
|
)}
|
|
258
293
|
<MessageAnchor id={msg.uuid} />
|
|
259
|
-
<MessageActions text={msg.text || ""} showNewSession />
|
|
294
|
+
<MessageActions text={msg.text || ""} showNewSession messageUuid={msg.uuid} messageType="assistant" />
|
|
260
295
|
</div>
|
|
261
296
|
)}
|
|
262
297
|
</div>
|
|
@@ -90,7 +90,7 @@ export function PromptQuestion(props: PromptQuestionProps) {
|
|
|
90
90
|
)}
|
|
91
91
|
<span className="text-[12px] text-base-content/50">{firstAnswer ? firstAnswer[0] : ""}</span>
|
|
92
92
|
<span className="flex-1" />
|
|
93
|
-
<span className="text-[12px] font-medium text-base-content/70">{firstAnswer ? firstAnswer[1] : ""}</span>
|
|
93
|
+
<span className="text-[12px] font-medium text-base-content/70">{firstAnswer ? String(firstAnswer[1]) : ""}</span>
|
|
94
94
|
<ChevronDown
|
|
95
95
|
size={12}
|
|
96
96
|
className={"text-base-content/25 transition-transform duration-200 " + (expanded ? "rotate-180" : "")}
|
|
@@ -100,7 +100,7 @@ export function PromptQuestion(props: PromptQuestionProps) {
|
|
|
100
100
|
{expanded && answeredQuestion && (
|
|
101
101
|
<div className="px-4 pb-3 border-t border-base-content/5">
|
|
102
102
|
<div className="flex flex-col gap-1 pt-2">
|
|
103
|
-
{answeredQuestion.options.map(function (opt, oi) {
|
|
103
|
+
{answeredQuestion.options.map(function (opt: typeof answeredQuestion.options[number], oi: number) {
|
|
104
104
|
var isChosen = firstAnswer && firstAnswer[1] === opt.label;
|
|
105
105
|
return (
|
|
106
106
|
<div
|
|
@@ -142,7 +142,7 @@ export function PromptQuestion(props: PromptQuestionProps) {
|
|
|
142
142
|
|
|
143
143
|
return (
|
|
144
144
|
<div className="px-5 py-2" aria-live="polite">
|
|
145
|
-
{questions.map(function (q, qi) {
|
|
145
|
+
{questions.map(function (q: typeof questions[number], qi: number) {
|
|
146
146
|
return (
|
|
147
147
|
<div
|
|
148
148
|
key={qi}
|
|
@@ -165,7 +165,7 @@ export function PromptQuestion(props: PromptQuestionProps) {
|
|
|
165
165
|
aria-label={q.question}
|
|
166
166
|
onKeyDown={function (e) { handleKeyDown(e, q.options.length, q, q.options); }}
|
|
167
167
|
>
|
|
168
|
-
{q.options.map(function (opt, oi) {
|
|
168
|
+
{q.options.map(function (opt: typeof q.options[number], oi: number) {
|
|
169
169
|
var isSelected = selectedMulti.has(opt.label);
|
|
170
170
|
var isFocused = focusIndex === oi;
|
|
171
171
|
return (
|
|
@@ -19,7 +19,7 @@ export function TodoCard(props: TodoCardProps) {
|
|
|
19
19
|
var todos = props.message.todos || [];
|
|
20
20
|
if (todos.length === 0) return null;
|
|
21
21
|
|
|
22
|
-
var completed = todos.filter(function (t) { return t.status === "completed"; }).length;
|
|
22
|
+
var completed = todos.filter(function (t: typeof todos[number]) { return t.status === "completed"; }).length;
|
|
23
23
|
var total = todos.length;
|
|
24
24
|
|
|
25
25
|
return (
|
|
@@ -32,7 +32,7 @@ export function TodoCard(props: TodoCardProps) {
|
|
|
32
32
|
</div>
|
|
33
33
|
|
|
34
34
|
<div className="px-3 py-2">
|
|
35
|
-
{todos.map(function (todo) {
|
|
35
|
+
{todos.map(function (todo: typeof todos[number]) {
|
|
36
36
|
return (
|
|
37
37
|
<div
|
|
38
38
|
key={todo.id}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useState, useEffect } from "react";
|
|
1
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
2
|
+
import { useFocusTrap } from "../../hooks/useFocusTrap";
|
|
2
3
|
import { createPortal } from "react-dom";
|
|
3
4
|
import Markdown from "react-markdown";
|
|
4
5
|
import remarkGfm from "remark-gfm";
|
|
@@ -175,6 +176,9 @@ function ImageRenderer(props: { path: string }) {
|
|
|
175
176
|
var [error, setError] = useState(false);
|
|
176
177
|
var [modalOpen, setModalOpen] = useState(false);
|
|
177
178
|
var imgSrc = "/api/file?path=" + encodeURIComponent(props.path);
|
|
179
|
+
var imageModalRef = useRef<HTMLDivElement>(null);
|
|
180
|
+
var closeImageModal = useCallback(function () { setModalOpen(false); }, []);
|
|
181
|
+
useFocusTrap(imageModalRef, closeImageModal, modalOpen);
|
|
178
182
|
|
|
179
183
|
useEffect(function () {
|
|
180
184
|
if (!modalOpen) return;
|
|
@@ -213,9 +217,9 @@ function ImageRenderer(props: { path: string }) {
|
|
|
213
217
|
</div>
|
|
214
218
|
{modalOpen && createPortal(
|
|
215
219
|
<div
|
|
220
|
+
ref={imageModalRef}
|
|
216
221
|
className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/80 backdrop-blur-sm p-4 sm:p-8 cursor-pointer overscroll-contain"
|
|
217
222
|
onClick={function () { setModalOpen(false); }}
|
|
218
|
-
onKeyDown={function (e) { if (e.key === "Escape") setModalOpen(false); }}
|
|
219
223
|
onWheel={function (e) { e.stopPropagation(); }}
|
|
220
224
|
onTouchMove={function (e) { e.stopPropagation(); }}
|
|
221
225
|
role="dialog"
|