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.
Files changed (142) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +151 -0
  3. package/app/db.ts +8 -0
  4. package/app/lib/llm-client.ts +265 -0
  5. package/app/lib/paths.ts +11 -0
  6. package/app/lib/publish.ts +204 -0
  7. package/app/lib/writer-prompt.ts +44 -0
  8. package/app/node_modules/.prisma/local-client/client.d.ts +1 -0
  9. package/app/node_modules/.prisma/local-client/client.js +5 -0
  10. package/app/node_modules/.prisma/local-client/default.d.ts +1 -0
  11. package/app/node_modules/.prisma/local-client/default.js +5 -0
  12. package/app/node_modules/.prisma/local-client/edge.d.ts +1 -0
  13. package/app/node_modules/.prisma/local-client/edge.js +184 -0
  14. package/app/node_modules/.prisma/local-client/index-browser.js +173 -0
  15. package/app/node_modules/.prisma/local-client/index.d.ts +3304 -0
  16. package/app/node_modules/.prisma/local-client/index.js +207 -0
  17. package/app/node_modules/.prisma/local-client/libquery_engine-darwin-arm64.dylib.node +0 -0
  18. package/app/node_modules/.prisma/local-client/package.json +183 -0
  19. package/app/node_modules/.prisma/local-client/query_engine_bg.js +2 -0
  20. package/app/node_modules/.prisma/local-client/query_engine_bg.wasm +0 -0
  21. package/app/node_modules/.prisma/local-client/runtime/edge-esm.js +35 -0
  22. package/app/node_modules/.prisma/local-client/runtime/edge.js +35 -0
  23. package/app/node_modules/.prisma/local-client/runtime/index-browser.d.ts +370 -0
  24. package/app/node_modules/.prisma/local-client/runtime/index-browser.js +17 -0
  25. package/app/node_modules/.prisma/local-client/runtime/library.d.ts +3982 -0
  26. package/app/node_modules/.prisma/local-client/runtime/library.js +147 -0
  27. package/app/node_modules/.prisma/local-client/runtime/react-native.js +84 -0
  28. package/app/node_modules/.prisma/local-client/runtime/wasm-compiler-edge.js +85 -0
  29. package/app/node_modules/.prisma/local-client/runtime/wasm-engine-edge.js +38 -0
  30. package/app/node_modules/.prisma/local-client/schema.prisma +21 -0
  31. package/app/node_modules/.prisma/local-client/wasm-edge-light-loader.mjs +5 -0
  32. package/app/node_modules/.prisma/local-client/wasm-worker-loader.mjs +5 -0
  33. package/app/node_modules/.prisma/local-client/wasm.d.ts +1 -0
  34. package/app/node_modules/.prisma/local-client/wasm.js +191 -0
  35. package/app/prisma/schema.prisma +57 -0
  36. package/app/routes/auth.ts +173 -0
  37. package/app/routes/chat.ts +135 -0
  38. package/app/routes/config.ts +210 -0
  39. package/app/routes/dashboard.ts +186 -0
  40. package/app/routes/oauth.ts +150 -0
  41. package/app/routes/publish.ts +112 -0
  42. package/app/routes/wallet.ts +99 -0
  43. package/app/server.ts +154 -0
  44. package/app/vite.config.ts +19 -0
  45. package/app/web/App.tsx +102 -0
  46. package/app/web/components/Chat.tsx +272 -0
  47. package/app/web/components/Dashboard.tsx +222 -0
  48. package/app/web/components/LLMSetup.tsx +291 -0
  49. package/app/web/components/Layout.tsx +235 -0
  50. package/app/web/components/Login.tsx +62 -0
  51. package/app/web/components/Publish.tsx +245 -0
  52. package/app/web/components/Settings.tsx +175 -0
  53. package/app/web/components/Setup.tsx +84 -0
  54. package/app/web/components/WalletCard.tsx +117 -0
  55. package/app/web/dist/assets/index-C9kXlYO_.css +2 -0
  56. package/app/web/dist/assets/index-CJiiaLHs.js +9 -0
  57. package/app/web/dist/index.html +16 -0
  58. package/app/web/index.html +15 -0
  59. package/app/web/main.tsx +10 -0
  60. package/app/web/plotlink-logo.svg +5 -0
  61. package/app/web/styles.css +51 -0
  62. package/bin/plotlink-ows.js +394 -0
  63. package/lib/ows/index.ts +3 -0
  64. package/lib/ows/policy.ts +68 -0
  65. package/lib/ows/types.ts +14 -0
  66. package/lib/ows/wallet.ts +70 -0
  67. package/package.json +79 -0
  68. package/packages/cli/node_modules/commander/LICENSE +22 -0
  69. package/packages/cli/node_modules/commander/Readme.md +1149 -0
  70. package/packages/cli/node_modules/commander/esm.mjs +16 -0
  71. package/packages/cli/node_modules/commander/index.js +24 -0
  72. package/packages/cli/node_modules/commander/lib/argument.js +149 -0
  73. package/packages/cli/node_modules/commander/lib/command.js +2662 -0
  74. package/packages/cli/node_modules/commander/lib/error.js +39 -0
  75. package/packages/cli/node_modules/commander/lib/help.js +709 -0
  76. package/packages/cli/node_modules/commander/lib/option.js +367 -0
  77. package/packages/cli/node_modules/commander/lib/suggestSimilar.js +101 -0
  78. package/packages/cli/node_modules/commander/package-support.json +16 -0
  79. package/packages/cli/node_modules/commander/package.json +82 -0
  80. package/packages/cli/node_modules/commander/typings/esm.d.mts +3 -0
  81. package/packages/cli/node_modules/commander/typings/index.d.ts +1045 -0
  82. package/packages/cli/node_modules/resolve-from/index.d.ts +31 -0
  83. package/packages/cli/node_modules/resolve-from/index.js +47 -0
  84. package/packages/cli/node_modules/resolve-from/license +9 -0
  85. package/packages/cli/node_modules/resolve-from/package.json +36 -0
  86. package/packages/cli/node_modules/resolve-from/readme.md +72 -0
  87. package/packages/cli/node_modules/tsup/LICENSE +21 -0
  88. package/packages/cli/node_modules/tsup/README.md +75 -0
  89. package/packages/cli/node_modules/tsup/assets/cjs_shims.js +13 -0
  90. package/packages/cli/node_modules/tsup/assets/esm_shims.js +9 -0
  91. package/packages/cli/node_modules/tsup/assets/package.json +3 -0
  92. package/packages/cli/node_modules/tsup/dist/chunk-DI5BO6XE.js +153 -0
  93. package/packages/cli/node_modules/tsup/dist/chunk-JZ25TPTY.js +42 -0
  94. package/packages/cli/node_modules/tsup/dist/chunk-PEEXUWMS.js +6 -0
  95. package/packages/cli/node_modules/tsup/dist/chunk-TWFEYLU4.js +352 -0
  96. package/packages/cli/node_modules/tsup/dist/chunk-VGC3FXLU.js +203 -0
  97. package/packages/cli/node_modules/tsup/dist/cli-default.js +12 -0
  98. package/packages/cli/node_modules/tsup/dist/cli-main.js +8 -0
  99. package/packages/cli/node_modules/tsup/dist/cli-node.js +14 -0
  100. package/packages/cli/node_modules/tsup/dist/index.d.ts +511 -0
  101. package/packages/cli/node_modules/tsup/dist/index.js +1711 -0
  102. package/packages/cli/node_modules/tsup/dist/rollup.js +6949 -0
  103. package/packages/cli/node_modules/tsup/package.json +99 -0
  104. package/packages/cli/node_modules/tsup/schema.json +362 -0
  105. package/packages/cli/package.json +35 -0
  106. package/packages/cli/src/commands/agent-register.ts +77 -0
  107. package/packages/cli/src/commands/chain.ts +29 -0
  108. package/packages/cli/src/commands/claim.ts +70 -0
  109. package/packages/cli/src/commands/create.ts +34 -0
  110. package/packages/cli/src/commands/status.ts +201 -0
  111. package/packages/cli/src/config.ts +103 -0
  112. package/packages/cli/src/index.ts +21 -0
  113. package/packages/cli/src/sdk/abi.ts +222 -0
  114. package/packages/cli/src/sdk/client.ts +713 -0
  115. package/packages/cli/src/sdk/constants.ts +56 -0
  116. package/packages/cli/src/sdk/index.ts +46 -0
  117. package/packages/cli/src/sdk/ipfs.ts +88 -0
  118. package/packages/cli/src/sdk.ts +36 -0
  119. package/packages/cli/tsconfig.json +20 -0
  120. package/packages/cli/tsup.config.ts +14 -0
  121. package/public/.well-known/farcaster.json +38 -0
  122. package/public/basescan-icon.svg +4 -0
  123. package/public/embed-image.png +0 -0
  124. package/public/favicon.png +0 -0
  125. package/public/hunt-token.svg +11 -0
  126. package/public/icon-192.png +0 -0
  127. package/public/icon.png +0 -0
  128. package/public/manifest.json +26 -0
  129. package/public/mc-icon-light.svg +12 -0
  130. package/public/og-image.png +0 -0
  131. package/public/plotlink-logo-symbol.svg +5 -0
  132. package/public/plotlink-logo.svg +5 -0
  133. package/public/screenshot-1.png +0 -0
  134. package/public/screenshot-2.png +0 -0
  135. package/public/screenshot-3.png +0 -0
  136. package/public/splash.png +0 -0
  137. package/public/wide-banner.png +0 -0
  138. package/scripts/backfill-trade-prices.ts +97 -0
  139. package/scripts/backfill-usd-rates.ts +220 -0
  140. package/scripts/e2e-verify.ts +1100 -0
  141. package/scripts/ows-smoke-test.ts +37 -0
  142. 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">&#x2713;</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 &middot; 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 &amp; 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">&#x25CF;</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
+ }