plotlink-ows 0.1.13
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/LICENSE +21 -0
- package/README.md +151 -0
- package/app/db.ts +8 -0
- package/app/lib/llm-client.ts +265 -0
- package/app/lib/paths.ts +11 -0
- package/app/lib/publish.ts +204 -0
- package/app/lib/writer-prompt.ts +44 -0
- package/app/node_modules/.prisma/local-client/client.d.ts +1 -0
- package/app/node_modules/.prisma/local-client/client.js +5 -0
- package/app/node_modules/.prisma/local-client/default.d.ts +1 -0
- package/app/node_modules/.prisma/local-client/default.js +5 -0
- package/app/node_modules/.prisma/local-client/edge.d.ts +1 -0
- package/app/node_modules/.prisma/local-client/edge.js +184 -0
- package/app/node_modules/.prisma/local-client/index-browser.js +173 -0
- package/app/node_modules/.prisma/local-client/index.d.ts +3304 -0
- package/app/node_modules/.prisma/local-client/index.js +207 -0
- package/app/node_modules/.prisma/local-client/libquery_engine-darwin-arm64.dylib.node +0 -0
- package/app/node_modules/.prisma/local-client/package.json +183 -0
- package/app/node_modules/.prisma/local-client/query_engine_bg.js +2 -0
- package/app/node_modules/.prisma/local-client/query_engine_bg.wasm +0 -0
- package/app/node_modules/.prisma/local-client/runtime/edge-esm.js +35 -0
- package/app/node_modules/.prisma/local-client/runtime/edge.js +35 -0
- package/app/node_modules/.prisma/local-client/runtime/index-browser.d.ts +370 -0
- package/app/node_modules/.prisma/local-client/runtime/index-browser.js +17 -0
- package/app/node_modules/.prisma/local-client/runtime/library.d.ts +3982 -0
- package/app/node_modules/.prisma/local-client/runtime/library.js +147 -0
- package/app/node_modules/.prisma/local-client/runtime/react-native.js +84 -0
- package/app/node_modules/.prisma/local-client/runtime/wasm-compiler-edge.js +85 -0
- package/app/node_modules/.prisma/local-client/runtime/wasm-engine-edge.js +38 -0
- package/app/node_modules/.prisma/local-client/schema.prisma +21 -0
- package/app/node_modules/.prisma/local-client/wasm-edge-light-loader.mjs +5 -0
- package/app/node_modules/.prisma/local-client/wasm-worker-loader.mjs +5 -0
- package/app/node_modules/.prisma/local-client/wasm.d.ts +1 -0
- package/app/node_modules/.prisma/local-client/wasm.js +191 -0
- package/app/prisma/schema.prisma +57 -0
- package/app/routes/auth.ts +173 -0
- package/app/routes/chat.ts +135 -0
- package/app/routes/config.ts +210 -0
- package/app/routes/dashboard.ts +186 -0
- package/app/routes/oauth.ts +150 -0
- package/app/routes/publish.ts +112 -0
- package/app/routes/wallet.ts +99 -0
- package/app/server.ts +154 -0
- package/app/vite.config.ts +19 -0
- package/app/web/App.tsx +102 -0
- package/app/web/components/Chat.tsx +272 -0
- package/app/web/components/Dashboard.tsx +222 -0
- package/app/web/components/LLMSetup.tsx +291 -0
- package/app/web/components/Layout.tsx +235 -0
- package/app/web/components/Login.tsx +62 -0
- package/app/web/components/Publish.tsx +245 -0
- package/app/web/components/Settings.tsx +175 -0
- package/app/web/components/Setup.tsx +84 -0
- package/app/web/components/WalletCard.tsx +117 -0
- package/app/web/dist/assets/index-C9kXlYO_.css +2 -0
- package/app/web/dist/assets/index-CJiiaLHs.js +9 -0
- package/app/web/dist/index.html +16 -0
- package/app/web/index.html +15 -0
- package/app/web/main.tsx +10 -0
- package/app/web/plotlink-logo.svg +5 -0
- package/app/web/styles.css +51 -0
- package/bin/plotlink-ows.js +394 -0
- package/lib/ows/index.ts +3 -0
- package/lib/ows/policy.ts +68 -0
- package/lib/ows/types.ts +14 -0
- package/lib/ows/wallet.ts +70 -0
- package/package.json +79 -0
- package/packages/cli/node_modules/commander/LICENSE +22 -0
- package/packages/cli/node_modules/commander/Readme.md +1149 -0
- package/packages/cli/node_modules/commander/esm.mjs +16 -0
- package/packages/cli/node_modules/commander/index.js +24 -0
- package/packages/cli/node_modules/commander/lib/argument.js +149 -0
- package/packages/cli/node_modules/commander/lib/command.js +2662 -0
- package/packages/cli/node_modules/commander/lib/error.js +39 -0
- package/packages/cli/node_modules/commander/lib/help.js +709 -0
- package/packages/cli/node_modules/commander/lib/option.js +367 -0
- package/packages/cli/node_modules/commander/lib/suggestSimilar.js +101 -0
- package/packages/cli/node_modules/commander/package-support.json +16 -0
- package/packages/cli/node_modules/commander/package.json +82 -0
- package/packages/cli/node_modules/commander/typings/esm.d.mts +3 -0
- package/packages/cli/node_modules/commander/typings/index.d.ts +1045 -0
- package/packages/cli/node_modules/resolve-from/index.d.ts +31 -0
- package/packages/cli/node_modules/resolve-from/index.js +47 -0
- package/packages/cli/node_modules/resolve-from/license +9 -0
- package/packages/cli/node_modules/resolve-from/package.json +36 -0
- package/packages/cli/node_modules/resolve-from/readme.md +72 -0
- package/packages/cli/node_modules/tsup/LICENSE +21 -0
- package/packages/cli/node_modules/tsup/README.md +75 -0
- package/packages/cli/node_modules/tsup/assets/cjs_shims.js +13 -0
- package/packages/cli/node_modules/tsup/assets/esm_shims.js +9 -0
- package/packages/cli/node_modules/tsup/assets/package.json +3 -0
- package/packages/cli/node_modules/tsup/dist/chunk-DI5BO6XE.js +153 -0
- package/packages/cli/node_modules/tsup/dist/chunk-JZ25TPTY.js +42 -0
- package/packages/cli/node_modules/tsup/dist/chunk-PEEXUWMS.js +6 -0
- package/packages/cli/node_modules/tsup/dist/chunk-TWFEYLU4.js +352 -0
- package/packages/cli/node_modules/tsup/dist/chunk-VGC3FXLU.js +203 -0
- package/packages/cli/node_modules/tsup/dist/cli-default.js +12 -0
- package/packages/cli/node_modules/tsup/dist/cli-main.js +8 -0
- package/packages/cli/node_modules/tsup/dist/cli-node.js +14 -0
- package/packages/cli/node_modules/tsup/dist/index.d.ts +511 -0
- package/packages/cli/node_modules/tsup/dist/index.js +1711 -0
- package/packages/cli/node_modules/tsup/dist/rollup.js +6949 -0
- package/packages/cli/node_modules/tsup/package.json +99 -0
- package/packages/cli/node_modules/tsup/schema.json +362 -0
- package/packages/cli/package.json +35 -0
- package/packages/cli/src/commands/agent-register.ts +77 -0
- package/packages/cli/src/commands/chain.ts +29 -0
- package/packages/cli/src/commands/claim.ts +70 -0
- package/packages/cli/src/commands/create.ts +34 -0
- package/packages/cli/src/commands/status.ts +201 -0
- package/packages/cli/src/config.ts +103 -0
- package/packages/cli/src/index.ts +21 -0
- package/packages/cli/src/sdk/abi.ts +222 -0
- package/packages/cli/src/sdk/client.ts +713 -0
- package/packages/cli/src/sdk/constants.ts +56 -0
- package/packages/cli/src/sdk/index.ts +46 -0
- package/packages/cli/src/sdk/ipfs.ts +88 -0
- package/packages/cli/src/sdk.ts +36 -0
- package/packages/cli/tsconfig.json +20 -0
- package/packages/cli/tsup.config.ts +14 -0
- package/public/.well-known/farcaster.json +38 -0
- package/public/basescan-icon.svg +4 -0
- package/public/embed-image.png +0 -0
- package/public/favicon.png +0 -0
- package/public/hunt-token.svg +11 -0
- package/public/icon-192.png +0 -0
- package/public/icon.png +0 -0
- package/public/manifest.json +26 -0
- package/public/mc-icon-light.svg +12 -0
- package/public/og-image.png +0 -0
- package/public/plotlink-logo-symbol.svg +5 -0
- package/public/plotlink-logo.svg +5 -0
- package/public/screenshot-1.png +0 -0
- package/public/screenshot-2.png +0 -0
- package/public/screenshot-3.png +0 -0
- package/public/splash.png +0 -0
- package/public/wide-banner.png +0 -0
- package/scripts/backfill-trade-prices.ts +97 -0
- package/scripts/backfill-usd-rates.ts +220 -0
- package/scripts/e2e-verify.ts +1100 -0
- package/scripts/ows-smoke-test.ts +37 -0
- package/scripts/score-users.mjs +203 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { LLMSetup } from "./LLMSetup";
|
|
3
|
+
import { WalletCard } from "./WalletCard";
|
|
4
|
+
import { Settings } from "./Settings";
|
|
5
|
+
import { Chat } from "./Chat";
|
|
6
|
+
import { Publish } from "./Publish";
|
|
7
|
+
import { Dashboard } from "./Dashboard";
|
|
8
|
+
|
|
9
|
+
const API_BASE = "http://localhost:7777";
|
|
10
|
+
|
|
11
|
+
type Page = "home" | "chat" | "publish" | "dashboard" | "llm-setup" | "wallet-setup" | "settings";
|
|
12
|
+
|
|
13
|
+
function WalletSetupPage({ token, onComplete }: { token: string; onComplete: () => void }) {
|
|
14
|
+
const [creating, setCreating] = useState(false);
|
|
15
|
+
const [error, setError] = useState<string | null>(null);
|
|
16
|
+
const [success, setSuccess] = useState(false);
|
|
17
|
+
|
|
18
|
+
const createWallet = async () => {
|
|
19
|
+
setCreating(true);
|
|
20
|
+
setError(null);
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(`${API_BASE}/api/wallet/create`, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
25
|
+
});
|
|
26
|
+
const data = await res.json();
|
|
27
|
+
if (!res.ok) throw new Error(data.error || "Wallet creation failed");
|
|
28
|
+
setSuccess(true);
|
|
29
|
+
} catch (err: unknown) {
|
|
30
|
+
setError(err instanceof Error ? err.message : "Wallet creation failed");
|
|
31
|
+
}
|
|
32
|
+
setCreating(false);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
useEffect(() => { createWallet(); }, []);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="mx-auto max-w-sm p-6 text-center">
|
|
39
|
+
<h2 className="text-accent mb-1 text-lg font-bold">Wallet Setup</h2>
|
|
40
|
+
<p className="text-muted mb-6 text-xs">creating your OWS wallet for autonomous transactions</p>
|
|
41
|
+
|
|
42
|
+
{creating && <p className="text-accent text-sm">creating wallet...</p>}
|
|
43
|
+
|
|
44
|
+
{error && (
|
|
45
|
+
<div className="space-y-4">
|
|
46
|
+
<div className="rounded border border-red-700/30 p-3 text-xs text-red-700">{error}</div>
|
|
47
|
+
<button
|
|
48
|
+
onClick={createWallet}
|
|
49
|
+
className="border-accent text-accent hover:bg-accent/10 w-full rounded border px-4 py-2 text-sm font-medium transition-colors"
|
|
50
|
+
>
|
|
51
|
+
retry
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
{success && (
|
|
57
|
+
<div className="space-y-4">
|
|
58
|
+
<div className="text-accent text-2xl">✓</div>
|
|
59
|
+
<p className="text-foreground text-sm font-medium">wallet created</p>
|
|
60
|
+
<button
|
|
61
|
+
onClick={onComplete}
|
|
62
|
+
className="border-accent text-accent hover:bg-accent/10 w-full rounded border px-4 py-2 text-sm font-medium transition-colors"
|
|
63
|
+
>
|
|
64
|
+
continue
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function Layout({ token, onLogout }: { token: string; onLogout: () => void }) {
|
|
73
|
+
const [page, setPage] = useState<Page>("home");
|
|
74
|
+
const [llmConfigured, setLlmConfigured] = useState<boolean | null>(null);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
async function checkSetup() {
|
|
78
|
+
try {
|
|
79
|
+
// Check LLM config
|
|
80
|
+
const llmRes = await fetch(`${API_BASE}/api/config/llm`, {
|
|
81
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
82
|
+
});
|
|
83
|
+
const llmData = await llmRes.json();
|
|
84
|
+
const hasLlm = llmData.configured?.length > 0;
|
|
85
|
+
setLlmConfigured(hasLlm);
|
|
86
|
+
|
|
87
|
+
if (!hasLlm) {
|
|
88
|
+
setPage("llm-setup");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check wallet existence
|
|
93
|
+
const walletRes = await fetch(`${API_BASE}/api/wallet`, {
|
|
94
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
95
|
+
});
|
|
96
|
+
const walletData = await walletRes.json();
|
|
97
|
+
if (!walletData.exists) {
|
|
98
|
+
setPage("wallet-setup");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
setLlmConfigured(false);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
checkSetup();
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className="flex h-screen flex-col">
|
|
110
|
+
{/* Header */}
|
|
111
|
+
<header className="border-border flex items-center justify-between border-b px-4 py-3">
|
|
112
|
+
<div className="flex items-center gap-3">
|
|
113
|
+
<button onClick={() => { if (page !== "wallet-setup") setPage("home"); }} className="flex items-center gap-2 hover:opacity-80">
|
|
114
|
+
<img src="/plotlink-logo.svg" alt="PlotLink" className="h-5 w-5" />
|
|
115
|
+
<span className="text-accent text-sm font-bold tracking-tight">PlotLink OWS</span>
|
|
116
|
+
</button>
|
|
117
|
+
<span className="text-muted text-[10px] uppercase tracking-wider">local writer</span>
|
|
118
|
+
</div>
|
|
119
|
+
{page !== "wallet-setup" && (
|
|
120
|
+
<nav className="flex items-center gap-4">
|
|
121
|
+
<button
|
|
122
|
+
onClick={() => setPage("chat")}
|
|
123
|
+
className={`text-xs transition-colors ${page === "chat" ? "text-accent" : "text-muted hover:text-foreground"}`}
|
|
124
|
+
>
|
|
125
|
+
write
|
|
126
|
+
</button>
|
|
127
|
+
<button
|
|
128
|
+
onClick={() => setPage("publish")}
|
|
129
|
+
className={`text-xs transition-colors ${page === "publish" ? "text-accent" : "text-muted hover:text-foreground"}`}
|
|
130
|
+
>
|
|
131
|
+
publish
|
|
132
|
+
</button>
|
|
133
|
+
<button
|
|
134
|
+
onClick={() => setPage("dashboard")}
|
|
135
|
+
className={`text-xs transition-colors ${page === "dashboard" ? "text-accent" : "text-muted hover:text-foreground"}`}
|
|
136
|
+
>
|
|
137
|
+
dashboard
|
|
138
|
+
</button>
|
|
139
|
+
<button
|
|
140
|
+
onClick={() => setPage("llm-setup")}
|
|
141
|
+
className={`text-xs transition-colors ${page === "llm-setup" ? "text-accent" : "text-muted hover:text-foreground"}`}
|
|
142
|
+
>
|
|
143
|
+
llm
|
|
144
|
+
</button>
|
|
145
|
+
<button
|
|
146
|
+
onClick={() => setPage("settings")}
|
|
147
|
+
className={`text-xs transition-colors ${page === "settings" ? "text-accent" : "text-muted hover:text-foreground"}`}
|
|
148
|
+
>
|
|
149
|
+
settings
|
|
150
|
+
</button>
|
|
151
|
+
<button onClick={onLogout} className="text-muted hover:text-foreground text-xs transition-colors">
|
|
152
|
+
logout
|
|
153
|
+
</button>
|
|
154
|
+
</nav>
|
|
155
|
+
)}
|
|
156
|
+
</header>
|
|
157
|
+
|
|
158
|
+
{/* Main content */}
|
|
159
|
+
<main className="flex-1 overflow-y-auto">
|
|
160
|
+
{page === "home" && (
|
|
161
|
+
<div className="mx-auto max-w-lg space-y-6 p-6">
|
|
162
|
+
{llmConfigured === false && (
|
|
163
|
+
<div className="border-accent/30 rounded border p-4 text-center">
|
|
164
|
+
<p className="text-accent text-sm font-medium">setup required</p>
|
|
165
|
+
<p className="text-muted mt-1 text-xs">connect an LLM provider to get started</p>
|
|
166
|
+
<button
|
|
167
|
+
onClick={() => setPage("llm-setup")}
|
|
168
|
+
className="border-accent text-accent hover:bg-accent/10 mt-3 rounded border px-4 py-2 text-xs font-medium transition-colors"
|
|
169
|
+
>
|
|
170
|
+
setup LLM
|
|
171
|
+
</button>
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
{llmConfigured && (
|
|
176
|
+
<>
|
|
177
|
+
<div className="text-center">
|
|
178
|
+
<p className="text-foreground text-sm font-medium">ready to write</p>
|
|
179
|
+
<p className="text-muted mt-1 text-xs">start a collaborative story session with the AI writer</p>
|
|
180
|
+
<button
|
|
181
|
+
onClick={() => setPage("chat")}
|
|
182
|
+
className="border-accent text-accent hover:bg-accent/10 mt-3 rounded border px-4 py-2 text-xs font-medium transition-colors"
|
|
183
|
+
>
|
|
184
|
+
start writing
|
|
185
|
+
</button>
|
|
186
|
+
</div>
|
|
187
|
+
<WalletCard token={token} />
|
|
188
|
+
</>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{page === "chat" && (
|
|
194
|
+
<Chat token={token} />
|
|
195
|
+
)}
|
|
196
|
+
|
|
197
|
+
{page === "publish" && (
|
|
198
|
+
<Publish token={token} />
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
{page === "dashboard" && (
|
|
202
|
+
<Dashboard token={token} />
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
{page === "llm-setup" && (
|
|
206
|
+
<LLMSetup
|
|
207
|
+
token={token}
|
|
208
|
+
onComplete={() => {
|
|
209
|
+
setLlmConfigured(true);
|
|
210
|
+
setPage("wallet-setup");
|
|
211
|
+
}}
|
|
212
|
+
/>
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
{page === "wallet-setup" && (
|
|
216
|
+
<WalletSetupPage
|
|
217
|
+
token={token}
|
|
218
|
+
onComplete={() => setPage("home")}
|
|
219
|
+
/>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
{page === "settings" && (
|
|
223
|
+
<Settings token={token} onLogout={onLogout} onChangeLLM={() => setPage("llm-setup")} />
|
|
224
|
+
)}
|
|
225
|
+
</main>
|
|
226
|
+
|
|
227
|
+
{/* Footer */}
|
|
228
|
+
<footer className="border-border border-t px-4 py-2">
|
|
229
|
+
<p className="text-muted text-[10px]">
|
|
230
|
+
session active · localhost:7777
|
|
231
|
+
</p>
|
|
232
|
+
</footer>
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
|
|
3
|
+
export function Login({ onLogin }: { onLogin: (passphrase: string) => Promise<string | null> }) {
|
|
4
|
+
const [passphrase, setPassphrase] = useState("");
|
|
5
|
+
const [error, setError] = useState<string | null>(null);
|
|
6
|
+
const [loading, setLoading] = useState(false);
|
|
7
|
+
|
|
8
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
9
|
+
e.preventDefault();
|
|
10
|
+
if (!passphrase.trim()) return;
|
|
11
|
+
setLoading(true);
|
|
12
|
+
setError(null);
|
|
13
|
+
const err = await onLogin(passphrase);
|
|
14
|
+
if (err) setError(err);
|
|
15
|
+
setLoading(false);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex h-screen items-center justify-center p-4">
|
|
20
|
+
<div className="w-full max-w-sm">
|
|
21
|
+
<div className="border-border rounded border p-6">
|
|
22
|
+
<div className="mb-6 text-center">
|
|
23
|
+
<h1 className="text-accent text-lg font-bold tracking-tight">PlotLink OWS</h1>
|
|
24
|
+
<p className="text-muted mt-1 text-xs">local writer agent</p>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
28
|
+
<div>
|
|
29
|
+
<label className="text-muted mb-1.5 block text-xs uppercase tracking-wider">
|
|
30
|
+
Passphrase
|
|
31
|
+
</label>
|
|
32
|
+
<input
|
|
33
|
+
type="password"
|
|
34
|
+
value={passphrase}
|
|
35
|
+
onChange={(e) => setPassphrase(e.target.value)}
|
|
36
|
+
placeholder="enter your passphrase"
|
|
37
|
+
autoFocus
|
|
38
|
+
className="bg-surface border-border text-foreground placeholder:text-muted/50 w-full rounded border px-3 py-2 text-sm outline-none focus:border-accent"
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{error && (
|
|
43
|
+
<p className="text-error text-xs">{error}</p>
|
|
44
|
+
)}
|
|
45
|
+
|
|
46
|
+
<button
|
|
47
|
+
type="submit"
|
|
48
|
+
disabled={loading || !passphrase.trim()}
|
|
49
|
+
className="border-accent text-accent hover:bg-accent/10 disabled:opacity-40 w-full rounded border px-4 py-2 text-sm font-medium transition-colors"
|
|
50
|
+
>
|
|
51
|
+
{loading ? "authenticating..." : "unlock"}
|
|
52
|
+
</button>
|
|
53
|
+
</form>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<p className="text-muted mt-4 text-center text-[10px]">
|
|
57
|
+
enter your passphrase to unlock
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import Markdown from "react-markdown";
|
|
3
|
+
|
|
4
|
+
const API_BASE = "http://localhost:7777";
|
|
5
|
+
|
|
6
|
+
interface Draft {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
content: string;
|
|
10
|
+
genre: string | null;
|
|
11
|
+
status: string;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Preflight {
|
|
16
|
+
ready: boolean;
|
|
17
|
+
address?: string;
|
|
18
|
+
ethBalance?: string;
|
|
19
|
+
creationFee?: string;
|
|
20
|
+
requiredBalance?: string;
|
|
21
|
+
hasEnoughEth?: boolean;
|
|
22
|
+
hasFilebase?: boolean;
|
|
23
|
+
error?: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface PublishProgress {
|
|
27
|
+
step: string;
|
|
28
|
+
message: string;
|
|
29
|
+
txHash?: string;
|
|
30
|
+
contentCid?: string;
|
|
31
|
+
error?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function Publish({ token }: { token: string }) {
|
|
35
|
+
const [drafts, setDrafts] = useState<Draft[]>([]);
|
|
36
|
+
const [selected, setSelected] = useState<Draft | null>(null);
|
|
37
|
+
const [preflight, setPreflight] = useState<Preflight | null>(null);
|
|
38
|
+
const [publishing, setPublishing] = useState(false);
|
|
39
|
+
const [progress, setProgress] = useState<PublishProgress | null>(null);
|
|
40
|
+
|
|
41
|
+
const authFetch = (url: string, opts?: RequestInit) =>
|
|
42
|
+
fetch(url, { ...opts, headers: { ...opts?.headers, Authorization: `Bearer ${token}`, "Content-Type": "application/json" } });
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
authFetch(`${API_BASE}/api/chat/drafts`)
|
|
46
|
+
.then((r) => r.json())
|
|
47
|
+
.then((data) => setDrafts(data.filter((d: Draft) => d.status !== "published")));
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const checkPreflight = async () => {
|
|
51
|
+
const res = await authFetch(`${API_BASE}/api/publish/preflight`);
|
|
52
|
+
const data = await res.json();
|
|
53
|
+
setPreflight(data);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handlePublish = async (draft: Draft) => {
|
|
57
|
+
setPublishing(true);
|
|
58
|
+
setProgress(null);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const res = await fetch(`${API_BASE}/api/publish/${draft.id}`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const reader = res.body?.getReader();
|
|
67
|
+
if (!reader) throw new Error("No stream");
|
|
68
|
+
const decoder = new TextDecoder();
|
|
69
|
+
let buffer = "";
|
|
70
|
+
|
|
71
|
+
while (true) {
|
|
72
|
+
const { done, value } = await reader.read();
|
|
73
|
+
if (done) break;
|
|
74
|
+
buffer += decoder.decode(value, { stream: true });
|
|
75
|
+
const lines = buffer.split("\n");
|
|
76
|
+
buffer = lines.pop() || "";
|
|
77
|
+
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
if (line.startsWith("data: ")) {
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(line.slice(6));
|
|
82
|
+
setProgress(parsed);
|
|
83
|
+
} catch { /* ignore */ }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Refresh drafts list
|
|
89
|
+
const draftsRes = await authFetch(`${API_BASE}/api/chat/drafts`);
|
|
90
|
+
const draftsData = await draftsRes.json();
|
|
91
|
+
setDrafts(draftsData.filter((d: Draft) => d.status !== "published"));
|
|
92
|
+
} catch (err: unknown) {
|
|
93
|
+
setProgress({ step: "error", message: err instanceof Error ? err.message : "Publish failed" });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setPublishing(false);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const formatEth = (wei: string) => {
|
|
100
|
+
const eth = Number(BigInt(wei)) / 1e18;
|
|
101
|
+
return eth.toFixed(6);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="mx-auto max-w-2xl space-y-6 p-6">
|
|
106
|
+
<h2 className="text-accent text-lg font-bold">Publish to PlotLink</h2>
|
|
107
|
+
<p className="text-muted text-xs">sign and broadcast your story on-chain via OWS wallet</p>
|
|
108
|
+
|
|
109
|
+
{/* Preflight check */}
|
|
110
|
+
{!preflight && (
|
|
111
|
+
<button
|
|
112
|
+
onClick={checkPreflight}
|
|
113
|
+
className="border-accent text-accent hover:bg-accent/10 rounded border px-4 py-2 text-sm font-medium transition-colors"
|
|
114
|
+
>
|
|
115
|
+
check publishing readiness
|
|
116
|
+
</button>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
{preflight && (
|
|
120
|
+
<div className="border-border rounded border p-4 space-y-2">
|
|
121
|
+
<h3 className="text-accent text-xs font-bold uppercase tracking-wider">Preflight</h3>
|
|
122
|
+
<div className="flex justify-between text-xs">
|
|
123
|
+
<span className="text-muted">Wallet</span>
|
|
124
|
+
<span className="text-foreground font-mono text-[10px]">{preflight.address?.slice(0, 10)}...</span>
|
|
125
|
+
</div>
|
|
126
|
+
<div className="flex justify-between text-xs">
|
|
127
|
+
<span className="text-muted">ETH Balance</span>
|
|
128
|
+
<span className={preflight.hasEnoughEth ? "text-accent" : "text-error"}>{preflight.ethBalance ? formatEth(preflight.ethBalance) : "0"} ETH</span>
|
|
129
|
+
</div>
|
|
130
|
+
{preflight.creationFee && BigInt(preflight.creationFee) > 0n && (
|
|
131
|
+
<div className="flex justify-between text-xs">
|
|
132
|
+
<span className="text-muted">Creation Fee</span>
|
|
133
|
+
<span className="text-foreground">{formatEth(preflight.creationFee)} ETH</span>
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
{preflight.requiredBalance && (
|
|
137
|
+
<div className="flex justify-between text-xs">
|
|
138
|
+
<span className="text-muted">Required (fee + gas)</span>
|
|
139
|
+
<span className="text-foreground">~{formatEth(preflight.requiredBalance)} ETH</span>
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
<div className="flex justify-between text-xs">
|
|
143
|
+
<span className="text-muted">Filebase (IPFS)</span>
|
|
144
|
+
<span className={preflight.hasFilebase ? "text-accent" : "text-error"}>{preflight.hasFilebase ? "configured" : "missing"}</span>
|
|
145
|
+
</div>
|
|
146
|
+
{preflight.error && <p className="text-error text-xs">{preflight.error}</p>}
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{/* Draft list */}
|
|
151
|
+
{drafts.length === 0 && (
|
|
152
|
+
<p className="text-muted text-xs">no drafts ready for publishing — finalize a story from the chat first</p>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
{drafts.map((draft) => (
|
|
156
|
+
<div key={draft.id} className="border-border rounded border p-4 space-y-3">
|
|
157
|
+
<div className="flex items-center justify-between">
|
|
158
|
+
<h3 className="text-foreground text-sm font-medium">{draft.title}</h3>
|
|
159
|
+
<span className="text-accent text-[10px]">{draft.genre}</span>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{selected?.id === draft.id ? (
|
|
163
|
+
<div className="space-y-3">
|
|
164
|
+
{/* Full preview */}
|
|
165
|
+
<div className="bg-surface max-h-80 overflow-y-auto rounded p-3">
|
|
166
|
+
<div className="prose prose-xs max-w-none text-xs leading-relaxed">
|
|
167
|
+
<Markdown>{draft.content}</Markdown>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
{/* Publish button */}
|
|
172
|
+
{preflight?.ready && !publishing && (
|
|
173
|
+
<div className="space-y-2">
|
|
174
|
+
<p className="text-muted text-[10px]">
|
|
175
|
+
This will upload to IPFS and publish on-chain to Base via your OWS wallet.
|
|
176
|
+
</p>
|
|
177
|
+
<button
|
|
178
|
+
onClick={() => handlePublish(draft)}
|
|
179
|
+
className="border-accent text-accent hover:bg-accent/10 w-full rounded border px-4 py-2 text-sm font-medium transition-colors"
|
|
180
|
+
>
|
|
181
|
+
publish to PlotLink
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
{!preflight?.ready && (
|
|
187
|
+
<p className="text-error text-xs">publishing not ready — check preflight above</p>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
<button onClick={() => setSelected(null)} className="text-muted hover:text-foreground text-xs">
|
|
191
|
+
collapse
|
|
192
|
+
</button>
|
|
193
|
+
</div>
|
|
194
|
+
) : (
|
|
195
|
+
<div className="flex items-center justify-between">
|
|
196
|
+
<p className="text-muted text-xs truncate max-w-[60%]">{draft.content.slice(0, 100)}...</p>
|
|
197
|
+
<button
|
|
198
|
+
onClick={() => { setSelected(draft); if (!preflight) checkPreflight(); }}
|
|
199
|
+
className="border-border text-muted hover:border-accent hover:text-accent rounded border px-3 py-1 text-[10px] font-medium transition-colors"
|
|
200
|
+
>
|
|
201
|
+
preview & publish
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
))}
|
|
207
|
+
|
|
208
|
+
{/* Progress */}
|
|
209
|
+
{progress && (
|
|
210
|
+
<div className={`rounded border p-4 space-y-2 ${progress.step === "error" ? "border-red-700/30" : progress.step === "done" ? "border-green-700/30" : "border-accent/30"}`}>
|
|
211
|
+
<div className="flex items-center gap-2">
|
|
212
|
+
{progress.step !== "done" && progress.step !== "error" && (
|
|
213
|
+
<span className="text-accent animate-pulse text-xs">●</span>
|
|
214
|
+
)}
|
|
215
|
+
<span className={`text-xs font-medium ${progress.step === "error" ? "text-red-700" : progress.step === "done" ? "text-accent" : "text-accent"}`}>
|
|
216
|
+
{progress.message}
|
|
217
|
+
</span>
|
|
218
|
+
</div>
|
|
219
|
+
{progress.txHash && (
|
|
220
|
+
<div className="text-xs">
|
|
221
|
+
<span className="text-muted">tx: </span>
|
|
222
|
+
<a href={`https://basescan.org/tx/${progress.txHash}`} target="_blank" rel="noopener noreferrer" className="text-accent font-mono text-[10px] underline">
|
|
223
|
+
{progress.txHash.slice(0, 14)}...
|
|
224
|
+
</a>
|
|
225
|
+
</div>
|
|
226
|
+
)}
|
|
227
|
+
{progress.contentCid && (
|
|
228
|
+
<div className="text-xs">
|
|
229
|
+
<span className="text-muted">IPFS: </span>
|
|
230
|
+
<span className="text-foreground font-mono text-[10px]">{progress.contentCid}</span>
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
{progress.storylineId && (
|
|
234
|
+
<div className="text-xs">
|
|
235
|
+
<span className="text-muted">story: </span>
|
|
236
|
+
<a href={`https://plotlink.xyz/story/${progress.storylineId}`} target="_blank" rel="noopener noreferrer" className="text-accent underline">
|
|
237
|
+
plotlink.xyz/story/{progress.storylineId}
|
|
238
|
+
</a>
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|