plotlink-ows 0.1.14 → 0.1.18

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 (37) hide show
  1. package/README.md +49 -57
  2. package/app/db.ts +1 -1
  3. package/app/lib/paths.ts +0 -2
  4. package/app/lib/publish.ts +150 -39
  5. package/app/prisma/schema.prisma +0 -36
  6. package/app/routes/dashboard.ts +53 -56
  7. package/app/routes/publish.ts +55 -24
  8. package/app/routes/stories.ts +156 -0
  9. package/app/routes/terminal.ts +154 -0
  10. package/app/routes/wallet.ts +40 -10
  11. package/app/server.ts +29 -81
  12. package/app/web/App.tsx +4 -6
  13. package/app/web/components/Dashboard.tsx +15 -47
  14. package/app/web/components/Layout.tsx +70 -103
  15. package/app/web/components/PreviewPanel.tsx +149 -0
  16. package/app/web/components/Settings.tsx +3 -84
  17. package/app/web/components/StoriesPage.tsx +157 -0
  18. package/app/web/components/StoryBrowser.tsx +137 -0
  19. package/app/web/components/TerminalPanel.tsx +122 -0
  20. package/app/web/components/WalletCard.tsx +14 -8
  21. package/app/web/dist/assets/index-D5gfwaEX.css +32 -0
  22. package/app/web/dist/assets/index-pBt5Q_bN.js +117 -0
  23. package/app/web/dist/index.html +3 -3
  24. package/app/web/dist/plotlink-logo.svg +5 -0
  25. package/app/web/public/plotlink-logo.svg +5 -0
  26. package/bin/plotlink-ows.js +13 -12
  27. package/package.json +9 -5
  28. package/app/lib/llm-client.ts +0 -265
  29. package/app/lib/writer-prompt.ts +0 -44
  30. package/app/routes/chat.ts +0 -135
  31. package/app/routes/config.ts +0 -210
  32. package/app/routes/oauth.ts +0 -150
  33. package/app/web/components/Chat.tsx +0 -272
  34. package/app/web/components/LLMSetup.tsx +0 -291
  35. package/app/web/components/Publish.tsx +0 -245
  36. package/app/web/dist/assets/index-C9kXlYO_.css +0 -2
  37. package/app/web/dist/assets/index-CJiiaLHs.js +0 -9
@@ -29,17 +29,15 @@ interface DashboardData {
29
29
  pnl: { totalCostsEth: string; totalCostsUsd: string; totalRoyaltiesPlot: string; totalRoyaltiesUsd: string; netPnlUsd: string; plotUsdPrice: string };
30
30
  stories: {
31
31
  published: Story[];
32
- drafts: Story[];
33
32
  totalPublished: number;
34
- totalDrafts: number;
33
+ totalStories: number;
34
+ totalFiles: number;
35
+ pendingFiles: number;
35
36
  };
36
- sessions: { total: number; totalMessages: number };
37
37
  }
38
38
 
