heartbeads 0.4.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/.next/BUILD_ID +1 -0
- package/.next/app-build-manifest.json +49 -0
- package/.next/app-path-routes-manifest.json +1 -0
- package/.next/build-manifest.json +32 -0
- package/.next/export-marker.json +1 -0
- package/.next/images-manifest.json +1 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +1 -0
- package/.next/react-loadable-manifest.json +8 -0
- package/.next/required-server-files.json +1 -0
- package/.next/routes-manifest.json +1 -0
- package/.next/server/app/_not-found/page.js +1 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_not-found.html +1 -0
- package/.next/server/app/_not-found.meta +6 -0
- package/.next/server/app/_not-found.rsc +9 -0
- package/.next/server/app/api/auth/route.js +1 -0
- package/.next/server/app/api/auth/route.js.nft.json +1 -0
- package/.next/server/app/api/beads/route.js +8 -0
- package/.next/server/app/api/beads/route.js.nft.json +1 -0
- package/.next/server/app/api/beads/stream/route.js +10 -0
- package/.next/server/app/api/beads/stream/route.js.nft.json +1 -0
- package/.next/server/app/api/beads.body +1 -0
- package/.next/server/app/api/beads.meta +1 -0
- package/.next/server/app/api/config/route.js +8 -0
- package/.next/server/app/api/config/route.js.nft.json +1 -0
- package/.next/server/app/api/docs/page.js +120 -0
- package/.next/server/app/api/docs/page.js.nft.json +1 -0
- package/.next/server/app/api/docs/page_client-reference-manifest.js +1 -0
- package/.next/server/app/api/docs.html +120 -0
- package/.next/server/app/api/docs.meta +5 -0
- package/.next/server/app/api/docs.rsc +70 -0
- package/.next/server/app/api/login/route.js +1 -0
- package/.next/server/app/api/login/route.js.nft.json +1 -0
- package/.next/server/app/api/logout/route.js +1 -0
- package/.next/server/app/api/logout/route.js.nft.json +1 -0
- package/.next/server/app/api/oauth/callback/route.js +1 -0
- package/.next/server/app/api/oauth/callback/route.js.nft.json +1 -0
- package/.next/server/app/api/oauth/client-metadata.json/route.js +1 -0
- package/.next/server/app/api/oauth/client-metadata.json/route.js.nft.json +1 -0
- package/.next/server/app/api/oauth/jwks.json/route.js +1 -0
- package/.next/server/app/api/oauth/jwks.json/route.js.nft.json +1 -0
- package/.next/server/app/api/records/route.js +1 -0
- package/.next/server/app/api/records/route.js.nft.json +1 -0
- package/.next/server/app/api/status/route.js +1 -0
- package/.next/server/app/api/status/route.js.nft.json +1 -0
- package/.next/server/app/api/v1/graph/route.js +1 -0
- package/.next/server/app/api/v1/graph/route.js.nft.json +1 -0
- package/.next/server/app/api/v1/issues/[id]/route.js +1 -0
- package/.next/server/app/api/v1/issues/[id]/route.js.nft.json +1 -0
- package/.next/server/app/api/v1/ready/route.js +1 -0
- package/.next/server/app/api/v1/ready/route.js.nft.json +1 -0
- package/.next/server/app/index.html +1 -0
- package/.next/server/app/index.meta +5 -0
- package/.next/server/app/index.rsc +9 -0
- package/.next/server/app/login/page.js +1 -0
- package/.next/server/app/login/page.js.nft.json +1 -0
- package/.next/server/app/login/page_client-reference-manifest.js +1 -0
- package/.next/server/app/login.html +1 -0
- package/.next/server/app/login.meta +5 -0
- package/.next/server/app/login.rsc +9 -0
- package/.next/server/app/opengraph-image.png/route.js +1 -0
- package/.next/server/app/opengraph-image.png/route.js.nft.json +1 -0
- package/.next/server/app/opengraph-image.png.body +0 -0
- package/.next/server/app/opengraph-image.png.meta +1 -0
- package/.next/server/app/page.js +24 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app/twitter-image.png/route.js +1 -0
- package/.next/server/app/twitter-image.png/route.js.nft.json +1 -0
- package/.next/server/app/twitter-image.png.body +0 -0
- package/.next/server/app/twitter-image.png.meta +1 -0
- package/.next/server/app-paths-manifest.json +22 -0
- package/.next/server/chunks/247.js +12 -0
- package/.next/server/chunks/29.js +1 -0
- package/.next/server/chunks/343.js +1 -0
- package/.next/server/chunks/460.js +12 -0
- package/.next/server/chunks/533.js +38 -0
- package/.next/server/chunks/542.js +27 -0
- package/.next/server/chunks/590.js +6 -0
- package/.next/server/chunks/615.js +15 -0
- package/.next/server/chunks/696.js +25 -0
- package/.next/server/chunks/719.js +2 -0
- package/.next/server/chunks/739.js +1 -0
- package/.next/server/chunks/950.js +2 -0
- package/.next/server/chunks/font-manifest.json +1 -0
- package/.next/server/edge-runtime-webpack.js +2 -0
- package/.next/server/edge-runtime-webpack.js.map +1 -0
- package/.next/server/font-manifest.json +1 -0
- package/.next/server/functions-config-manifest.json +1 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +32 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/middleware.js +14 -0
- package/.next/server/middleware.js.map +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/404.html +1 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages/_app.js +1 -0
- package/.next/server/pages/_app.js.nft.json +1 -0
- package/.next/server/pages/_document.js +1 -0
- package/.next/server/pages/_document.js.nft.json +1 -0
- package/.next/server/pages/_error.js +1 -0
- package/.next/server/pages/_error.js.nft.json +1 -0
- package/.next/server/pages-manifest.json +1 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/chunks/149.a3e3a5dc03e21086.js +1 -0
- package/.next/static/chunks/2200cc46-7c93a0e00b0bb825.js +1 -0
- package/.next/static/chunks/788-aa413085174e935a.js +1 -0
- package/.next/static/chunks/945-3ff1d381a0af1ecd.js +2 -0
- package/.next/static/chunks/971-bb44d52bcd9ee2a9.js +1 -0
- package/.next/static/chunks/app/_not-found/page-200b7a7a6cfc29df.js +1 -0
- package/.next/static/chunks/app/api/docs/page-1dc18f40154cdce6.js +1 -0
- package/.next/static/chunks/app/layout-13e3cdaaa416edb6.js +1 -0
- package/.next/static/chunks/app/login/page-60d930d64f021753.js +1 -0
- package/.next/static/chunks/app/not-found-ae1139bed2018dd8.js +1 -0
- package/.next/static/chunks/app/page-583300dd8af66e5a.js +1 -0
- package/.next/static/chunks/framework-6e06c675866dc992.js +1 -0
- package/.next/static/chunks/main-app-8b0c4a1007dbb7f4.js +1 -0
- package/.next/static/chunks/main-e680fb049d7426e1.js +1 -0
- package/.next/static/chunks/pages/_app-0c3037849002a4aa.js +1 -0
- package/.next/static/chunks/pages/_error-a647cd2c75dc4dc7.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-117444a4bfe51057.js +1 -0
- package/.next/static/css/8c1b520a38ba4ccd.css +3 -0
- package/.next/static/vFM69sDrBUf_9ULwPmVAE/_buildManifest.js +1 -0
- package/.next/static/vFM69sDrBUf_9ULwPmVAE/_ssgManifest.js +1 -0
- package/README.md +389 -0
- package/app/api/auth/route.ts +103 -0
- package/app/api/beads/route.ts +27 -0
- package/app/api/beads/stream/route.ts +83 -0
- package/app/api/config/route.ts +48 -0
- package/app/api/docs/page.tsx +497 -0
- package/app/api/login/route.ts +42 -0
- package/app/api/logout/route.ts +14 -0
- package/app/api/oauth/callback/route.ts +97 -0
- package/app/api/oauth/client-metadata.json/route.ts +33 -0
- package/app/api/oauth/jwks.json/route.ts +32 -0
- package/app/api/records/route.ts +168 -0
- package/app/api/status/route.ts +25 -0
- package/app/api/v1/graph/route.ts +251 -0
- package/app/api/v1/issues/[id]/route.ts +158 -0
- package/app/api/v1/ready/route.ts +229 -0
- package/app/globals.css +230 -0
- package/app/layout.tsx +51 -0
- package/app/login/page.tsx +164 -0
- package/app/not-found.tsx +91 -0
- package/app/opengraph-image.png +0 -0
- package/app/page.tsx +2041 -0
- package/app/twitter-image.png +0 -0
- package/bin/heartbeads.mjs +225 -0
- package/components/ActivityItem.tsx +326 -0
- package/components/ActivityOverlay.tsx +125 -0
- package/components/ActivityPanel.tsx +345 -0
- package/components/AllCommentsPanel.tsx +270 -0
- package/components/AuthButton.tsx +202 -0
- package/components/BeadTooltip.tsx +246 -0
- package/components/BeadsGraph.tsx +2493 -0
- package/components/BeadsLogo.tsx +94 -0
- package/components/CommentTooltip.tsx +338 -0
- package/components/ContextMenu.tsx +272 -0
- package/components/DescriptionModal.tsx +595 -0
- package/components/GraphStats.tsx +121 -0
- package/components/HeartIcon.tsx +33 -0
- package/components/HelpPanel.tsx +339 -0
- package/components/MobileActionSheet.tsx +255 -0
- package/components/NodeDetail.tsx +793 -0
- package/components/SettingsModal.tsx +315 -0
- package/components/StatusLegend.tsx +99 -0
- package/components/TimelineBar.tsx +116 -0
- package/components/TutorialOverlay.tsx +235 -0
- package/hooks/useBeadsComments.ts +81 -0
- package/hooks/useIsMobile.ts +19 -0
- package/lib/activity.ts +377 -0
- package/lib/agent.ts +29 -0
- package/lib/api-helpers.ts +46 -0
- package/lib/auth/client.ts +221 -0
- package/lib/auth.tsx +159 -0
- package/lib/comments.ts +413 -0
- package/lib/diff-beads.ts +128 -0
- package/lib/discover.ts +228 -0
- package/lib/env.ts +33 -0
- package/lib/gate.ts +55 -0
- package/lib/parse-beads.ts +234 -0
- package/lib/session.ts +52 -0
- package/lib/settings.ts +42 -0
- package/lib/timeline.ts +138 -0
- package/lib/tts.ts +397 -0
- package/lib/types.ts +271 -0
- package/lib/utils.ts +48 -0
- package/lib/watch-beads.ts +97 -0
- package/next.config.mjs +4 -0
- package/package.json +81 -0
- package/postcss.config.mjs +9 -0
- package/public/image.png +0 -0
- package/scripts/generate-jwk.js +38 -0
- package/tailwind.config.ts +41 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import {
|
|
6
|
+
getSettings,
|
|
7
|
+
saveSettings,
|
|
8
|
+
type BeadsMapSettings,
|
|
9
|
+
} from "@/lib/settings";
|
|
10
|
+
|
|
11
|
+
interface SettingsModalProps {
|
|
12
|
+
isOpen: boolean;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
|
|
17
|
+
const [apiKey, setApiKey] = useState("");
|
|
18
|
+
const [voiceId, setVoiceId] = useState("");
|
|
19
|
+
const [model, setModel] = useState("");
|
|
20
|
+
const [showKey, setShowKey] = useState(false);
|
|
21
|
+
const [mounted, setMounted] = useState(false);
|
|
22
|
+
const [isGateProtected, setIsGateProtected] = useState(false);
|
|
23
|
+
|
|
24
|
+
// SSR guard for createPortal
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
setMounted(true);
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
// Load current settings when modal opens
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (isOpen) {
|
|
32
|
+
const s = getSettings();
|
|
33
|
+
setApiKey(s.elevenLabsApiKey || "");
|
|
34
|
+
setVoiceId(s.elevenLabsVoiceId);
|
|
35
|
+
setModel(s.elevenLabsModel);
|
|
36
|
+
setShowKey(false);
|
|
37
|
+
|
|
38
|
+
// Check if dashboard is password-protected
|
|
39
|
+
fetch("/api/auth")
|
|
40
|
+
.then((r) => r.json())
|
|
41
|
+
.then((data) => setIsGateProtected(data.protected === true))
|
|
42
|
+
.catch(() => setIsGateProtected(false));
|
|
43
|
+
}
|
|
44
|
+
}, [isOpen]);
|
|
45
|
+
|
|
46
|
+
// Escape key
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!isOpen) return;
|
|
49
|
+
const handler = (e: KeyboardEvent) => {
|
|
50
|
+
if (e.key === "Escape") onClose();
|
|
51
|
+
};
|
|
52
|
+
window.addEventListener("keydown", handler);
|
|
53
|
+
return () => window.removeEventListener("keydown", handler);
|
|
54
|
+
}, [isOpen, onClose]);
|
|
55
|
+
|
|
56
|
+
const handleLockDashboard = useCallback(async () => {
|
|
57
|
+
try {
|
|
58
|
+
await fetch("/api/auth", { method: "DELETE" });
|
|
59
|
+
} catch {
|
|
60
|
+
// If delete fails, still redirect — middleware will catch it
|
|
61
|
+
}
|
|
62
|
+
window.location.href = "/login";
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const handleSave = useCallback(() => {
|
|
66
|
+
const updates: Partial<BeadsMapSettings> = {
|
|
67
|
+
elevenLabsApiKey: apiKey.trim() || undefined,
|
|
68
|
+
elevenLabsVoiceId: voiceId.trim() || "UgBBYS2sOqTuMpoF3BR0",
|
|
69
|
+
elevenLabsModel: model || "eleven_flash_v2_5",
|
|
70
|
+
};
|
|
71
|
+
saveSettings(updates);
|
|
72
|
+
onClose();
|
|
73
|
+
}, [apiKey, voiceId, model, onClose]);
|
|
74
|
+
|
|
75
|
+
if (!isOpen || !mounted) return null;
|
|
76
|
+
|
|
77
|
+
return createPortal(
|
|
78
|
+
<div
|
|
79
|
+
className="fixed inset-0 z-[100] flex items-center justify-center"
|
|
80
|
+
style={{ backgroundColor: "rgba(0,0,0,0.4)", backdropFilter: "blur(4px)" }}
|
|
81
|
+
onClick={onClose}
|
|
82
|
+
>
|
|
83
|
+
<div
|
|
84
|
+
className="bg-white rounded-xl shadow-2xl w-[90vw] max-w-md flex flex-col"
|
|
85
|
+
onClick={(e) => e.stopPropagation()}
|
|
86
|
+
>
|
|
87
|
+
{/* Header */}
|
|
88
|
+
<div className="flex items-center justify-between px-5 py-3.5 border-b border-zinc-100">
|
|
89
|
+
<h2 className="text-sm font-semibold text-zinc-900">Settings</h2>
|
|
90
|
+
<button
|
|
91
|
+
onClick={onClose}
|
|
92
|
+
className="p-1 text-zinc-400 hover:text-zinc-600 transition-colors"
|
|
93
|
+
>
|
|
94
|
+
<svg
|
|
95
|
+
className="w-4 h-4"
|
|
96
|
+
fill="none"
|
|
97
|
+
stroke="currentColor"
|
|
98
|
+
viewBox="0 0 24 24"
|
|
99
|
+
strokeWidth={2}
|
|
100
|
+
>
|
|
101
|
+
<path
|
|
102
|
+
strokeLinecap="round"
|
|
103
|
+
strokeLinejoin="round"
|
|
104
|
+
d="M6 18L18 6M6 6l12 12"
|
|
105
|
+
/>
|
|
106
|
+
</svg>
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{/* Body */}
|
|
111
|
+
<div className="px-5 py-4 space-y-4 overflow-y-auto max-h-[60vh]">
|
|
112
|
+
{/* Section header */}
|
|
113
|
+
<h3 className="text-[11px] font-semibold uppercase tracking-widest text-teal-600">
|
|
114
|
+
Text-to-Speech
|
|
115
|
+
</h3>
|
|
116
|
+
|
|
117
|
+
{/* API Key */}
|
|
118
|
+
<div>
|
|
119
|
+
<label className="block text-xs font-medium text-zinc-700 mb-1">
|
|
120
|
+
ElevenLabs API Key
|
|
121
|
+
</label>
|
|
122
|
+
<div className="relative">
|
|
123
|
+
<input
|
|
124
|
+
type={showKey ? "text" : "password"}
|
|
125
|
+
value={apiKey}
|
|
126
|
+
onChange={(e) => setApiKey(e.target.value)}
|
|
127
|
+
placeholder="sk_..."
|
|
128
|
+
className="w-full rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-900 placeholder:text-zinc-400 focus:outline-none focus:ring-2 focus:ring-emerald-500/30 focus:border-emerald-500 pr-10"
|
|
129
|
+
/>
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
onClick={() => setShowKey((v) => !v)}
|
|
133
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-zinc-400 hover:text-zinc-600 transition-colors"
|
|
134
|
+
title={showKey ? "Hide key" : "Show key"}
|
|
135
|
+
>
|
|
136
|
+
{showKey ? (
|
|
137
|
+
<svg
|
|
138
|
+
className="w-4 h-4"
|
|
139
|
+
fill="none"
|
|
140
|
+
viewBox="0 0 24 24"
|
|
141
|
+
strokeWidth={1.5}
|
|
142
|
+
stroke="currentColor"
|
|
143
|
+
>
|
|
144
|
+
<path
|
|
145
|
+
strokeLinecap="round"
|
|
146
|
+
strokeLinejoin="round"
|
|
147
|
+
d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"
|
|
148
|
+
/>
|
|
149
|
+
</svg>
|
|
150
|
+
) : (
|
|
151
|
+
<svg
|
|
152
|
+
className="w-4 h-4"
|
|
153
|
+
fill="none"
|
|
154
|
+
viewBox="0 0 24 24"
|
|
155
|
+
strokeWidth={1.5}
|
|
156
|
+
stroke="currentColor"
|
|
157
|
+
>
|
|
158
|
+
<path
|
|
159
|
+
strokeLinecap="round"
|
|
160
|
+
strokeLinejoin="round"
|
|
161
|
+
d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"
|
|
162
|
+
/>
|
|
163
|
+
<path
|
|
164
|
+
strokeLinecap="round"
|
|
165
|
+
strokeLinejoin="round"
|
|
166
|
+
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
167
|
+
/>
|
|
168
|
+
</svg>
|
|
169
|
+
)}
|
|
170
|
+
</button>
|
|
171
|
+
</div>
|
|
172
|
+
<p className="mt-1 text-[11px] text-zinc-400">
|
|
173
|
+
Get your key at{" "}
|
|
174
|
+
<a
|
|
175
|
+
href="https://elevenlabs.io/app/settings/api-keys"
|
|
176
|
+
target="_blank"
|
|
177
|
+
rel="noopener noreferrer"
|
|
178
|
+
className="underline underline-offset-2 text-teal-500 hover:text-teal-600"
|
|
179
|
+
>
|
|
180
|
+
elevenlabs.io/app/settings
|
|
181
|
+
</a>
|
|
182
|
+
</p>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Voice ID */}
|
|
186
|
+
<div>
|
|
187
|
+
<label className="block text-xs font-medium text-zinc-700 mb-1">
|
|
188
|
+
Voice ID
|
|
189
|
+
</label>
|
|
190
|
+
<input
|
|
191
|
+
type="text"
|
|
192
|
+
value={voiceId}
|
|
193
|
+
onChange={(e) => setVoiceId(e.target.value)}
|
|
194
|
+
placeholder="UgBBYS2sOqTuMpoF3BR0"
|
|
195
|
+
className="w-full rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-900 placeholder:text-zinc-400 focus:outline-none focus:ring-2 focus:ring-emerald-500/30 focus:border-emerald-500"
|
|
196
|
+
/>
|
|
197
|
+
<p className="mt-1 text-[11px] text-zinc-400">
|
|
198
|
+
Default: Mark - Natural Conversations. Find voice IDs in the{" "}
|
|
199
|
+
<a
|
|
200
|
+
href="https://elevenlabs.io/app/voice-library"
|
|
201
|
+
target="_blank"
|
|
202
|
+
rel="noopener noreferrer"
|
|
203
|
+
className="underline underline-offset-2 text-teal-500 hover:text-teal-600"
|
|
204
|
+
>
|
|
205
|
+
ElevenLabs Voice Library
|
|
206
|
+
</a>
|
|
207
|
+
.
|
|
208
|
+
</p>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
{/* Model */}
|
|
212
|
+
<div>
|
|
213
|
+
<label className="block text-xs font-medium text-zinc-700 mb-1">
|
|
214
|
+
Model
|
|
215
|
+
</label>
|
|
216
|
+
<select
|
|
217
|
+
value={model}
|
|
218
|
+
onChange={(e) => setModel(e.target.value)}
|
|
219
|
+
className="w-full rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-900 focus:outline-none focus:ring-2 focus:ring-emerald-500/30 focus:border-emerald-500"
|
|
220
|
+
>
|
|
221
|
+
<option value="eleven_flash_v2_5">
|
|
222
|
+
Flash v2.5 (default, fastest & cheapest)
|
|
223
|
+
</option>
|
|
224
|
+
<option value="eleven_turbo_v2_5">
|
|
225
|
+
Turbo v2.5 (fast, good quality)
|
|
226
|
+
</option>
|
|
227
|
+
<option value="eleven_multilingual_v2">
|
|
228
|
+
Multilingual v2 (highest quality)
|
|
229
|
+
</option>
|
|
230
|
+
</select>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
{/* Divider */}
|
|
234
|
+
<div className="border-t border-zinc-100 pt-4">
|
|
235
|
+
<h3 className="text-[11px] font-semibold uppercase tracking-widest text-teal-600 mb-2">
|
|
236
|
+
Public API
|
|
237
|
+
</h3>
|
|
238
|
+
<p className="text-xs text-zinc-500 leading-relaxed">
|
|
239
|
+
Heartbeads exposes a read-only REST API for AI agents, CI/CD
|
|
240
|
+
bots, and integrations.{" "}
|
|
241
|
+
{isGateProtected
|
|
242
|
+
? "API requests require the dashboard password as a Bearer token. "
|
|
243
|
+
: "No authentication required. "}
|
|
244
|
+
<a
|
|
245
|
+
href="/api/docs"
|
|
246
|
+
target="_blank"
|
|
247
|
+
rel="noopener noreferrer"
|
|
248
|
+
className="underline underline-offset-2 text-teal-500 hover:text-teal-600"
|
|
249
|
+
>
|
|
250
|
+
View API documentation
|
|
251
|
+
</a>
|
|
252
|
+
</p>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{/* Dashboard Access — only shown when password-protected */}
|
|
256
|
+
{isGateProtected && (
|
|
257
|
+
<div className="border-t border-zinc-100 pt-4">
|
|
258
|
+
<h3 className="text-[11px] font-semibold uppercase tracking-widest text-teal-600 mb-2">
|
|
259
|
+
Dashboard Access
|
|
260
|
+
</h3>
|
|
261
|
+
<p className="text-xs text-zinc-500 leading-relaxed mb-3">
|
|
262
|
+
This instance is password-protected. The{" "}
|
|
263
|
+
<span className="font-medium text-zinc-600">Sign In</span>{" "}
|
|
264
|
+
button in the navbar is for{" "}
|
|
265
|
+
<span className="font-medium text-zinc-600">
|
|
266
|
+
ATProto/Bluesky
|
|
267
|
+
</span>{" "}
|
|
268
|
+
— used for posting comments, likes, and claiming
|
|
269
|
+
tasks. The dashboard password controls who can view the
|
|
270
|
+
graph and access the API.
|
|
271
|
+
</p>
|
|
272
|
+
<button
|
|
273
|
+
onClick={handleLockDashboard}
|
|
274
|
+
type="button"
|
|
275
|
+
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg border border-zinc-200 text-xs font-medium text-zinc-600 hover:bg-zinc-50 hover:border-zinc-300 transition-colors"
|
|
276
|
+
>
|
|
277
|
+
<svg
|
|
278
|
+
className="w-3.5 h-3.5"
|
|
279
|
+
fill="none"
|
|
280
|
+
viewBox="0 0 24 24"
|
|
281
|
+
strokeWidth={1.5}
|
|
282
|
+
stroke="currentColor"
|
|
283
|
+
>
|
|
284
|
+
<path
|
|
285
|
+
strokeLinecap="round"
|
|
286
|
+
strokeLinejoin="round"
|
|
287
|
+
d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z"
|
|
288
|
+
/>
|
|
289
|
+
</svg>
|
|
290
|
+
Lock dashboard
|
|
291
|
+
</button>
|
|
292
|
+
</div>
|
|
293
|
+
)}
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
{/* Footer */}
|
|
297
|
+
<div className="flex items-center justify-end gap-2 px-5 py-3.5 border-t border-zinc-100">
|
|
298
|
+
<button
|
|
299
|
+
onClick={onClose}
|
|
300
|
+
className="px-4 py-2 text-sm font-medium text-zinc-500 hover:text-zinc-700 rounded-lg hover:bg-zinc-50 transition-colors"
|
|
301
|
+
>
|
|
302
|
+
Cancel
|
|
303
|
+
</button>
|
|
304
|
+
<button
|
|
305
|
+
onClick={handleSave}
|
|
306
|
+
className="px-4 py-2 text-sm font-medium text-white rounded-lg bg-emerald-500 hover:bg-emerald-600 transition-colors"
|
|
307
|
+
>
|
|
308
|
+
Save
|
|
309
|
+
</button>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
</div>,
|
|
313
|
+
document.body
|
|
314
|
+
);
|
|
315
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { STATUS_COLORS, STATUS_LABELS, PREFIX_COLORS, PREFIX_LABELS } from "@/lib/types";
|
|
4
|
+
|
|
5
|
+
interface StatusLegendProps {
|
|
6
|
+
prefixes: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function StatusLegend({ prefixes }: StatusLegendProps) {
|
|
10
|
+
const statuses = ["open", "in_progress", "blocked", "closed"];
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="space-y-4">
|
|
14
|
+
{/* Status legend */}
|
|
15
|
+
<div>
|
|
16
|
+
<h4 className="text-[10px] font-semibold text-zinc-400 uppercase tracking-wider mb-2">
|
|
17
|
+
Status
|
|
18
|
+
</h4>
|
|
19
|
+
<div className="flex flex-wrap gap-x-4 gap-y-1.5">
|
|
20
|
+
{statuses.map((status) => (
|
|
21
|
+
<div key={status} className="flex items-center gap-1.5">
|
|
22
|
+
<span
|
|
23
|
+
className="w-2.5 h-2.5 rounded-full"
|
|
24
|
+
style={{ backgroundColor: STATUS_COLORS[status] }}
|
|
25
|
+
/>
|
|
26
|
+
<span className="text-xs text-zinc-500">
|
|
27
|
+
{STATUS_LABELS[status]}
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
))}
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{/* Project prefix legend */}
|
|
35
|
+
{prefixes.length > 1 && (
|
|
36
|
+
<div>
|
|
37
|
+
<h4 className="text-[10px] font-semibold text-zinc-400 uppercase tracking-wider mb-2">
|
|
38
|
+
Projects (outer ring)
|
|
39
|
+
</h4>
|
|
40
|
+
<div className="flex flex-wrap gap-x-4 gap-y-1.5">
|
|
41
|
+
{prefixes.map((prefix) => (
|
|
42
|
+
<div key={prefix} className="flex items-center gap-1.5">
|
|
43
|
+
<span
|
|
44
|
+
className="w-2.5 h-2.5 rounded-full"
|
|
45
|
+
style={{
|
|
46
|
+
backgroundColor: "transparent",
|
|
47
|
+
boxShadow: `0 0 0 2px ${PREFIX_COLORS[prefix] || "#a1a1aa"}`,
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
<span className="text-xs text-zinc-500">
|
|
51
|
+
{PREFIX_LABELS[prefix] || prefix}
|
|
52
|
+
</span>
|
|
53
|
+
</div>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
{/* Interaction hints */}
|
|
60
|
+
<div>
|
|
61
|
+
<h4 className="text-[10px] font-semibold text-zinc-400 uppercase tracking-wider mb-2">
|
|
62
|
+
Interactions
|
|
63
|
+
</h4>
|
|
64
|
+
<div className="space-y-1 text-xs text-zinc-400">
|
|
65
|
+
<div className="flex items-center gap-2">
|
|
66
|
+
<kbd className="px-1.5 py-0.5 bg-zinc-100 rounded text-[10px] font-mono border border-zinc-200">
|
|
67
|
+
Click
|
|
68
|
+
</kbd>
|
|
69
|
+
<span>Select node</span>
|
|
70
|
+
</div>
|
|
71
|
+
<div className="flex items-center gap-2">
|
|
72
|
+
<kbd className="px-1.5 py-0.5 bg-zinc-100 rounded text-[10px] font-mono border border-zinc-200">
|
|
73
|
+
Hover
|
|
74
|
+
</kbd>
|
|
75
|
+
<span>Highlight connections</span>
|
|
76
|
+
</div>
|
|
77
|
+
<div className="flex items-center gap-2">
|
|
78
|
+
<kbd className="px-1.5 py-0.5 bg-zinc-100 rounded text-[10px] font-mono border border-zinc-200">
|
|
79
|
+
Scroll
|
|
80
|
+
</kbd>
|
|
81
|
+
<span>Zoom in/out</span>
|
|
82
|
+
</div>
|
|
83
|
+
<div className="flex items-center gap-2">
|
|
84
|
+
<kbd className="px-1.5 py-0.5 bg-zinc-100 rounded text-[10px] font-mono border border-zinc-200">
|
|
85
|
+
Drag
|
|
86
|
+
</kbd>
|
|
87
|
+
<span>Pan / Move nodes</span>
|
|
88
|
+
</div>
|
|
89
|
+
<div className="flex items-center gap-2">
|
|
90
|
+
<kbd className="px-1.5 py-0.5 bg-zinc-100 rounded text-[10px] font-mono border border-zinc-200">
|
|
91
|
+
Pinch
|
|
92
|
+
</kbd>
|
|
93
|
+
<span>Zoom (touch)</span>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useCallback } from "react";
|
|
4
|
+
|
|
5
|
+
interface TimelineBarProps {
|
|
6
|
+
totalSteps: number;
|
|
7
|
+
currentStep: number;
|
|
8
|
+
currentTime: number; // unix ms of current event (for date display)
|
|
9
|
+
isPlaying: boolean;
|
|
10
|
+
speed: number;
|
|
11
|
+
onStepChange: (step: number) => void;
|
|
12
|
+
onPlayPause: () => void;
|
|
13
|
+
onSpeedChange: (speed: number) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function formatTimelineDate(ms: number): string {
|
|
17
|
+
const d = new Date(ms);
|
|
18
|
+
return d.toLocaleDateString("en-US", {
|
|
19
|
+
month: "short",
|
|
20
|
+
day: "numeric",
|
|
21
|
+
year: "numeric",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function TimelineBar({
|
|
26
|
+
totalSteps,
|
|
27
|
+
currentStep,
|
|
28
|
+
currentTime,
|
|
29
|
+
isPlaying,
|
|
30
|
+
speed,
|
|
31
|
+
onStepChange,
|
|
32
|
+
onPlayPause,
|
|
33
|
+
onSpeedChange,
|
|
34
|
+
}: TimelineBarProps) {
|
|
35
|
+
const handleSliderChange = useCallback(
|
|
36
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
37
|
+
onStepChange(Number(e.target.value));
|
|
38
|
+
},
|
|
39
|
+
[onStepChange]
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const handleSpeedCycle = useCallback(() => {
|
|
43
|
+
const next = speed === 1 ? 2 : speed === 2 ? 4 : 1;
|
|
44
|
+
onSpeedChange(next);
|
|
45
|
+
}, [speed, onSpeedChange]);
|
|
46
|
+
|
|
47
|
+
const hasRange = totalSteps > 1;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="bg-white/90 backdrop-blur-sm rounded-lg border border-zinc-200 shadow-sm px-3 py-2 min-w-[280px] max-w-[480px]">
|
|
51
|
+
<div className="flex items-center gap-2">
|
|
52
|
+
{/* Play/Pause button */}
|
|
53
|
+
<button
|
|
54
|
+
onClick={onPlayPause}
|
|
55
|
+
disabled={!hasRange}
|
|
56
|
+
className={`shrink-0 w-6 h-6 flex items-center justify-center rounded transition-colors ${
|
|
57
|
+
isPlaying
|
|
58
|
+
? "text-emerald-500 hover:text-emerald-600"
|
|
59
|
+
: "text-zinc-500 hover:text-zinc-700"
|
|
60
|
+
} disabled:opacity-40 disabled:cursor-not-allowed`}
|
|
61
|
+
>
|
|
62
|
+
{isPlaying ? (
|
|
63
|
+
<svg
|
|
64
|
+
className="w-4 h-4"
|
|
65
|
+
viewBox="0 0 16 16"
|
|
66
|
+
fill="currentColor"
|
|
67
|
+
>
|
|
68
|
+
<rect x="3" y="2" width="3.5" height="12" rx="1" />
|
|
69
|
+
<rect x="9.5" y="2" width="3.5" height="12" rx="1" />
|
|
70
|
+
</svg>
|
|
71
|
+
) : (
|
|
72
|
+
<svg
|
|
73
|
+
className="w-4 h-4"
|
|
74
|
+
viewBox="0 0 16 16"
|
|
75
|
+
fill="currentColor"
|
|
76
|
+
>
|
|
77
|
+
<path d="M4 2l10 6-10 6V2z" />
|
|
78
|
+
</svg>
|
|
79
|
+
)}
|
|
80
|
+
</button>
|
|
81
|
+
|
|
82
|
+
{/* Scrubber slider */}
|
|
83
|
+
<input
|
|
84
|
+
type="range"
|
|
85
|
+
min={0}
|
|
86
|
+
max={Math.max(totalSteps - 1, 0)}
|
|
87
|
+
value={currentStep}
|
|
88
|
+
onChange={handleSliderChange}
|
|
89
|
+
disabled={!hasRange}
|
|
90
|
+
className="timeline-slider flex-1 h-4 disabled:opacity-40"
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
{/* Step counter + date */}
|
|
94
|
+
<span className="shrink-0 text-xs text-zinc-500 font-medium text-right whitespace-nowrap">
|
|
95
|
+
<span className="text-zinc-400">
|
|
96
|
+
{currentStep + 1}/{totalSteps}
|
|
97
|
+
</span>
|
|
98
|
+
{" "}
|
|
99
|
+
{formatTimelineDate(currentTime)}
|
|
100
|
+
</span>
|
|
101
|
+
|
|
102
|
+
{/* Speed toggle */}
|
|
103
|
+
<button
|
|
104
|
+
onClick={handleSpeedCycle}
|
|
105
|
+
className={`shrink-0 px-1.5 py-0.5 text-[10px] font-semibold rounded transition-colors ${
|
|
106
|
+
speed > 1
|
|
107
|
+
? "bg-emerald-500 text-white"
|
|
108
|
+
: "text-zinc-500 border border-zinc-200 hover:text-zinc-700"
|
|
109
|
+
}`}
|
|
110
|
+
>
|
|
111
|
+
{speed}x
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|