beads-map 0.3.4 → 0.3.5
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/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +2 -2
- package/.next/app-path-routes-manifest.json +1 -1
- package/.next/build-manifest.json +2 -2
- package/.next/next-minimal-server.js.nft.json +1 -1
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/api/beads.body +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +2 -2
- package/.next/server/app/page.js +3 -3
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +1 -1
- package/.next/server/functions-config-manifest.json +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/app/page-7a8908706a09b720.js +1 -0
- package/.next/static/css/a506e3d172da58ef.css +3 -0
- package/app/page.tsx +23 -0
- package/components/DescriptionModal.tsx +313 -10
- package/components/HelpPanel.tsx +4 -1
- package/components/NodeDetail.tsx +3 -1
- package/components/SettingsModal.tsx +235 -0
- package/components/TutorialOverlay.tsx +3 -3
- package/lib/settings.ts +42 -0
- package/lib/tts.ts +137 -0
- package/package.json +1 -1
- package/.next/static/chunks/app/page-cf8e14cb4afc8112.js +0 -1
- package/.next/static/css/ade5301262971664.css +0 -3
- /package/.next/static/{JmL0suxsggbSwPxWcmUFV → e6v54SLUeGDtx1DXW7JjL}/_buildManifest.js +0 -0
- /package/.next/static/{JmL0suxsggbSwPxWcmUFV → e6v54SLUeGDtx1DXW7JjL}/_ssgManifest.js +0 -0
package/app/page.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import AllCommentsPanel from "@/components/AllCommentsPanel";
|
|
|
19
19
|
import { ActivityOverlay } from "@/components/ActivityOverlay";
|
|
20
20
|
import { ActivityPanel } from "@/components/ActivityPanel";
|
|
21
21
|
import { HelpPanel } from "@/components/HelpPanel";
|
|
22
|
+
import { SettingsModal } from "@/components/SettingsModal";
|
|
22
23
|
import { TutorialOverlay, TUTORIAL_STEPS } from "@/components/TutorialOverlay";
|
|
23
24
|
import { useBeadsComments } from "@/hooks/useBeadsComments";
|
|
24
25
|
import type { BeadsComment } from "@/hooks/useBeadsComments";
|
|
@@ -294,6 +295,9 @@ export default function Home() {
|
|
|
294
295
|
const [descriptionModalNode, setDescriptionModalNode] =
|
|
295
296
|
useState<GraphNode | null>(null);
|
|
296
297
|
|
|
298
|
+
// Settings modal state
|
|
299
|
+
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
|
300
|
+
|
|
297
301
|
// Avatar hover tooltip state
|
|
298
302
|
const [avatarTooltip, setAvatarTooltip] = useState<{
|
|
299
303
|
handle: string;
|
|
@@ -1306,6 +1310,16 @@ export default function Home() {
|
|
|
1306
1310
|
<span className="hidden sm:inline">Learn</span>
|
|
1307
1311
|
</button>
|
|
1308
1312
|
<div className="w-px h-5 bg-zinc-200 mx-2" />
|
|
1313
|
+
<button
|
|
1314
|
+
onClick={() => setSettingsModalOpen(true)}
|
|
1315
|
+
className="p-2 text-zinc-400 hover:text-zinc-600 hover:bg-zinc-50 rounded-full transition-colors"
|
|
1316
|
+
title="Settings"
|
|
1317
|
+
>
|
|
1318
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
|
1319
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 010 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 010-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28z" />
|
|
1320
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
1321
|
+
</svg>
|
|
1322
|
+
</button>
|
|
1309
1323
|
<AuthButton />
|
|
1310
1324
|
</div>
|
|
1311
1325
|
</div>
|
|
@@ -1497,9 +1511,16 @@ export default function Home() {
|
|
|
1497
1511
|
node={descriptionModalNode}
|
|
1498
1512
|
onClose={() => setDescriptionModalNode(null)}
|
|
1499
1513
|
repoUrl={repoUrls[descriptionModalNode.prefix]}
|
|
1514
|
+
onOpenSettings={() => setSettingsModalOpen(true)}
|
|
1500
1515
|
/>
|
|
1501
1516
|
)}
|
|
1502
1517
|
|
|
1518
|
+
{/* Settings modal */}
|
|
1519
|
+
<SettingsModal
|
|
1520
|
+
isOpen={settingsModalOpen}
|
|
1521
|
+
onClose={() => setSettingsModalOpen(false)}
|
|
1522
|
+
/>
|
|
1523
|
+
|
|
1503
1524
|
{/* Node hover tooltip */}
|
|
1504
1525
|
{nodeTooltip && !avatarTooltip && (
|
|
1505
1526
|
<BeadTooltip
|
|
@@ -1618,6 +1639,7 @@ export default function Home() {
|
|
|
1618
1639
|
isAuthenticated={isAuthenticated}
|
|
1619
1640
|
currentDid={session?.did}
|
|
1620
1641
|
repoUrls={repoUrls}
|
|
1642
|
+
onOpenSettings={() => setSettingsModalOpen(true)}
|
|
1621
1643
|
/>
|
|
1622
1644
|
|
|
1623
1645
|
</div>
|
|
@@ -1676,6 +1698,7 @@ export default function Home() {
|
|
|
1676
1698
|
isAuthenticated={isAuthenticated}
|
|
1677
1699
|
currentDid={session?.did}
|
|
1678
1700
|
repoUrls={repoUrls}
|
|
1701
|
+
onOpenSettings={() => setSettingsModalOpen(true)}
|
|
1679
1702
|
/>
|
|
1680
1703
|
</div>
|
|
1681
1704
|
</div>
|
|
@@ -1,29 +1,124 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useState, useCallback, useEffect, useRef } from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
5
|
import ReactMarkdown from "react-markdown";
|
|
6
6
|
import remarkGfm from "remark-gfm";
|
|
7
7
|
import type { GraphNode } from "@/lib/types";
|
|
8
8
|
import { buildDescriptionCopyText } from "@/lib/utils";
|
|
9
|
+
import {
|
|
10
|
+
speakWithElevenLabs,
|
|
11
|
+
stopTts,
|
|
12
|
+
stripMarkdown,
|
|
13
|
+
setTtsPlaybackRate,
|
|
14
|
+
type TtsState,
|
|
15
|
+
} from "@/lib/tts";
|
|
16
|
+
import { hasApiKey } from "@/lib/settings";
|
|
17
|
+
|
|
18
|
+
const SPEED_PRESETS = [
|
|
19
|
+
{ label: "Normal", value: 1 },
|
|
20
|
+
{ label: "1.25x", value: 1.25 },
|
|
21
|
+
{ label: "1.5x", value: 1.5 },
|
|
22
|
+
{ label: "1.75x", value: 1.75 },
|
|
23
|
+
{ label: "2x", value: 2 },
|
|
24
|
+
];
|
|
9
25
|
|
|
10
26
|
interface DescriptionModalProps {
|
|
11
27
|
node: GraphNode;
|
|
12
28
|
onClose: () => void;
|
|
13
29
|
repoUrl?: string;
|
|
30
|
+
onOpenSettings?: () => void;
|
|
14
31
|
}
|
|
15
32
|
|
|
16
|
-
export function DescriptionModal({
|
|
33
|
+
export function DescriptionModal({
|
|
34
|
+
node,
|
|
35
|
+
onClose,
|
|
36
|
+
repoUrl,
|
|
37
|
+
onOpenSettings,
|
|
38
|
+
}: DescriptionModalProps) {
|
|
17
39
|
const [copied, setCopied] = useState(false);
|
|
40
|
+
const [ttsState, setTtsState] = useState<TtsState>("idle");
|
|
41
|
+
const [ttsError, setTtsError] = useState<string | null>(null);
|
|
42
|
+
const [ttsSpeed, setTtsSpeedState] = useState(1);
|
|
43
|
+
const [speedMenuOpen, setSpeedMenuOpen] = useState(false);
|
|
44
|
+
const [customSpeedInput, setCustomSpeedInput] = useState("");
|
|
45
|
+
const [showCustomInput, setShowCustomInput] = useState(false);
|
|
46
|
+
const speedBtnRef = useRef<HTMLButtonElement>(null);
|
|
47
|
+
const speedMenuRef = useRef<HTMLDivElement>(null);
|
|
18
48
|
|
|
19
49
|
const handleCopy = () => {
|
|
20
50
|
if (!node.description) return;
|
|
21
|
-
navigator.clipboard
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
51
|
+
navigator.clipboard
|
|
52
|
+
.writeText(buildDescriptionCopyText(node, repoUrl))
|
|
53
|
+
.then(() => {
|
|
54
|
+
setCopied(true);
|
|
55
|
+
setTimeout(() => setCopied(false), 1500);
|
|
56
|
+
});
|
|
25
57
|
};
|
|
26
58
|
|
|
59
|
+
const handleTts = useCallback(() => {
|
|
60
|
+
if (!hasApiKey()) {
|
|
61
|
+
onOpenSettings?.();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const plainText = stripMarkdown(node.description || "");
|
|
65
|
+
if (!plainText) return;
|
|
66
|
+
setTtsError(null);
|
|
67
|
+
speakWithElevenLabs(plainText, (state, error) => {
|
|
68
|
+
setTtsState(state);
|
|
69
|
+
if (error) setTtsError(error);
|
|
70
|
+
});
|
|
71
|
+
}, [node.description, onOpenSettings]);
|
|
72
|
+
|
|
73
|
+
const handleStopTts = useCallback(() => {
|
|
74
|
+
stopTts();
|
|
75
|
+
setTtsState("idle");
|
|
76
|
+
setSpeedMenuOpen(false);
|
|
77
|
+
setShowCustomInput(false);
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
const handleSpeedChange = useCallback((speed: number) => {
|
|
81
|
+
const clamped = Math.max(0.25, Math.min(4, speed));
|
|
82
|
+
setTtsSpeedState(clamped);
|
|
83
|
+
setTtsPlaybackRate(clamped);
|
|
84
|
+
setSpeedMenuOpen(false);
|
|
85
|
+
setShowCustomInput(false);
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
// Apply speed when playback starts (carries over from previous session)
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (ttsState === "playing") {
|
|
91
|
+
setTtsPlaybackRate(ttsSpeed);
|
|
92
|
+
}
|
|
93
|
+
}, [ttsState, ttsSpeed]);
|
|
94
|
+
|
|
95
|
+
// Click-outside handler for speed menu
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!speedMenuOpen) return;
|
|
98
|
+
const handler = (e: MouseEvent) => {
|
|
99
|
+
if (
|
|
100
|
+
speedMenuRef.current && !speedMenuRef.current.contains(e.target as Node) &&
|
|
101
|
+
speedBtnRef.current && !speedBtnRef.current.contains(e.target as Node)
|
|
102
|
+
) {
|
|
103
|
+
setSpeedMenuOpen(false);
|
|
104
|
+
setShowCustomInput(false);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const timer = setTimeout(() => window.addEventListener("mousedown", handler), 50);
|
|
108
|
+
return () => {
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
window.removeEventListener("mousedown", handler);
|
|
111
|
+
};
|
|
112
|
+
}, [speedMenuOpen]);
|
|
113
|
+
|
|
114
|
+
// Stop TTS on unmount / modal close
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
return () => {
|
|
117
|
+
stopTts();
|
|
118
|
+
setSpeedMenuOpen(false);
|
|
119
|
+
};
|
|
120
|
+
}, []);
|
|
121
|
+
|
|
27
122
|
if (!node.description) return null;
|
|
28
123
|
|
|
29
124
|
return createPortal(
|
|
@@ -46,21 +141,203 @@ export function DescriptionModal({ node, onClose, repoUrl }: DescriptionModalPro
|
|
|
46
141
|
</span>
|
|
47
142
|
</div>
|
|
48
143
|
<div className="flex items-center gap-1 shrink-0">
|
|
144
|
+
{/* Copy button */}
|
|
49
145
|
<button
|
|
50
146
|
onClick={handleCopy}
|
|
51
147
|
className="p-1 text-zinc-400 hover:text-zinc-600 transition-colors"
|
|
52
148
|
title="Copy description"
|
|
53
149
|
>
|
|
54
150
|
{copied ? (
|
|
55
|
-
<svg
|
|
56
|
-
|
|
151
|
+
<svg
|
|
152
|
+
className="w-4 h-4 text-emerald-500"
|
|
153
|
+
fill="none"
|
|
154
|
+
stroke="currentColor"
|
|
155
|
+
viewBox="0 0 24 24"
|
|
156
|
+
strokeWidth={2}
|
|
157
|
+
>
|
|
158
|
+
<path
|
|
159
|
+
strokeLinecap="round"
|
|
160
|
+
strokeLinejoin="round"
|
|
161
|
+
d="M4.5 12.75l6 6 9-13.5"
|
|
162
|
+
/>
|
|
57
163
|
</svg>
|
|
58
164
|
) : (
|
|
59
|
-
<svg
|
|
60
|
-
|
|
165
|
+
<svg
|
|
166
|
+
className="w-4 h-4"
|
|
167
|
+
fill="none"
|
|
168
|
+
stroke="currentColor"
|
|
169
|
+
viewBox="0 0 24 24"
|
|
170
|
+
strokeWidth={2}
|
|
171
|
+
>
|
|
172
|
+
<path
|
|
173
|
+
strokeLinecap="round"
|
|
174
|
+
strokeLinejoin="round"
|
|
175
|
+
d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9.75a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184"
|
|
176
|
+
/>
|
|
61
177
|
</svg>
|
|
62
178
|
)}
|
|
63
179
|
</button>
|
|
180
|
+
|
|
181
|
+
{/* TTS button */}
|
|
182
|
+
{ttsState === "loading" ? (
|
|
183
|
+
<button
|
|
184
|
+
disabled
|
|
185
|
+
className="p-1 text-zinc-300 cursor-wait"
|
|
186
|
+
title="Loading audio..."
|
|
187
|
+
>
|
|
188
|
+
<svg
|
|
189
|
+
className="w-4 h-4 animate-spin"
|
|
190
|
+
fill="none"
|
|
191
|
+
viewBox="0 0 24 24"
|
|
192
|
+
>
|
|
193
|
+
<circle
|
|
194
|
+
className="opacity-25"
|
|
195
|
+
cx="12"
|
|
196
|
+
cy="12"
|
|
197
|
+
r="10"
|
|
198
|
+
stroke="currentColor"
|
|
199
|
+
strokeWidth={4}
|
|
200
|
+
/>
|
|
201
|
+
<path
|
|
202
|
+
className="opacity-75"
|
|
203
|
+
fill="currentColor"
|
|
204
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
205
|
+
/>
|
|
206
|
+
</svg>
|
|
207
|
+
</button>
|
|
208
|
+
) : ttsState === "playing" ? (
|
|
209
|
+
<button
|
|
210
|
+
onClick={handleStopTts}
|
|
211
|
+
className="p-1 text-emerald-500 hover:text-red-500 hover:bg-red-50 rounded transition-colors"
|
|
212
|
+
title="Stop reading"
|
|
213
|
+
>
|
|
214
|
+
<svg
|
|
215
|
+
className="w-4 h-4"
|
|
216
|
+
fill="none"
|
|
217
|
+
viewBox="0 0 24 24"
|
|
218
|
+
strokeWidth={1.5}
|
|
219
|
+
stroke="currentColor"
|
|
220
|
+
>
|
|
221
|
+
<path
|
|
222
|
+
strokeLinecap="round"
|
|
223
|
+
strokeLinejoin="round"
|
|
224
|
+
d="M5.25 7.5A2.25 2.25 0 017.5 5.25h9a2.25 2.25 0 012.25 2.25v9a2.25 2.25 0 01-2.25 2.25h-9a2.25 2.25 0 01-2.25-2.25v-9z"
|
|
225
|
+
/>
|
|
226
|
+
</svg>
|
|
227
|
+
</button>
|
|
228
|
+
) : (
|
|
229
|
+
<button
|
|
230
|
+
onClick={handleTts}
|
|
231
|
+
className="p-1 text-zinc-400 hover:text-zinc-600 transition-colors"
|
|
232
|
+
title="Read aloud"
|
|
233
|
+
>
|
|
234
|
+
<svg
|
|
235
|
+
className="w-4 h-4"
|
|
236
|
+
fill="none"
|
|
237
|
+
viewBox="0 0 24 24"
|
|
238
|
+
strokeWidth={1.5}
|
|
239
|
+
stroke="currentColor"
|
|
240
|
+
>
|
|
241
|
+
<path
|
|
242
|
+
strokeLinecap="round"
|
|
243
|
+
strokeLinejoin="round"
|
|
244
|
+
d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
|
|
245
|
+
/>
|
|
246
|
+
</svg>
|
|
247
|
+
</button>
|
|
248
|
+
)}
|
|
249
|
+
|
|
250
|
+
{/* Speed selector — visible during playback/loading */}
|
|
251
|
+
{(ttsState === "playing" || ttsState === "loading") && (
|
|
252
|
+
<div className="relative">
|
|
253
|
+
<button
|
|
254
|
+
ref={speedBtnRef}
|
|
255
|
+
onClick={() => setSpeedMenuOpen((v) => !v)}
|
|
256
|
+
className="px-1.5 py-0.5 text-[11px] font-mono font-medium rounded text-zinc-500 hover:text-zinc-700 hover:bg-zinc-100 transition-colors"
|
|
257
|
+
title="Playback speed"
|
|
258
|
+
>
|
|
259
|
+
{ttsSpeed === 1 ? "1x" : `${ttsSpeed}x`}
|
|
260
|
+
</button>
|
|
261
|
+
|
|
262
|
+
{speedMenuOpen && (
|
|
263
|
+
<div
|
|
264
|
+
ref={speedMenuRef}
|
|
265
|
+
className="absolute right-0 top-full mt-1 bg-white border border-zinc-200 rounded-lg shadow-lg overflow-hidden z-[110]"
|
|
266
|
+
style={{ minWidth: 150 }}
|
|
267
|
+
onClick={(e) => e.stopPropagation()}
|
|
268
|
+
>
|
|
269
|
+
{SPEED_PRESETS.map((preset) => (
|
|
270
|
+
<button
|
|
271
|
+
key={preset.value}
|
|
272
|
+
onClick={() => handleSpeedChange(preset.value)}
|
|
273
|
+
className={`w-full px-3 py-2 text-xs text-left flex items-center justify-between hover:bg-zinc-50 transition-colors ${
|
|
274
|
+
ttsSpeed === preset.value ? "text-emerald-600 font-medium" : "text-zinc-700"
|
|
275
|
+
}`}
|
|
276
|
+
>
|
|
277
|
+
<span>{preset.label}</span>
|
|
278
|
+
{ttsSpeed === preset.value && (
|
|
279
|
+
<svg className="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
|
|
280
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
|
281
|
+
</svg>
|
|
282
|
+
)}
|
|
283
|
+
</button>
|
|
284
|
+
))}
|
|
285
|
+
|
|
286
|
+
<div className="border-t border-zinc-100" />
|
|
287
|
+
|
|
288
|
+
{!showCustomInput ? (
|
|
289
|
+
<button
|
|
290
|
+
onClick={() => {
|
|
291
|
+
setShowCustomInput(true);
|
|
292
|
+
setCustomSpeedInput(String(ttsSpeed));
|
|
293
|
+
}}
|
|
294
|
+
className={`w-full px-3 py-2 text-xs text-left flex items-center justify-between hover:bg-zinc-50 transition-colors ${
|
|
295
|
+
!SPEED_PRESETS.some((p) => p.value === ttsSpeed) ? "text-emerald-600 font-medium" : "text-zinc-700"
|
|
296
|
+
}`}
|
|
297
|
+
>
|
|
298
|
+
<span>Custom{!SPEED_PRESETS.some((p) => p.value === ttsSpeed) ? ` (${ttsSpeed}x)` : "..."}</span>
|
|
299
|
+
{!SPEED_PRESETS.some((p) => p.value === ttsSpeed) && (
|
|
300
|
+
<svg className="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
|
|
301
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
|
302
|
+
</svg>
|
|
303
|
+
)}
|
|
304
|
+
</button>
|
|
305
|
+
) : (
|
|
306
|
+
<div className="px-3 py-2 flex items-center gap-2">
|
|
307
|
+
<input
|
|
308
|
+
type="number"
|
|
309
|
+
min={0.25}
|
|
310
|
+
max={4}
|
|
311
|
+
step={0.25}
|
|
312
|
+
value={customSpeedInput}
|
|
313
|
+
onChange={(e) => setCustomSpeedInput(e.target.value)}
|
|
314
|
+
onKeyDown={(e) => {
|
|
315
|
+
if (e.key === "Enter") {
|
|
316
|
+
const val = parseFloat(customSpeedInput);
|
|
317
|
+
if (!isNaN(val) && val >= 0.25 && val <= 4) handleSpeedChange(val);
|
|
318
|
+
}
|
|
319
|
+
if (e.key === "Escape") {
|
|
320
|
+
setShowCustomInput(false);
|
|
321
|
+
setSpeedMenuOpen(false);
|
|
322
|
+
}
|
|
323
|
+
}}
|
|
324
|
+
onBlur={() => {
|
|
325
|
+
const val = parseFloat(customSpeedInput);
|
|
326
|
+
if (!isNaN(val) && val >= 0.25 && val <= 4) handleSpeedChange(val);
|
|
327
|
+
else setShowCustomInput(false);
|
|
328
|
+
}}
|
|
329
|
+
autoFocus
|
|
330
|
+
className="w-16 rounded border border-zinc-200 bg-zinc-50 px-2 py-1 text-xs text-zinc-900 focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
|
331
|
+
/>
|
|
332
|
+
<span className="text-[11px] text-zinc-400">x</span>
|
|
333
|
+
</div>
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
)}
|
|
339
|
+
|
|
340
|
+
{/* Close button */}
|
|
64
341
|
<button
|
|
65
342
|
onClick={onClose}
|
|
66
343
|
className="p-1 text-zinc-400 hover:text-zinc-600 transition-colors"
|
|
@@ -81,6 +358,32 @@ export function DescriptionModal({ node, onClose, repoUrl }: DescriptionModalPro
|
|
|
81
358
|
</button>
|
|
82
359
|
</div>
|
|
83
360
|
</div>
|
|
361
|
+
|
|
362
|
+
{/* TTS error banner */}
|
|
363
|
+
{ttsError && (
|
|
364
|
+
<div className="px-5 py-2 text-xs text-red-500 bg-red-50 border-b border-red-100 flex items-center justify-between">
|
|
365
|
+
<span>{ttsError}</span>
|
|
366
|
+
<button
|
|
367
|
+
onClick={() => setTtsError(null)}
|
|
368
|
+
className="text-red-400 hover:text-red-600 ml-2"
|
|
369
|
+
>
|
|
370
|
+
<svg
|
|
371
|
+
className="w-3 h-3"
|
|
372
|
+
fill="none"
|
|
373
|
+
viewBox="0 0 24 24"
|
|
374
|
+
strokeWidth={2}
|
|
375
|
+
stroke="currentColor"
|
|
376
|
+
>
|
|
377
|
+
<path
|
|
378
|
+
strokeLinecap="round"
|
|
379
|
+
strokeLinejoin="round"
|
|
380
|
+
d="M6 18L18 6M6 6l12 12"
|
|
381
|
+
/>
|
|
382
|
+
</svg>
|
|
383
|
+
</button>
|
|
384
|
+
</div>
|
|
385
|
+
)}
|
|
386
|
+
|
|
84
387
|
{/* Modal body */}
|
|
85
388
|
<div className="flex-1 overflow-y-auto px-5 py-4 custom-scrollbar description-markdown text-sm text-zinc-700 leading-relaxed">
|
|
86
389
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
package/components/HelpPanel.tsx
CHANGED
|
@@ -268,7 +268,7 @@ function HelpContent({ onStartTutorial }: { onStartTutorial?: () => void }) {
|
|
|
268
268
|
<ul className="space-y-1.5 mb-3">
|
|
269
269
|
<Bullet color={CAT.blue}><strong>Click</strong> a node to open its details</Bullet>
|
|
270
270
|
<Bullet color={CAT.blue}><strong>Hover</strong> for a quick summary</Bullet>
|
|
271
|
-
<Bullet color={CAT.blue}><strong>Right-click</strong> for actions — descriptions, comments, claims, collapse</Bullet>
|
|
271
|
+
<Bullet color={CAT.blue}><strong>Right-click</strong> for actions — descriptions, comments, claims, collapse, or focus on an epic</Bullet>
|
|
272
272
|
<Bullet color={CAT.blue}><strong>Scroll</strong> to zoom, <strong>drag</strong> to pan</Bullet>
|
|
273
273
|
<Bullet color={CAT.blue}><strong>Cmd/Ctrl+F</strong> to search by name, ID, owner, or commenter</Bullet>
|
|
274
274
|
</ul>
|
|
@@ -300,12 +300,15 @@ function HelpContent({ onStartTutorial }: { onStartTutorial?: () => void }) {
|
|
|
300
300
|
<SectionTitle color={CAT.mauve}>More</SectionTitle>
|
|
301
301
|
<ul className="space-y-1.5 mb-3">
|
|
302
302
|
<Bullet color={CAT.mauve}><strong>Collapse/Expand</strong> — tidy up epics into single nodes</Bullet>
|
|
303
|
+
<Bullet color={CAT.mauve}><strong>Focus on epic</strong> — right-click an epic to isolate its subgraph (children + dependencies). Click the banner or right-click again to return</Bullet>
|
|
303
304
|
<Bullet color={CAT.mauve}><strong>Clusters</strong> — dashed circles grouping projects when zoomed out</Bullet>
|
|
304
305
|
<Bullet color={CAT.mauve}><strong>Replay</strong> — step through your project's history</Bullet>
|
|
305
306
|
<Bullet color={CAT.mauve}><strong>Comments</strong> — leave notes on tasks (sign in first)</Bullet>
|
|
307
|
+
<Bullet color={CAT.mauve}><strong>Activity feed</strong> — real-time feed filtered to only your local beads, not global</Bullet>
|
|
306
308
|
<Bullet color={CAT.mauve}><strong>Claim tasks</strong> — right-click to mark as yours</Bullet>
|
|
307
309
|
<Bullet color={CAT.mauve}><strong>Minimap</strong> — click to jump, drag edges to resize</Bullet>
|
|
308
310
|
<Bullet color={CAT.mauve}><strong>Auto-fit</strong> — top-left toggle to lock/unlock automatic camera reframing</Bullet>
|
|
311
|
+
<Bullet color={CAT.mauve}><strong>Pulse</strong> — the most recently active node pulses with emerald ripples. Toggle it on/off in view controls</Bullet>
|
|
309
312
|
<Bullet color={CAT.mauve}><strong>Copy</strong> — clipboard icon copies task with project info</Bullet>
|
|
310
313
|
</ul>
|
|
311
314
|
|
|
@@ -30,6 +30,7 @@ interface NodeDetailProps {
|
|
|
30
30
|
isAuthenticated?: boolean;
|
|
31
31
|
currentDid?: string;
|
|
32
32
|
repoUrls?: Record<string, string>;
|
|
33
|
+
onOpenSettings?: () => void;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export default function NodeDetail({
|
|
@@ -44,6 +45,7 @@ export default function NodeDetail({
|
|
|
44
45
|
isAuthenticated,
|
|
45
46
|
currentDid,
|
|
46
47
|
repoUrls,
|
|
48
|
+
onOpenSettings,
|
|
47
49
|
}: NodeDetailProps) {
|
|
48
50
|
// Reply state — managed here so it's shared across the comment tree
|
|
49
51
|
const [replyingToUri, setReplyingToUri] = useState<string | null>(null);
|
|
@@ -356,7 +358,7 @@ export default function NodeDetail({
|
|
|
356
358
|
|
|
357
359
|
{/* Description expanded modal */}
|
|
358
360
|
{descriptionExpanded && node.description && (
|
|
359
|
-
<DescriptionModal node={node} onClose={() => setDescriptionExpanded(false)} repoUrl={repoUrls?.[node.prefix]} />
|
|
361
|
+
<DescriptionModal node={node} onClose={() => setDescriptionExpanded(false)} repoUrl={repoUrls?.[node.prefix]} onOpenSettings={onOpenSettings} />
|
|
360
362
|
)}
|
|
361
363
|
|
|
362
364
|
{/* Blocks (issues this blocks) */}
|