@recallkit/web 0.1.1
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-env.d.ts +6 -0
- package/next.config.ts +13 -0
- package/package.json +40 -0
- package/public/logo.png +0 -0
- package/public/textures/bg-scene.png +0 -0
- package/src/app/api/_lib/guards.ts +35 -0
- package/src/app/api/_lib/limits.ts +6 -0
- package/src/app/api/_lib/responses.ts +9 -0
- package/src/app/api/commit/complete/route.ts +112 -0
- package/src/app/api/commit/preview/route.ts +71 -0
- package/src/app/api/commit/route.ts +16 -0
- package/src/app/api/memory-cache/route.ts +50 -0
- package/src/app/api/pending/[id]/delete/route.ts +21 -0
- package/src/app/api/pending/[id]/route.ts +47 -0
- package/src/app/api/pending/route.ts +41 -0
- package/src/app/api/security.ts +25 -0
- package/src/app/api/status/route.ts +35 -0
- package/src/app/dashboard/page.tsx +57 -0
- package/src/app/drafts/page.tsx +5 -0
- package/src/app/globals.css +10 -0
- package/src/app/icon.png +0 -0
- package/src/app/layout.tsx +43 -0
- package/src/app/page.tsx +132 -0
- package/src/app/settings/page.tsx +76 -0
- package/src/components/ArrowRightIcon.tsx +25 -0
- package/src/components/CommitPreview.tsx +156 -0
- package/src/components/CopyValue.tsx +49 -0
- package/src/components/MemoryInbox.tsx +74 -0
- package/src/components/RetrievedMemories.tsx +36 -0
- package/src/components/TopNav.tsx +39 -0
- package/src/components/WalletConnectButton.tsx +68 -0
- package/src/components/inbox/EmptyInbox.tsx +20 -0
- package/src/components/inbox/InboxStats.tsx +41 -0
- package/src/components/inbox/MemoryCandidateList.tsx +90 -0
- package/src/components/inbox/MemoryCandidateRow.tsx +195 -0
- package/src/components/memory-cache/CachedMemoryList.tsx +47 -0
- package/src/components/memory-cache/EmptyCache.tsx +13 -0
- package/src/hooks/useCommitFlow.ts +55 -0
- package/src/hooks/useMemoryCache.ts +44 -0
- package/src/hooks/usePendingMemories.ts +137 -0
- package/src/hooks/useWallet.ts +69 -0
- package/src/lib/api.ts +71 -0
- package/src/lib/wallet.ts +88 -0
- package/src/services/commitMemories.ts +153 -0
- package/src/services/signerApi.ts +67 -0
- package/src/services/types.ts +22 -0
- package/src/stores/appStore.ts +18 -0
- package/src/stores/createStore.ts +41 -0
- package/src/stores/slices/memoryCacheSlice.ts +29 -0
- package/src/stores/slices/pendingMemorySlice.ts +21 -0
- package/src/stores/slices/walletSlice.ts +24 -0
- package/src/styles/base.css +61 -0
- package/src/styles/buttons.css +53 -0
- package/src/styles/data-display.css +485 -0
- package/src/styles/forms.css +86 -0
- package/src/styles/landing.css +75 -0
- package/src/styles/layout.css +111 -0
- package/src/styles/navigation.css +121 -0
- package/src/styles/overlays.css +65 -0
- package/src/styles/tokens.css +26 -0
- package/src/styles/utilities.css +358 -0
- package/src/utils/errors.ts +5 -0
- package/src/utils/format.ts +37 -0
- package/tsconfig.json +44 -0
package/src/app/page.tsx
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { DEFAULT_APPROVAL_APP_URL, DEFAULT_DAEMON_URL } from "@recallkit/core/constants";
|
|
3
|
+
import { CopyValue } from "@/components/CopyValue";
|
|
4
|
+
import { ArrowRightIcon } from "@/components/ArrowRightIcon";
|
|
5
|
+
|
|
6
|
+
export const dynamic = "force-dynamic";
|
|
7
|
+
|
|
8
|
+
const installCommand = "npx skills add recallkit/core";
|
|
9
|
+
|
|
10
|
+
const runtimeApiUrl = process.env.RECALLKIT_DAEMON_URL ?? DEFAULT_DAEMON_URL;
|
|
11
|
+
const approvalAppUrl = process.env.RECALLKIT_LOCAL_URL ?? DEFAULT_APPROVAL_APP_URL;
|
|
12
|
+
|
|
13
|
+
export default function LandingPage() {
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<section className="page page--hero">
|
|
17
|
+
<h1 className="h1 h1--hero" style={{ margin: "24px 0 20px" }}>
|
|
18
|
+
Wallet-bound memory for every coding agent <span className="accent-text">you</span> use.
|
|
19
|
+
</h1>
|
|
20
|
+
<p className="lede lede--hero" style={{ marginBottom: 36 }}>
|
|
21
|
+
RecallKit turns your project context, preferences, and decisions into ownable encrypted memory on Arkiv,
|
|
22
|
+
reviewed by you, signed by your wallet, and retrieved privately on your machine across Codex, Claude Code,
|
|
23
|
+
and future agents.
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<div className="v-row" style={{ gap: 12, flexWrap: "wrap" }}>
|
|
27
|
+
<Link href="/drafts" className="btn btn--primary">
|
|
28
|
+
Review drafts
|
|
29
|
+
<ArrowRightIcon />
|
|
30
|
+
</Link>
|
|
31
|
+
<Link href="#how" className="btn btn--ghost">
|
|
32
|
+
How it works
|
|
33
|
+
</Link>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div className="terminal-cmd" style={{ marginTop: 28 }}>
|
|
37
|
+
<div className="terminal-cmd__main">
|
|
38
|
+
<span className="terminal-cmd__prompt">$</span>
|
|
39
|
+
<code>{installCommand}</code>
|
|
40
|
+
</div>
|
|
41
|
+
<CopyValue
|
|
42
|
+
value={installCommand}
|
|
43
|
+
className="copy-value--terminal"
|
|
44
|
+
iconOnly
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
</section>
|
|
48
|
+
|
|
49
|
+
<section className="page" style={{ borderTop: "1px solid var(--border)" }}>
|
|
50
|
+
<div className="page-header">
|
|
51
|
+
<div className="page-header__title">
|
|
52
|
+
<div className="eyebrow">
|
|
53
|
+
/ why it matters
|
|
54
|
+
</div>
|
|
55
|
+
<h2 className="h2">Memory without agent custody.</h2>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div className="v-stack" style={{ gap: 0 }}>
|
|
60
|
+
<div className="settings-row" style={{ borderTop: "1px solid var(--border-strong)" }}>
|
|
61
|
+
<div>
|
|
62
|
+
<div className="settings-row__label">Captured in the flow</div>
|
|
63
|
+
<div className="settings-row__hint">
|
|
64
|
+
The agent quietly proposes useful preferences, project decisions, and context while you keep working.
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<div className="settings-row__value">drafts</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="settings-row">
|
|
70
|
+
<div>
|
|
71
|
+
<div className="settings-row__label">Approved by the user</div>
|
|
72
|
+
<div className="settings-row__hint">
|
|
73
|
+
Suggested memories stay local until you choose what should become durable.
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div className="settings-row__value">review</div>
|
|
77
|
+
</div>
|
|
78
|
+
<div className="settings-row" style={{ borderBottom: "1px solid var(--border-strong)" }}>
|
|
79
|
+
<div>
|
|
80
|
+
<div className="settings-row__label">Owned by the wallet</div>
|
|
81
|
+
<div className="settings-row__hint">
|
|
82
|
+
The final encrypted save is signed by your connected wallet, so memory follows you across tools.
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
<div className="settings-row__value">portable</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</section>
|
|
89
|
+
|
|
90
|
+
<section id="how" className="page" style={{ borderTop: "1px solid var(--border)" }}>
|
|
91
|
+
<div className="page-header">
|
|
92
|
+
<div className="page-header__title">
|
|
93
|
+
<div className="eyebrow">
|
|
94
|
+
/ how it works
|
|
95
|
+
</div>
|
|
96
|
+
<h2 className="h2">Three pieces, one clear boundary.</h2>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div className="v-stack" style={{ gap: 0 }}>
|
|
101
|
+
<div className="settings-row" style={{ borderTop: "1px solid var(--border-strong)" }}>
|
|
102
|
+
<div>
|
|
103
|
+
<div className="settings-row__label">Agent skill</div>
|
|
104
|
+
<div className="settings-row__hint">
|
|
105
|
+
Installed in your coding agent so that it knows when to capture drafts or retrieve context.
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<div className="settings-row__value">packages/skill</div>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="settings-row">
|
|
111
|
+
<div>
|
|
112
|
+
<div className="settings-row__label">Memory runtime API</div>
|
|
113
|
+
<div className="settings-row__hint">
|
|
114
|
+
Runs in the background so memory capture and retrieval feel like part of the conversation.
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<div className="settings-row__value">{runtimeApiUrl}</div>
|
|
118
|
+
</div>
|
|
119
|
+
<div className="settings-row" style={{ borderBottom: "1px solid var(--border-strong)" }}>
|
|
120
|
+
<div>
|
|
121
|
+
<div className="settings-row__label">Approval app</div>
|
|
122
|
+
<div className="settings-row__hint">
|
|
123
|
+
The browser UI where you review memories, encrypt them, and confirm the wallet-signed save.
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
<div className="settings-row__value">{approvalAppUrl}</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</section>
|
|
130
|
+
</>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readConfig } from "@recallkit/core";
|
|
2
|
+
import { CopyValue } from "@/components/CopyValue";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const revalidate = 0;
|
|
6
|
+
|
|
7
|
+
export default async function SettingsPage() {
|
|
8
|
+
const config = await readConfig();
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<section className="page page--narrow">
|
|
12
|
+
<div className="page-header">
|
|
13
|
+
<div className="page-header__title">
|
|
14
|
+
<h1 className="h1">Settings</h1>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div className="settings-group">
|
|
19
|
+
<div className="settings-group__head">// approval app</div>
|
|
20
|
+
<div className="settings-row">
|
|
21
|
+
<div>
|
|
22
|
+
<div className="settings-row__label">Approval app URL</div>
|
|
23
|
+
<div className="settings-row__hint">
|
|
24
|
+
Where the browser approval app is served. Skill scripts open this when you review drafts.
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<CopyValue value={config.approvalAppUrl} />
|
|
28
|
+
</div>
|
|
29
|
+
<div className="settings-row">
|
|
30
|
+
<div>
|
|
31
|
+
<div className="settings-row__label">Arkiv chain</div>
|
|
32
|
+
<div className="settings-row__hint">Target network for Arkiv writes.</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div className="settings-row__value">{config.arkivChain}</div>
|
|
35
|
+
</div>
|
|
36
|
+
<div className="settings-row">
|
|
37
|
+
<div>
|
|
38
|
+
<div className="settings-row__label">Owner wallet</div>
|
|
39
|
+
<div className="settings-row__hint">Wallet authorized to sign commits.</div>
|
|
40
|
+
</div>
|
|
41
|
+
{config.ownerWallet ? (
|
|
42
|
+
<CopyValue value={config.ownerWallet} display={shortAddress(config.ownerWallet)} />
|
|
43
|
+
) : (
|
|
44
|
+
<div className="settings-row__value">not connected</div>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div className="settings-group">
|
|
50
|
+
<div className="settings-group__head">// privacy</div>
|
|
51
|
+
<div className="settings-row">
|
|
52
|
+
<div>
|
|
53
|
+
<div className="settings-row__label">Encrypt payloads</div>
|
|
54
|
+
<div className="settings-row__hint">
|
|
55
|
+
Memory text, summaries, keywords, and embeddings are encrypted before Arkiv writes.
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
<span className="status-pill">always on</span>
|
|
59
|
+
</div>
|
|
60
|
+
<div className="settings-row">
|
|
61
|
+
<div>
|
|
62
|
+
<div className="settings-row__label">Public Arkiv attributes</div>
|
|
63
|
+
<div className="settings-row__hint">
|
|
64
|
+
project · entity type · owner · schema version · scope hash · timestamps
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<div className="settings-row__value">minimal</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</section>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function shortAddress(address: string): string {
|
|
75
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
76
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SVGProps } from "react";
|
|
2
|
+
|
|
3
|
+
export function ArrowRightIcon({
|
|
4
|
+
size = 16,
|
|
5
|
+
strokeWidth = 1.75,
|
|
6
|
+
...props
|
|
7
|
+
}: { size?: number; strokeWidth?: number } & SVGProps<SVGSVGElement>) {
|
|
8
|
+
return (
|
|
9
|
+
<svg
|
|
10
|
+
width={size}
|
|
11
|
+
height={size}
|
|
12
|
+
viewBox="0 0 24 24"
|
|
13
|
+
fill="none"
|
|
14
|
+
stroke="currentColor"
|
|
15
|
+
strokeWidth={strokeWidth}
|
|
16
|
+
strokeLinecap="round"
|
|
17
|
+
strokeLinejoin="round"
|
|
18
|
+
aria-hidden="true"
|
|
19
|
+
{...props}
|
|
20
|
+
>
|
|
21
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
22
|
+
<polyline points="13 6 19 12 13 18" />
|
|
23
|
+
</svg>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { X } from "lucide-react";
|
|
4
|
+
import { useCommitFlow } from "@/hooks/useCommitFlow";
|
|
5
|
+
import { shortAddress } from "@/utils/format";
|
|
6
|
+
import { CopyValue } from "./CopyValue";
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
selectedIds: string[];
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
onCommitted: () => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function CommitPreview({ selectedIds, onClose, onCommitted }: Props) {
|
|
15
|
+
const flow = useCommitFlow(selectedIds);
|
|
16
|
+
const title = previewTitle({
|
|
17
|
+
saved: Boolean(flow.entityKeys),
|
|
18
|
+
memoryCount: flow.preview?.memories.length,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
async function handleCommit() {
|
|
22
|
+
const committed = await flow.commit();
|
|
23
|
+
if (committed) {
|
|
24
|
+
onCommitted();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="modal-v2-backdrop" onClick={onClose}>
|
|
30
|
+
<div className="modal-v2" onClick={(event) => event.stopPropagation()}>
|
|
31
|
+
<div className="modal-v2__head">
|
|
32
|
+
<div>
|
|
33
|
+
<h2 className="modal-v2__title">{title}</h2>
|
|
34
|
+
</div>
|
|
35
|
+
<button className="modal-v2__close" aria-label="Close" onClick={onClose}>
|
|
36
|
+
<X size={16} />
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div className="modal-v2__body">
|
|
41
|
+
{!flow.preview && !flow.entityKeys ? (
|
|
42
|
+
<p className="body-dim" style={{ marginBottom: 16 }}>
|
|
43
|
+
Review exactly what will be saved. Nothing is signed until you confirm with your wallet.
|
|
44
|
+
</p>
|
|
45
|
+
) : null}
|
|
46
|
+
|
|
47
|
+
{flow.preview && !flow.entityKeys ? (
|
|
48
|
+
<>
|
|
49
|
+
<div className="summary-row">
|
|
50
|
+
<span className="summary-row__label">Signer wallet</span>
|
|
51
|
+
<span className="summary-row__value">{shortAddress(flow.preview.signerWallet)}</span>
|
|
52
|
+
</div>
|
|
53
|
+
<div className="summary-row">
|
|
54
|
+
<span className="summary-row__label">Network</span>
|
|
55
|
+
<span className="summary-row__value">{flow.preview.network}</span>
|
|
56
|
+
</div>
|
|
57
|
+
<div className="summary-row">
|
|
58
|
+
<span className="summary-row__label">Ready to save</span>
|
|
59
|
+
<span className="summary-row__value">{flow.preview.memories.length}</span>
|
|
60
|
+
</div>
|
|
61
|
+
{flow.preview.sessionDraftCount > 0 ? (
|
|
62
|
+
<div className="summary-row">
|
|
63
|
+
<span className="summary-row__label">Session drafts skipped</span>
|
|
64
|
+
<span className="summary-row__value">{flow.preview.sessionDraftCount}</span>
|
|
65
|
+
</div>
|
|
66
|
+
) : null}
|
|
67
|
+
<div className="summary-row">
|
|
68
|
+
<span className="summary-row__label">Estimated writes</span>
|
|
69
|
+
<span className="summary-row__value">{flow.preview.estimatedWrites}</span>
|
|
70
|
+
</div>
|
|
71
|
+
<div className="summary-row">
|
|
72
|
+
<span className="summary-row__label">Entities</span>
|
|
73
|
+
<span className="summary-row__value">{flow.preview.entities.join(", ")}</span>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="summary-row">
|
|
76
|
+
<span className="summary-row__label">Encryption</span>
|
|
77
|
+
<span className="summary-row__value summary-row__value--accent">
|
|
78
|
+
enabled · passphrase protected
|
|
79
|
+
</span>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{flow.preview.memories.length === 0 ? (
|
|
83
|
+
<div className="error-v2">
|
|
84
|
+
Session memories stay local. Change at least one selected draft to Global or Current Project
|
|
85
|
+
before saving.
|
|
86
|
+
</div>
|
|
87
|
+
) : null}
|
|
88
|
+
|
|
89
|
+
<label className="field-v2">
|
|
90
|
+
<span className="field-v2__label">Encryption passphrase</span>
|
|
91
|
+
<input
|
|
92
|
+
className="field-v2__input"
|
|
93
|
+
type="password"
|
|
94
|
+
value={flow.passphrase}
|
|
95
|
+
onChange={(event) => flow.setPassphrase(event.target.value)}
|
|
96
|
+
placeholder="Minimum 8 characters" autoComplete="new-password"
|
|
97
|
+
/>
|
|
98
|
+
</label>
|
|
99
|
+
</>
|
|
100
|
+
) : null}
|
|
101
|
+
|
|
102
|
+
{flow.entityKeys ? (
|
|
103
|
+
<>
|
|
104
|
+
<p className="body-dim" style={{ marginBottom: 16 }}>
|
|
105
|
+
Your wallet-signed save is complete. Entity keys:
|
|
106
|
+
</p>
|
|
107
|
+
{Object.entries(flow.entityKeys).map(([type, key]) => (
|
|
108
|
+
<div key={type} className="summary-row">
|
|
109
|
+
<span className="summary-row__label">{type}</span>
|
|
110
|
+
<CopyValue value={key} />
|
|
111
|
+
</div>
|
|
112
|
+
))}
|
|
113
|
+
</>
|
|
114
|
+
) : null}
|
|
115
|
+
|
|
116
|
+
{flow.error ? <div className="error-v2">{flow.error}</div> : null}
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div className="modal-v2__foot">
|
|
120
|
+
{!flow.entityKeys ? (
|
|
121
|
+
<button className="btn btn--ghost" onClick={onClose}>
|
|
122
|
+
Cancel
|
|
123
|
+
</button>
|
|
124
|
+
) : null}
|
|
125
|
+
{!flow.preview && !flow.entityKeys ? (
|
|
126
|
+
<button className="btn btn--primary" onClick={() => void flow.loadPreview()} disabled={flow.busy}>
|
|
127
|
+
{flow.busy ? <span className="spinner" /> : null}
|
|
128
|
+
{flow.busy ? "Preparing" : "Review details"}
|
|
129
|
+
</button>
|
|
130
|
+
) : null}
|
|
131
|
+
{flow.preview && !flow.entityKeys ? (
|
|
132
|
+
<button
|
|
133
|
+
className="btn btn--primary"
|
|
134
|
+
onClick={() => void handleCommit()}
|
|
135
|
+
disabled={!flow.canCommit}
|
|
136
|
+
>
|
|
137
|
+
{flow.busy ? <span className="spinner" /> : null}
|
|
138
|
+
{flow.busy ? "Signing" : "Confirm with wallet →"}
|
|
139
|
+
</button>
|
|
140
|
+
) : null}
|
|
141
|
+
{flow.entityKeys ? (
|
|
142
|
+
<button className="btn btn--primary" onClick={onClose}>
|
|
143
|
+
Done
|
|
144
|
+
</button>
|
|
145
|
+
) : null}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function previewTitle({ saved, memoryCount }: { saved: boolean; memoryCount: number | undefined }) {
|
|
153
|
+
if (saved) return "Saved to Arkiv";
|
|
154
|
+
if (memoryCount === undefined) return "Save approved memories";
|
|
155
|
+
return `Save ${memoryCount} approved ${memoryCount === 1 ? "memory" : "memories"}`;
|
|
156
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Check, Copy } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
value: string;
|
|
8
|
+
display?: string;
|
|
9
|
+
copiedDisplay?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
iconOnly?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function CopyValue({ value, display, copiedDisplay, className, iconOnly = false }: Props) {
|
|
15
|
+
const [copied, setCopied] = useState(false);
|
|
16
|
+
const [copyFailed, setCopyFailed] = useState(false);
|
|
17
|
+
|
|
18
|
+
async function copy() {
|
|
19
|
+
try {
|
|
20
|
+
await navigator.clipboard.writeText(value);
|
|
21
|
+
setCopied(true);
|
|
22
|
+
setCopyFailed(false);
|
|
23
|
+
setTimeout(() => setCopied(false), 1500);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.warn("Clipboard copy failed", error);
|
|
26
|
+
setCopyFailed(true);
|
|
27
|
+
setTimeout(() => setCopyFailed(false), 1500);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
onClick={copy}
|
|
35
|
+
className={`copy-value${copied ? " copy-value--copied" : ""}${className ? " " + className : ""}`}
|
|
36
|
+
title={copyFailed ? "Copy failed" : copied ? "Copied" : "Copy to clipboard"}
|
|
37
|
+
aria-label={copyFailed ? "Copy failed" : copied ? "Copied to clipboard" : "Copy to clipboard"}
|
|
38
|
+
>
|
|
39
|
+
<span className="copy-value__icon" aria-hidden="true">
|
|
40
|
+
{copied ? <Check size={13} /> : <Copy size={13} />}
|
|
41
|
+
</span>
|
|
42
|
+
{iconOnly ? null : (
|
|
43
|
+
<span className="copy-value__text">
|
|
44
|
+
{copyFailed ? "Copy failed" : copied && copiedDisplay ? copiedDisplay : display ?? value}
|
|
45
|
+
</span>
|
|
46
|
+
)}
|
|
47
|
+
</button>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { usePendingMemories } from "@/hooks/usePendingMemories";
|
|
5
|
+
import { EmptyInbox } from "./inbox/EmptyInbox";
|
|
6
|
+
import { InboxStats } from "./inbox/InboxStats";
|
|
7
|
+
import { MemoryCandidateList } from "./inbox/MemoryCandidateList";
|
|
8
|
+
import { CommitPreview } from "./CommitPreview";
|
|
9
|
+
|
|
10
|
+
export function MemoryInbox() {
|
|
11
|
+
const pending = usePendingMemories();
|
|
12
|
+
const [commitOpen, setCommitOpen] = useState(false);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<section className="page">
|
|
16
|
+
<div className="page-header">
|
|
17
|
+
<div className="page-header__title">
|
|
18
|
+
<h1 className="h1">Review memories</h1>
|
|
19
|
+
<p className="lede">
|
|
20
|
+
Your agent captured these suggestions. Approve what should be saved, then sign once when you are ready.
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
<div className="page-header__actions">
|
|
24
|
+
<button
|
|
25
|
+
className="btn btn--ghost"
|
|
26
|
+
onClick={() => void pending.refresh({ force: true })}
|
|
27
|
+
disabled={pending.refreshing}
|
|
28
|
+
>
|
|
29
|
+
{pending.refreshing ? <span className="spinner" /> : null}
|
|
30
|
+
{pending.refreshing ? "Refreshing" : "Refresh"}
|
|
31
|
+
</button>
|
|
32
|
+
<button
|
|
33
|
+
className="btn btn--primary"
|
|
34
|
+
onClick={() => setCommitOpen(true)}
|
|
35
|
+
disabled={pending.committableSelectedCount === 0}
|
|
36
|
+
>
|
|
37
|
+
{pending.committableSelectedCount > 0
|
|
38
|
+
? `Save ${pending.committableSelectedCount} memories`
|
|
39
|
+
: "Save memories"}
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<InboxStats pending={pending.items} selectedCount={pending.selectedIds.length} />
|
|
45
|
+
|
|
46
|
+
{pending.items.length === 0 ? (
|
|
47
|
+
<EmptyInbox />
|
|
48
|
+
) : (
|
|
49
|
+
<MemoryCandidateList
|
|
50
|
+
candidates={pending.sortedItems}
|
|
51
|
+
selectedIds={pending.selectedSet}
|
|
52
|
+
onToggle={pending.toggle}
|
|
53
|
+
onChange={pending.patchCandidate}
|
|
54
|
+
onSave={(candidate) => void pending.save(candidate)}
|
|
55
|
+
onApprove={(candidate) => void pending.approve(candidate)}
|
|
56
|
+
onReject={(id) => void pending.reject(id)}
|
|
57
|
+
/>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
{pending.error ? <div className="error-v2">{pending.error}</div> : null}
|
|
61
|
+
|
|
62
|
+
{commitOpen ? (
|
|
63
|
+
<CommitPreview
|
|
64
|
+
selectedIds={pending.selectedIds}
|
|
65
|
+
onClose={() => setCommitOpen(false)}
|
|
66
|
+
onCommitted={() => {
|
|
67
|
+
pending.clearSelection();
|
|
68
|
+
void pending.refresh();
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
) : null}
|
|
72
|
+
</section>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemoryCache } from "@/hooks/useMemoryCache";
|
|
4
|
+
import { CachedMemoryList } from "./memory-cache/CachedMemoryList";
|
|
5
|
+
import { EmptyCache } from "./memory-cache/EmptyCache";
|
|
6
|
+
|
|
7
|
+
export function RetrievedMemories() {
|
|
8
|
+
const cache = useMemoryCache();
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<section className="v-stack" style={{ gap: 14 }}>
|
|
12
|
+
<div className="page-header" style={{ marginBottom: 0 }}>
|
|
13
|
+
<div className="page-header__title">
|
|
14
|
+
<div className="eyebrow">/ retrieval cache</div>
|
|
15
|
+
|
|
16
|
+
<h2 className="h2">Local decrypted cache</h2>
|
|
17
|
+
<p className="lede">Approved index and pack cache used by skill-based search.</p>
|
|
18
|
+
</div>
|
|
19
|
+
<div className="page-header__actions">
|
|
20
|
+
<button className="btn btn--ghost" onClick={cache.load} disabled={cache.loading}>
|
|
21
|
+
{cache.loading ? <span className="spinner" /> : null}
|
|
22
|
+
{cache.loading ? "Loading" : cache.loaded ? "Reload" : "Load cache"}
|
|
23
|
+
</button>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
{cache.memories.length === 0 ? (
|
|
28
|
+
<EmptyCache loaded={cache.loaded} />
|
|
29
|
+
) : (
|
|
30
|
+
<CachedMemoryList memories={cache.memories} />
|
|
31
|
+
)}
|
|
32
|
+
|
|
33
|
+
{cache.error ? <div className="error-v2">{cache.error}</div> : null}
|
|
34
|
+
</section>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import { WalletConnectButton } from "@/components/WalletConnectButton";
|
|
6
|
+
|
|
7
|
+
const links = [
|
|
8
|
+
{ href: "/dashboard", label: "Dashboard" },
|
|
9
|
+
{ href: "/drafts", label: "Drafts" },
|
|
10
|
+
{ href: "/settings", label: "Settings" },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export function TopNav() {
|
|
14
|
+
const pathname = usePathname();
|
|
15
|
+
const isActive = (href: string) => pathname === href || pathname.startsWith(href + "/");
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<header className="topnav">
|
|
19
|
+
<Link href="/" className="topnav__brand">
|
|
20
|
+
<img src="/logo.png" alt="" className="topnav__mark" />
|
|
21
|
+
<span>RecallKit</span>
|
|
22
|
+
</Link>
|
|
23
|
+
<nav className="topnav__links" aria-label="Primary navigation">
|
|
24
|
+
{links.map((link) => (
|
|
25
|
+
<Link
|
|
26
|
+
key={link.href}
|
|
27
|
+
href={link.href}
|
|
28
|
+
className={isActive(link.href) ? "is-active" : undefined}
|
|
29
|
+
>
|
|
30
|
+
{link.label}
|
|
31
|
+
</Link>
|
|
32
|
+
))}
|
|
33
|
+
</nav>
|
|
34
|
+
<div className="topnav__right">
|
|
35
|
+
<WalletConnectButton variant="nav" />
|
|
36
|
+
</div>
|
|
37
|
+
</header>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useWallet } from "@/hooks/useWallet";
|
|
4
|
+
import { shortAddress } from "@/utils/format";
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
variant?: "panel" | "nav";
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function WalletConnectButton({ variant = "panel" }: Props) {
|
|
11
|
+
const { status, balance, balanceLow, busy, error, connect } = useWallet();
|
|
12
|
+
|
|
13
|
+
if (variant === "nav") {
|
|
14
|
+
return (
|
|
15
|
+
<div className="wallet-nav">
|
|
16
|
+
<button className="wallet-nav__button" onClick={connect} disabled={busy}>
|
|
17
|
+
{busy ? (
|
|
18
|
+
<span className="spinner" />
|
|
19
|
+
) : (
|
|
20
|
+
<span
|
|
21
|
+
className={
|
|
22
|
+
status?.connected ? "wallet-nav__dot" : "wallet-nav__dot wallet-nav__dot--off"
|
|
23
|
+
}
|
|
24
|
+
/>
|
|
25
|
+
)}
|
|
26
|
+
<span>{busy ? "Connecting" : status?.ownerWallet ? shortAddress(status.ownerWallet) : "Connect"}</span>
|
|
27
|
+
</button>
|
|
28
|
+
{balance !== undefined ? (
|
|
29
|
+
<span className="wallet-nav__balance" style={{ color: balanceLow ? "var(--danger)" : undefined }}>
|
|
30
|
+
{Number(balance).toFixed(4)} GLM
|
|
31
|
+
</span>
|
|
32
|
+
) : null}
|
|
33
|
+
{error ? <span className="wallet-nav__error">{error}</span> : null}
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="v-stack" style={{ gap: 14 }}>
|
|
40
|
+
<div className="v-row v-row--between">
|
|
41
|
+
<div>
|
|
42
|
+
<div className="tile__label" style={{ marginBottom: 4 }}>Wallet</div>
|
|
43
|
+
<div className="tile__value" style={{ fontSize: 18 }}>
|
|
44
|
+
{status?.ownerWallet ? shortAddress(status.ownerWallet) : "Not connected"}
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
<button className="btn btn--primary" onClick={connect} disabled={busy}>
|
|
48
|
+
{busy ? <span className="spinner" /> : null}
|
|
49
|
+
{busy ? "Connecting" : status?.connected ? "Reconnect" : "Connect wallet"}
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div className="v-row v-row--between">
|
|
54
|
+
<span className={status?.connected ? "status-pill" : "status-pill status-pill--off"}>
|
|
55
|
+
{status?.network ?? "arkiv-braga pending"}
|
|
56
|
+
</span>
|
|
57
|
+
<span
|
|
58
|
+
className="body-dim"
|
|
59
|
+
style={{ color: balanceLow ? "var(--danger)" : "var(--text-dim)" }}
|
|
60
|
+
>
|
|
61
|
+
{balance !== undefined ? `${Number(balance).toFixed(4)} GLM` : "Balance not checked"}
|
|
62
|
+
</span>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{error ? <div className="error-v2">{error}</div> : null}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|