plotlink-ows 0.1.15 → 1.0.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.
Files changed (40) hide show
  1. package/README.md +185 -93
  2. package/app/db.ts +1 -1
  3. package/app/lib/paths.ts +0 -2
  4. package/app/lib/publish.ts +257 -44
  5. package/app/prisma/schema.prisma +0 -36
  6. package/app/routes/dashboard.ts +105 -57
  7. package/app/routes/publish.ts +107 -25
  8. package/app/routes/settings.ts +194 -0
  9. package/app/routes/stories.ts +223 -0
  10. package/app/routes/terminal.ts +258 -0
  11. package/app/routes/wallet.ts +40 -10
  12. package/app/server.ts +35 -81
  13. package/app/web/App.tsx +4 -6
  14. package/app/web/components/Dashboard.tsx +98 -79
  15. package/app/web/components/Layout.tsx +70 -103
  16. package/app/web/components/PreviewPanel.tsx +388 -0
  17. package/app/web/components/Settings.tsx +210 -67
  18. package/app/web/components/StoriesPage.tsx +270 -0
  19. package/app/web/components/StoryBrowser.tsx +161 -0
  20. package/app/web/components/TerminalPanel.tsx +428 -0
  21. package/app/web/components/WalletCard.tsx +14 -8
  22. package/app/web/dist/assets/index-BuOxhUWG.css +32 -0
  23. package/app/web/dist/assets/index-De8CpT47.js +129 -0
  24. package/app/web/dist/index.html +3 -3
  25. package/app/web/dist/plotlink-logo.svg +5 -0
  26. package/app/web/public/plotlink-logo.svg +5 -0
  27. package/app/web/styles.css +18 -0
  28. package/bin/plotlink-ows.js +18 -62
  29. package/package.json +15 -6
  30. package/scripts/fix-index-status.ts +93 -0
  31. package/app/lib/llm-client.ts +0 -265
  32. package/app/lib/writer-prompt.ts +0 -44
  33. package/app/routes/chat.ts +0 -135
  34. package/app/routes/config.ts +0 -210
  35. package/app/routes/oauth.ts +0 -150
  36. package/app/web/components/Chat.tsx +0 -272
  37. package/app/web/components/LLMSetup.tsx +0 -291
  38. package/app/web/components/Publish.tsx +0 -245
  39. package/app/web/dist/assets/index-C9kXlYO_.css +0 -2
  40. package/app/web/dist/assets/index-CJiiaLHs.js +0 -9
@@ -1,245 +0,0 @@
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
- }
@@ -1,2 +0,0 @@
1
- /*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */
2
- @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial}}}@layer theme{:root,:host{--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--container-sm:24rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-medium:500;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wider:.05em;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent;font-family:Geist Mono,ui-monospace,monospace;line-height:1.5}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.block{display:block}.flex{display:flex}.h-screen{height:100vh}.w-full{width:100%}.max-w-sm{max-width:var(--container-sm)}.flex-1{flex:1}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-3{gap:calc(var(--spacing) * 3)}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}.rounded{border-radius:.25rem}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-accent{border-color:var(--accent)}.border-border{border-color:var(--border)}.bg-surface{background-color:var(--bg-surface)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.text-accent{color:var(--accent)}.text-error{color:var(--error)}.text-foreground{color:var(--text)}.text-muted{color:var(--text-muted)}.uppercase{text-transform:uppercase}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.outline-none{--tw-outline-style:none;outline-style:none}.placeholder\:text-muted\/50::placeholder{color:var(--text-muted)}@supports (color:color-mix(in lab, red, red)){.placeholder\:text-muted\/50::placeholder{color:color-mix(in oklab, var(--text-muted) 50%, transparent)}}@media (hover:hover){.hover\:bg-accent\/10:hover{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-accent\/10:hover{background-color:color-mix(in oklab, var(--accent) 10%, transparent)}}.hover\:text-foreground:hover{color:var(--text)}}.focus\:border-accent:focus{border-color:var(--accent)}.disabled\:opacity-40:disabled{opacity:.4}}:root{--bg:#0a0a0a;--bg-surface:#141414;--text:#e0e0e0;--text-muted:#666;--accent:#0f8;--accent-dim:#00cc6a;--border:#2a2a2a;--error:#f44}body{background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:Geist Mono,ui-monospace,monospace}::selection{background:var(--accent);color:var(--bg)}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}