39
39
  export function Dashboard({ token }: { token: string }) {
40
40
  const [data, setData] = useState<DashboardData | null>(null);
41
- const [deleting, setDeleting] = useState<string | null>(null);
42
-
43
41
  const authFetch = (url: string, opts?: RequestInit) =>
44
42
  fetch(url, { ...opts, headers: { ...opts?.headers, Authorization: `Bearer ${token}`, "Content-Type": "application/json" } });
45
43
 
@@ -51,13 +49,6 @@ export function Dashboard({ token }: { token: string }) {
51
49
 
52
50
  useEffect(() => { loadDashboard(); }, []);
53
51
 
54
- const handleDelete = async (id: string) => {
55
- setDeleting(id);
56
- await authFetch(`${API_BASE}/api/dashboard/drafts/${id}`, { method: "DELETE" });
57
- loadDashboard();
58
- setDeleting(null);
59
- };
60
-
61
52
  const truncate = (addr: string) => `${addr.slice(0, 6)}...${addr.slice(-4)}`;
62
53
  const formatDate = (d: string) => new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
63
54
 
@@ -80,16 +71,16 @@ export function Dashboard({ token }: { token: string }) {
80
71
  <div className="text-muted text-[10px] uppercase tracking-wider">published</div>
81
72
  </div>
82
73
  <div className="border-border rounded border p-3 text-center">
83
- <div className="text-foreground text-lg font-bold">{data.stories.totalDrafts}</div>
84
- <div className="text-muted text-[10px] uppercase tracking-wider">drafts</div>
74
+ <div className="text-foreground text-lg font-bold">{data.stories.pendingFiles}</div>
75
+ <div className="text-muted text-[10px] uppercase tracking-wider">pending</div>
85
76
  </div>
86
77
  <div className="border-border rounded border p-3 text-center">
87
- <div className="text-foreground text-lg font-bold">{data.sessions.total}</div>
88
- <div className="text-muted text-[10px] uppercase tracking-wider">sessions</div>
78
+ <div className="text-foreground text-lg font-bold">{data.stories.totalStories}</div>
79
+ <div className="text-muted text-[10px] uppercase tracking-wider">stories</div>
89
80
  </div>
90
81
  <div className="border-border rounded border p-3 text-center">
91
- <div className="text-foreground text-lg font-bold">{data.sessions.totalMessages}</div>
92
- <div className="text-muted text-[10px] uppercase tracking-wider">messages</div>
82
+ <div className="text-foreground text-lg font-bold">{data.stories.totalFiles}</div>
83
+ <div className="text-muted text-[10px] uppercase tracking-wider">files</div>
93
84
  </div>
94
85
  </div>
95
86
 
@@ -188,35 +179,12 @@ export function Dashboard({ token }: { token: string }) {
188
179
  )}
189
180
  </div>
190
181
 
191
- {/* Draft stories */}
192
- <div className="border-border rounded border p-4">
193
- <h3 className="text-accent mb-3 text-xs font-bold uppercase tracking-wider">Drafts</h3>
194
- {data.stories.drafts.length === 0 ? (
195
- <p className="text-muted text-xs">no drafts — start writing from the chat</p>
196
- ) : (
197
- <div className="space-y-2">
198
- {data.stories.drafts.map((draft) => (
199
- <div key={draft.id} className="bg-surface flex items-center justify-between rounded p-3">
200
- <div>
201
- <span className="text-foreground text-sm font-medium">{draft.title}</span>
202
- {draft.genre && <span className="text-accent ml-2 text-[10px]">{draft.genre}</span>}
203
- <div className="text-muted text-[10px]">{formatDate(draft.createdAt)}</div>
204
- </div>
205
- <div className="flex items-center gap-2">
206
- <span className="border-border rounded border px-1.5 py-0.5 text-[9px] text-muted">{draft.status}</span>
207
- <button
208
- onClick={() => handleDelete(draft.id)}
209
- disabled={deleting === draft.id}
210
- className="text-muted hover:text-error text-[10px] transition-colors"
211
- >
212
- {deleting === draft.id ? "..." : "delete"}
213
- </button>
214
- </div>
215
- </div>
216
- ))}
217
- </div>
218
- )}
219
- </div>
182
+ {/* Pending files info */}
183
+ {data.stories.pendingFiles > 0 && (
184
+ <div className="border-border rounded border p-4">
185
+ <p className="text-muted text-xs">{data.stories.pendingFiles} file(s) pending publish — go to Stories to publish them.</p>
186
+ </div>
187
+ )}
220
188
  </div>
221
189
  );
222
190
  }
@@ -1,14 +1,10 @@
1
- import React, { useState, useEffect } from "react";
2
- import { LLMSetup } from "./LLMSetup";
3
- import { WalletCard } from "./WalletCard";
1
+ import React, { useState, useEffect, useCallback } from "react";
4
2
  import { Settings } from "./Settings";
5
- import { Chat } from "./Chat";
6
- import { Publish } from "./Publish";
7
3
  import { Dashboard } from "./Dashboard";
4
+ import { StoriesPage } from "./StoriesPage";
5
+ import { WalletCard } from "./WalletCard";
8
6
 
9
- const API_BASE = "http://localhost:7777";
10
-
11
- type Page = "home" | "chat" | "publish" | "dashboard" | "llm-setup" | "wallet-setup" | "settings";
7
+ type Page = "home" | "stories" | "dashboard" | "wallet-setup" | "settings";
12
8
 
13
9
  function WalletSetupPage({ token, onComplete }: { token: string; onComplete: () => void }) {
14
10
  const [creating, setCreating] = useState(false);
@@ -19,7 +15,7 @@ function WalletSetupPage({ token, onComplete }: { token: string; onComplete: ()
19
15
  setCreating(true);
20
16
  setError(null);
21
17
  try {
22
- const res = await fetch(`${API_BASE}/api/wallet/create`, {
18
+ const res = await fetch("/api/wallet/create", {
23
19
  method: "POST",
24
20
  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
25
21
  });
@@ -32,12 +28,13 @@ function WalletSetupPage({ token, onComplete }: { token: string; onComplete: ()
32
28
  setCreating(false);
33
29
  };
34
30
 
31
+ // eslint-disable-next-line react-hooks/exhaustive-deps
35
32
  useEffect(() => { createWallet(); }, []);
36
33
 
37
34
  return (
38
35
  <div className="mx-auto max-w-sm p-6 text-center">
39
36
  <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>
37
+ <p className="text-muted mb-6 text-xs">creating your OWS wallet for on-chain publishing</p>
41
38
 
42
39
  {creating && <p className="text-accent text-sm">creating wallet...</p>}
43
40
 
@@ -71,64 +68,58 @@ function WalletSetupPage({ token, onComplete }: { token: string; onComplete: ()
71
68
 
72
69
  export function Layout({ token, onLogout }: { token: string; onLogout: () => void }) {
73
70
  const [page, setPage] = useState<Page>("home");
74
- const [llmConfigured, setLlmConfigured] = useState<boolean | null>(null);
71
+ const [storyCount, setStoryCount] = useState(0);
72
+
73
+ const authFetch = useCallback(async (url: string, opts?: RequestInit) => {
74
+ return fetch(url, {
75
+ ...opts,
76
+ headers: {
77
+ ...(opts?.headers || {}),
78
+ Authorization: `Bearer ${token}`,
79
+ },
80
+ });
81
+ }, [token]);
75
82
 
76
83
  useEffect(() => {
77
84
  async function checkSetup() {
78
85
  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
86
  // Check wallet existence
93
- const walletRes = await fetch(`${API_BASE}/api/wallet`, {
94
- headers: { Authorization: `Bearer ${token}` },
95
- });
87
+ const walletRes = await authFetch("/api/wallet");
96
88
  const walletData = await walletRes.json();
97
89
  if (!walletData.exists) {
98
90
  setPage("wallet-setup");
99
91
  return;
100
92
  }
101
- } catch {
102
- setLlmConfigured(false);
103
- }
93
+
94
+ // Load story count
95
+ const storiesRes = await authFetch("/api/stories");
96
+ if (storiesRes.ok) {
97
+ const data = await storiesRes.json();
98
+ setStoryCount(data.stories.filter((s: { name: string }) => s.name !== "_example").length);
99
+ }
100
+ } catch { /* ignore */ }
104
101
  }
105
102
  checkSetup();
106
- }, []);
103
+ // eslint-disable-next-line react-hooks/exhaustive-deps
104
+ }, [token]);
107
105
 
108
106
  return (
109
107
  <div className="flex h-screen flex-col">
110
108
  {/* Header */}
111
- <header className="border-border flex items-center justify-between border-b px-4 py-3">
109
+ <header className="border-border flex h-14 items-center justify-between border-b px-4 flex-shrink-0">
112
110
  <div className="flex items-center gap-3">
113
111
  <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
112
  <span className="text-accent text-sm font-bold tracking-tight">PlotLink OWS</span>
116
113
  </button>
117
- <span className="text-muted text-[10px] uppercase tracking-wider">local writer</span>
114
+ <span className="text-muted text-[10px] uppercase tracking-wider">writer</span>
118
115
  </div>
119
116
  {page !== "wallet-setup" && (
120
117
  <nav className="flex items-center gap-4">
121
118
  <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"}`}
119
+ onClick={() => setPage("stories")}
120
+ className={`text-xs transition-colors ${page === "stories" ? "text-accent" : "text-muted hover:text-foreground"}`}
130
121
  >
131
- publish
122
+ stories
132
123
  </button>
133
124
  <button
134
125
  onClick={() => setPage("dashboard")}
@@ -136,12 +127,6 @@ export function Layout({ token, onLogout }: { token: string; onLogout: () => voi
136
127
  >
137
128
  dashboard
138
129
  </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
130
  <button
146
131
  onClick={() => setPage("settings")}
147
132
  className={`text-xs transition-colors ${page === "settings" ? "text-accent" : "text-muted hover:text-foreground"}`}
@@ -156,62 +141,51 @@ export function Layout({ token, onLogout }: { token: string; onLogout: () => voi
156
141
  </header>
157
142
 
158
143
  {/* Main content */}
159
- <main className="flex-1 overflow-y-auto">
144
+ <main className="flex-1 min-h-0">
160
145
  {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
- )}
146
+ <div className="mx-auto max-w-lg space-y-6 p-8">
147
+ <div className="text-center space-y-2">
148
+ <h1 className="text-2xl font-serif text-foreground">Write. Publish. Earn.</h1>
149
+ <p className="text-muted text-sm">
150
+ Claude CLI writes stories. You publish them on-chain.
151
+ </p>
152
+ </div>
153
+
154
+ <div className="text-center space-y-3">
155
+ <button
156
+ onClick={() => setPage("stories")}
157
+ className="bg-accent text-white hover:bg-accent-dim px-6 py-2.5 rounded text-sm font-medium transition-colors"
158
+ >
159
+ Start Writing
160
+ </button>
161
+ {storyCount > 0 && (
162
+ <p className="text-muted text-xs">{storyCount} {storyCount === 1 ? "story" : "stories"} in progress</p>
163
+ )}
164
+ </div>
165
+
166
+ <div className="rounded border border-border p-4 space-y-2 text-xs text-muted">
167
+ <p className="font-medium text-foreground text-sm">How it works</p>
168
+ <ol className="space-y-1.5 list-decimal list-inside">
169
+ <li>Open the <strong>Stories</strong> tab — Claude CLI launches in the terminal</li>
170
+ <li>Tell Claude your story idea — it brainstorms, outlines, and writes</li>
171
+ <li>Review the live preview as Claude creates files</li>
172
+ <li>Click <strong>Publish</strong> to put your story on-chain</li>
173
+ <li>Earn 5% royalties on every trade at <a href="https://plotlink.xyz" target="_blank" rel="noopener noreferrer" className="text-accent underline">plotlink.xyz</a></li>
174
+ </ol>
175
+ </div>
176
+
177
+ <WalletCard token={token} />
190
178
  </div>
191
179
  )}
192
180
 
193
- {page === "chat" && (
194
- <Chat token={token} />
195
- )}
196
-
197
- {page === "publish" && (
198
- <Publish token={token} />
181
+ {page === "stories" && (
182
+ <StoriesPage token={token} authFetch={authFetch} />
199
183
  )}
200
184
 
201
185
  {page === "dashboard" && (
202
186
  <Dashboard token={token} />
203
187
  )}
204
188
 
205
- {page === "llm-setup" && (
206
- <LLMSetup
207
- token={token}
208
- onComplete={() => {
209
- setLlmConfigured(true);
210
- setPage("wallet-setup");
211
- }}
212
- />
213
- )}
214
-
215
189
  {page === "wallet-setup" && (
216
190
  <WalletSetupPage
217
191
  token={token}
@@ -220,16 +194,9 @@ export function Layout({ token, onLogout }: { token: string; onLogout: () => voi
220
194
  )}
221
195
 
222
196
  {page === "settings" && (
223
- <Settings token={token} onLogout={onLogout} onChangeLLM={() => setPage("llm-setup")} />
197
+ <Settings token={token} onLogout={onLogout} />
224
198
  )}
225
199
  </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
200
  </div>
234
201
  );
235
202
  }
@@ -0,0 +1,149 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import ReactMarkdown from "react-markdown";
3
+ import remarkBreaks from "remark-breaks";
4
+ import remarkGfm from "remark-gfm";
5
+ import rehypeSanitize from "rehype-sanitize";
6
+
7
+ interface PreviewPanelProps {
8
+ storyName: string | null;
9
+ fileName: string | null;
10
+ authFetch: (url: string, opts?: RequestInit) => Promise<Response>;
11
+ onPublish?: (storyName: string, fileName: string) => void;
12
+ publishingFile?: string | null;
13
+ }
14
+
15
+ interface FileData {
16
+ file: string;
17
+ status: "published" | "pending" | "draft";
18
+ content: string;
19
+ txHash?: string;
20
+ storylineId?: number;
21
+ }
22
+
23
+ export function PreviewPanel({ storyName, fileName, authFetch, onPublish, publishingFile }: PreviewPanelProps) {
24
+ const [fileData, setFileData] = useState<FileData | null>(null);
25
+ const [loading, setLoading] = useState(false);
26
+
27
+ const loadFile = useCallback(async () => {
28
+ if (!storyName || !fileName) { setFileData(null); return; }
29
+ try {
30
+ const res = await authFetch(`/api/stories/${storyName}/${fileName}`);
31
+ if (res.ok) {
32
+ setFileData(await res.json());
33
+ }
34
+ } catch { /* ignore */ }
35
+ }, [storyName, fileName, authFetch]);
36
+
37
+ // Initial load
38
+ useEffect(() => {
39
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- async fetch on mount
40
+ setLoading(true);
41
+ loadFile().finally(() => setLoading(false));
42
+ }, [loadFile]);
43
+
44
+ // Auto-refresh every 3 seconds
45
+ useEffect(() => {
46
+ if (!storyName || !fileName) return;
47
+ const interval = setInterval(loadFile, 3000);
48
+ return () => clearInterval(interval);
49
+ }, [storyName, fileName, loadFile]);
50
+
51
+ if (!storyName || !fileName) {
52
+ return (
53
+ <div className="h-full flex items-center justify-center text-muted">
54
+ <div className="text-center">
55
+ <p className="text-lg font-serif">Select a file to preview</p>
56
+ <p className="text-sm mt-1">Click a story file in the sidebar</p>
57
+ </div>
58
+ </div>
59
+ );
60
+ }
61
+
62
+ if (loading && !fileData) {
63
+ return (
64
+ <div className="h-full flex items-center justify-center text-muted">
65
+ Loading...
66
+ </div>
67
+ );
68
+ }
69
+
70
+ const charCount = fileData?.content?.length ?? 0;
71
+ const isGenesis = fileName === "genesis.md";
72
+ const charLimit = isGenesis ? 1000 : 10000;
73
+ const overLimit = charCount > charLimit;
74
+
75
+ return (
76
+ <div className="h-full flex flex-col">
77
+ <div className="px-3 py-1.5 border-b border-border flex items-center justify-between">
78
+ <div className="flex items-center gap-2 text-xs font-mono text-muted">
79
+ <span>{storyName}/{fileName}</span>
80
+ {fileData?.status === "published" && (
81
+ <span className="text-green-700 font-medium">Published</span>
82
+ )}
83
+ {fileData?.status === "pending" && (
84
+ <span className="text-amber-700 font-medium">Pending</span>
85
+ )}
86
+ </div>
87
+ <div className="flex items-center gap-2">
88
+ <span className={`text-xs font-mono ${overLimit ? "text-error" : "text-muted"}`}>
89
+ {charCount.toLocaleString()}/{charLimit.toLocaleString()}
90
+ </span>
91
+ </div>
92
+ </div>
93
+
94
+ <div className="flex-1 min-h-0 overflow-y-auto px-6 py-4" style={{ background: "var(--paper-bg)" }}>
95
+ {fileData?.content ? (
96
+ <div className="prose max-w-none">
97
+ <ReactMarkdown
98
+ remarkPlugins={[remarkBreaks, remarkGfm]}
99
+ rehypePlugins={[rehypeSanitize]}
100
+ >
101
+ {fileData.content}
102
+ </ReactMarkdown>
103
+ </div>
104
+ ) : (
105
+ <p className="text-muted italic">No content</p>
106
+ )}
107
+ </div>
108
+
109
+ {/* Action bar */}
110
+ <div className="px-3 py-2 border-t border-border flex items-center justify-between">
111
+ {fileName === "structure.md" ? (
112
+ <p className="text-muted text-xs italic">This is your story outline — not publishable. Ask AI to write the genesis next.</p>
113
+ ) : fileData?.status === "published" ? (
114
+ <div className="flex items-center gap-2 text-xs">
115
+ <span className="text-green-700">Published</span>
116
+ {fileData.storylineId && (
117
+ <a
118
+ href={`https://plotlink.xyz/story/${fileData.storylineId}`}
119
+ target="_blank"
120
+ rel="noopener noreferrer"
121
+ className="text-accent underline"
122
+ >
123
+ View on PlotLink
124
+ </a>
125
+ )}
126
+ {fileData.txHash && (
127
+ <a
128
+ href={`https://basescan.org/tx/${fileData.txHash}`}
129
+ target="_blank"
130
+ rel="noopener noreferrer"
131
+ className="text-muted underline"
132
+ >
133
+ BaseScan
134
+ </a>
135
+ )}
136
+ </div>
137
+ ) : (
138
+ <button
139
+ onClick={() => storyName && fileName && onPublish?.(storyName, fileName)}
140
+ disabled={!!publishingFile || fileData?.status === "published"}
141
+ className="px-4 py-1.5 bg-accent text-white text-sm rounded hover:bg-accent-dim disabled:opacity-50 disabled:cursor-not-allowed"
142
+ >
143
+ {publishingFile === fileName ? "Publishing..." : "Publish to PlotLink"}
144
+ </button>
145
+ )}
146
+ </div>
147
+ </div>
148
+ );
149
+ }
@@ -1,13 +1,7 @@
1
- import React, { useState, useEffect } from "react";
1
+ import React, { useState } from "react";
2
2
  import { WalletCard } from "./WalletCard";
3
3
 
4
- const API_BASE = "http://localhost:7777";
5
-
6
- export function Settings({ token, onLogout, onChangeLLM }: { token: string; onLogout: () => void; onChangeLLM: () => void }) {
7
- const [llmConfig, setLlmConfig] = useState<{ llm: Record<string, unknown>; configured: string[] } | null>(null);
8
- const [spendCap, setSpendCap] = useState<string>("10");
9
- const [savingCap, setSavingCap] = useState(false);
10
- const [capSaved, setCapSaved] = useState(false);
4
+ export function Settings({ token, onLogout }: { token: string; onLogout: () => void }) {
11
5
  const [newPassphrase, setNewPassphrase] = useState("");
12
6
  const [confirmPassphrase, setConfirmPassphrase] = useState("");
13
7
  const [passphraseError, setPassphraseError] = useState<string | null>(null);
@@ -17,28 +11,6 @@ export function Settings({ token, onLogout, onChangeLLM }: { token: string; onLo
17
11
  const authFetch = (url: string, opts?: RequestInit) =>
18
12
  fetch(url, { ...opts, headers: { ...opts?.headers, Authorization: `Bearer ${token}`, "Content-Type": "application/json" } });
19
13
 
20
- useEffect(() => {
21
- authFetch(`${API_BASE}/api/config/llm`)
22
- .then((r) => r.json())
23
- .then((data) => setLlmConfig(data));
24
- }, []);
25
-
26
- const activeProvider = (llmConfig?.llm as Record<string, unknown>)?.activeProvider as string | undefined;
27
- const activeModel = (llmConfig?.llm as Record<string, unknown>)?.activeModel as string | undefined;
28
-
29
- const handleSaveSpendCap = async () => {
30
- setSavingCap(true);
31
- setCapSaved(false);
32
- // Persist spending cap to agent.config.json
33
- await authFetch(`${API_BASE}/api/config/llm`, {
34
- method: "POST",
35
- body: JSON.stringify({ provider: activeProvider || "anthropic", model: activeModel || "", spendCap: Number(spendCap) }),
36
- });
37
- setSavingCap(false);
38
- setCapSaved(true);
39
- setTimeout(() => setCapSaved(false), 2000);
40
- };
41
-
42
14
  const handleResetPassphrase = async () => {
43
15
  setPassphraseError(null);
44
16
  setPassphraseSuccess(false);
@@ -52,7 +24,7 @@ export function Settings({ token, onLogout, onChangeLLM }: { token: string; onLo
52
24
  }
53
25
  setSavingPassphrase(true);
54
26
  try {
55
- const res = await authFetch(`${API_BASE}/api/auth/reset-passphrase`, {
27
+ const res = await authFetch("/api/auth/reset-passphrase", {
56
28
  method: "POST",
57
29
  body: JSON.stringify({ passphrase: newPassphrase }),
58
30
  });
@@ -74,62 +46,9 @@ export function Settings({ token, onLogout, onChangeLLM }: { token: string; onLo
74
46
  <div className="mx-auto max-w-lg space-y-6 p-6">
75
47
  <h2 className="text-accent text-lg font-bold">Settings</h2>
76
48
 
77
- {/* LLM Config */}
78
- <div className="border-border rounded border p-4">
79
- <h3 className="text-accent mb-3 text-xs font-bold uppercase tracking-wider">LLM Provider</h3>
80
- {llmConfig ? (
81
- <div className="space-y-3">
82
- <div className="flex justify-between text-xs">
83
- <span className="text-muted">Provider</span>
84
- <span className="text-foreground font-medium">{activeProvider || "not configured"}</span>
85
- </div>
86
- <div className="flex justify-between text-xs">
87
- <span className="text-muted">Model</span>
88
- <span className="text-foreground font-medium">{activeModel || "—"}</span>
89
- </div>
90
- <div className="flex justify-between text-xs">
91
- <span className="text-muted">Configured</span>
92
- <span className="text-foreground font-medium">{llmConfig.configured.join(", ") || "none"}</span>
93
- </div>
94
- <button
95
- onClick={onChangeLLM}
96
- className="border-accent text-accent hover:bg-accent/10 w-full rounded border px-4 py-2 text-xs font-medium transition-colors"
97
- >
98
- change provider / model
99
- </button>
100
- </div>
101
- ) : (
102
- <p className="text-muted text-xs">loading...</p>
103
- )}
104
- </div>
105
-
106
49
  {/* Wallet */}
107
50
  <WalletCard token={token} />
108
51
 
109
- {/* Spending Cap */}
110
- <div className="border-border rounded border p-4">
111
- <h3 className="text-accent mb-3 text-xs font-bold uppercase tracking-wider">Spending Cap</h3>
112
- <div className="flex items-center gap-2">
113
- <span className="text-muted text-xs">$</span>
114
- <input
115
- type="number"
116
- value={spendCap}
117
- onChange={(e) => setSpendCap(e.target.value)}
118
- min="0"
119
- step="1"
120
- className="bg-surface border-border text-foreground w-24 rounded border px-2 py-1.5 text-sm outline-none focus:border-accent"
121
- />
122
- <span className="text-muted text-xs">USDC per session</span>
123
- <button
124
- onClick={handleSaveSpendCap}
125
- disabled={savingCap}
126
- className="border-accent text-accent hover:bg-accent/10 disabled:opacity-40 ml-auto rounded border px-3 py-1.5 text-xs font-medium transition-colors"
127
- >
128
- {savingCap ? "saving..." : capSaved ? "saved" : "save"}
129
- </button>
130
- </div>
131
- </div>
132
-
133
52
  {/* Reset Passphrase */}
134
53
  <div className="border-border rounded border p-4">
135
54
  <h3 className="text-accent mb-3 text-xs font-bold uppercase tracking-wider">Reset Passphrase</h3>