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,222 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
const API_BASE = "http://localhost:7777";
|
|
4
|
+
|
|
5
|
+
interface WalletInfo {
|
|
6
|
+
address: string;
|
|
7
|
+
ethBalance: string;
|
|
8
|
+
ethFormatted: string;
|
|
9
|
+
usdcBalance: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Story {
|
|
13
|
+
id: string;
|
|
14
|
+
title: string;
|
|
15
|
+
genre: string | null;
|
|
16
|
+
status: string;
|
|
17
|
+
txHash?: string | null;
|
|
18
|
+
storylineId?: number | null;
|
|
19
|
+
gasCostEth?: string | null;
|
|
20
|
+
gasCostUsd?: string | null;
|
|
21
|
+
createdAt: string;
|
|
22
|
+
updatedAt?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface DashboardData {
|
|
26
|
+
wallet: WalletInfo | null;
|
|
27
|
+
costs: { totalGasCostEth: string; totalCostUsd: string; ethUsdPrice: number; storiesPublished: number };
|
|
28
|
+
royalties: { earned: string; claimed: string; unclaimed: string; token: string };
|
|
29
|
+
pnl: { totalCostsEth: string; totalCostsUsd: string; totalRoyaltiesPlot: string; totalRoyaltiesUsd: string; netPnlUsd: string; plotUsdPrice: string };
|
|
30
|
+
stories: {
|
|
31
|
+
published: Story[];
|
|
32
|
+
drafts: Story[];
|
|
33
|
+
totalPublished: number;
|
|
34
|
+
totalDrafts: number;
|
|
35
|
+
};
|
|
36
|
+
sessions: { total: number; totalMessages: number };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function Dashboard({ token }: { token: string }) {
|
|
40
|
+
const [data, setData] = useState<DashboardData | null>(null);
|
|
41
|
+
const [deleting, setDeleting] = useState<string | null>(null);
|
|
42
|
+
|
|
43
|
+
const authFetch = (url: string, opts?: RequestInit) =>
|
|
44
|
+
fetch(url, { ...opts, headers: { ...opts?.headers, Authorization: `Bearer ${token}`, "Content-Type": "application/json" } });
|
|
45
|
+
|
|
46
|
+
const loadDashboard = () => {
|
|
47
|
+
authFetch(`${API_BASE}/api/dashboard`)
|
|
48
|
+
.then((r) => r.json())
|
|
49
|
+
.then(setData);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
useEffect(() => { loadDashboard(); }, []);
|
|
53
|
+
|
|
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
|
+
const truncate = (addr: string) => `${addr.slice(0, 6)}...${addr.slice(-4)}`;
|
|
62
|
+
const formatDate = (d: string) => new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
63
|
+
|
|
64
|
+
if (!data) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="flex h-full items-center justify-center">
|
|
67
|
+
<span className="text-muted text-sm">loading dashboard...</span>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="mx-auto max-w-2xl space-y-6 p-6">
|
|
74
|
+
<h2 className="text-accent text-lg font-bold">Writer Dashboard</h2>
|
|
75
|
+
|
|
76
|
+
{/* Stats overview */}
|
|
77
|
+
<div className="grid grid-cols-4 gap-3">
|
|
78
|
+
<div className="border-border rounded border p-3 text-center">
|
|
79
|
+
<div className="text-accent text-lg font-bold">{data.stories.totalPublished}</div>
|
|
80
|
+
<div className="text-muted text-[10px] uppercase tracking-wider">published</div>
|
|
81
|
+
</div>
|
|
82
|
+
<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>
|
|
85
|
+
</div>
|
|
86
|
+
<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>
|
|
89
|
+
</div>
|
|
90
|
+
<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>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{/* Wallet overview */}
|
|
97
|
+
{data.wallet && (
|
|
98
|
+
<div className="border-border rounded border p-4">
|
|
99
|
+
<h3 className="text-accent mb-3 text-xs font-bold uppercase tracking-wider">Wallet</h3>
|
|
100
|
+
<div className="space-y-1.5">
|
|
101
|
+
<div className="flex justify-between text-xs">
|
|
102
|
+
<span className="text-muted">Address</span>
|
|
103
|
+
<code className="text-foreground font-mono text-[10px]">{truncate(data.wallet.address)}</code>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="flex justify-between text-xs">
|
|
106
|
+
<span className="text-muted">ETH Balance</span>
|
|
107
|
+
<span className="text-foreground">{data.wallet.ethFormatted} ETH</span>
|
|
108
|
+
</div>
|
|
109
|
+
<div className="flex justify-between text-xs">
|
|
110
|
+
<span className="text-muted">USDC Balance</span>
|
|
111
|
+
<span className="text-foreground">${data.wallet.usdcBalance}</span>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
{/* P&L */}
|
|
118
|
+
<div className="border-border rounded border p-4">
|
|
119
|
+
<h3 className="text-accent mb-3 text-xs font-bold uppercase tracking-wider">Profit & Loss</h3>
|
|
120
|
+
<div className="space-y-1.5">
|
|
121
|
+
<div className="flex justify-between text-xs">
|
|
122
|
+
<span className="text-muted">Total costs (gas)</span>
|
|
123
|
+
<span className="text-error">-{data.pnl.totalCostsEth} ETH (~${data.pnl.totalCostsUsd})</span>
|
|
124
|
+
</div>
|
|
125
|
+
<div className="flex justify-between text-xs">
|
|
126
|
+
<span className="text-muted">Royalties earned</span>
|
|
127
|
+
<span className="text-accent">+{data.pnl.totalRoyaltiesPlot} PLOT</span>
|
|
128
|
+
</div>
|
|
129
|
+
<div className="flex justify-between text-xs">
|
|
130
|
+
<span className="text-muted">Unclaimed royalties</span>
|
|
131
|
+
<span className="text-foreground">{data.royalties.unclaimed} PLOT</span>
|
|
132
|
+
</div>
|
|
133
|
+
<div className="border-border flex justify-between border-t pt-1.5 text-xs font-medium">
|
|
134
|
+
<span className="text-muted">Net P&L (USD)</span>
|
|
135
|
+
<span className={parseFloat(data.pnl.netPnlUsd) >= 0 ? "text-accent" : "text-error"}>
|
|
136
|
+
{parseFloat(data.pnl.netPnlUsd) >= 0 ? "+" : ""}${data.pnl.netPnlUsd}
|
|
137
|
+
</span>
|
|
138
|
+
</div>
|
|
139
|
+
<div className="flex justify-between text-xs">
|
|
140
|
+
<span className="text-muted">Stories published</span>
|
|
141
|
+
<span className="text-foreground">{data.costs.storiesPublished}</span>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{/* Published stories */}
|
|
147
|
+
<div className="border-border rounded border p-4">
|
|
148
|
+
<h3 className="text-accent mb-3 text-xs font-bold uppercase tracking-wider">Published Stories</h3>
|
|
149
|
+
{data.stories.published.length === 0 ? (
|
|
150
|
+
<p className="text-muted text-xs">no published stories yet</p>
|
|
151
|
+
) : (
|
|
152
|
+
<div className="space-y-2">
|
|
153
|
+
{data.stories.published.map((story) => (
|
|
154
|
+
<div key={story.id} className="bg-surface rounded p-3">
|
|
155
|
+
<div className="flex items-center justify-between">
|
|
156
|
+
<div>
|
|
157
|
+
<span className="text-foreground text-sm font-medium">{story.title}</span>
|
|
158
|
+
{story.genre && <span className="text-accent ml-2 text-[10px]">{story.genre}</span>}
|
|
159
|
+
</div>
|
|
160
|
+
<div className="flex items-center gap-2">
|
|
161
|
+
<span className="rounded border border-green-700/30 px-1.5 py-0.5 text-[9px] text-accent">published</span>
|
|
162
|
+
{story.storylineId ? (
|
|
163
|
+
<a
|
|
164
|
+
href={`https://plotlink.xyz/story/${story.storylineId}`}
|
|
165
|
+
target="_blank"
|
|
166
|
+
rel="noopener noreferrer"
|
|
167
|
+
className="text-accent text-[10px] underline"
|
|
168
|
+
>
|
|
169
|
+
view
|
|
170
|
+
</a>
|
|
171
|
+
) : (
|
|
172
|
+
<a href="https://plotlink.xyz" target="_blank" rel="noopener noreferrer" className="text-accent text-[10px] underline">plotlink.xyz</a>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
<div className="mt-1 flex items-center gap-3 text-[10px]">
|
|
177
|
+
<span className="text-muted">{formatDate(story.createdAt)}</span>
|
|
178
|
+
{story.txHash && (
|
|
179
|
+
<a href={`https://basescan.org/tx/${story.txHash}`} target="_blank" rel="noopener noreferrer" className="text-muted hover:text-accent font-mono">
|
|
180
|
+
tx:{story.txHash.slice(0, 10)}...
|
|
181
|
+
</a>
|
|
182
|
+
)}
|
|
183
|
+
{story.gasCostEth && <span className="text-muted">{story.gasCostEth} ETH{story.gasCostUsd ? ` (~$${story.gasCostUsd})` : ""}</span>}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
))}
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
|
|
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>
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
const API_BASE = "http://localhost:7777";
|
|
4
|
+
|
|
5
|
+
interface Provider {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
envKey: string | null;
|
|
9
|
+
models: string[];
|
|
10
|
+
tag: string | null;
|
|
11
|
+
configured: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type Step = "provider" | "auth" | "model" | "test" | "done";
|
|
15
|
+
|
|
16
|
+
export function LLMSetup({ token, onComplete }: { token: string; onComplete: () => void }) {
|
|
17
|
+
const [step, setStep] = useState<Step>("provider");
|
|
18
|
+
const [providers, setProviders] = useState<Provider[]>([]);
|
|
19
|
+
const [selected, setSelected] = useState<string>("");
|
|
20
|
+
const [model, setModel] = useState<string>("");
|
|
21
|
+
const [apiKey, setApiKey] = useState<string>("");
|
|
22
|
+
const [baseUrl, setBaseUrl] = useState<string>("http://localhost:11434");
|
|
23
|
+
const [testing, setTesting] = useState(false);
|
|
24
|
+
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
|
25
|
+
const [saving, setSaving] = useState(false);
|
|
26
|
+
const [error, setError] = useState<string | null>(null);
|
|
27
|
+
|
|
28
|
+
const authFetch = (url: string, opts?: RequestInit) =>
|
|
29
|
+
fetch(url, { ...opts, headers: { ...opts?.headers, Authorization: `Bearer ${token}`, "Content-Type": "application/json" } });
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
authFetch(`${API_BASE}/api/config/llm/providers`)
|
|
33
|
+
.then((r) => r.json())
|
|
34
|
+
.then((data) => setProviders(data));
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const selectedProvider = providers.find((p) => p.id === selected);
|
|
38
|
+
|
|
39
|
+
const handleTest = async () => {
|
|
40
|
+
setTesting(true);
|
|
41
|
+
setTestResult(null);
|
|
42
|
+
try {
|
|
43
|
+
const res = await authFetch(`${API_BASE}/api/config/llm/test`, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
body: JSON.stringify({ provider: selected, model, apiKey: apiKey || undefined, baseUrl: selected === "local" ? baseUrl : undefined }),
|
|
46
|
+
});
|
|
47
|
+
const data = await res.json();
|
|
48
|
+
setTestResult(data);
|
|
49
|
+
} catch {
|
|
50
|
+
setTestResult({ success: false, message: "Connection failed" });
|
|
51
|
+
}
|
|
52
|
+
setTesting(false);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleSave = async () => {
|
|
56
|
+
setSaving(true);
|
|
57
|
+
setError(null);
|
|
58
|
+
try {
|
|
59
|
+
const res = await authFetch(`${API_BASE}/api/config/llm`, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
body: JSON.stringify({ provider: selected, model, apiKey: apiKey || undefined, baseUrl: selected === "local" ? baseUrl : undefined }),
|
|
62
|
+
});
|
|
63
|
+
const data = await res.json();
|
|
64
|
+
if (!res.ok) throw new Error(data.error || "Save failed");
|
|
65
|
+
setStep("done");
|
|
66
|
+
} catch (err: unknown) {
|
|
67
|
+
setError(err instanceof Error ? err.message : "Save failed");
|
|
68
|
+
}
|
|
69
|
+
setSaving(false);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="mx-auto max-w-lg p-6">
|
|
74
|
+
<h2 className="text-accent mb-1 text-lg font-bold">LLM Setup</h2>
|
|
75
|
+
<p className="text-muted mb-6 text-xs">connect your AI provider to power the writer agent</p>
|
|
76
|
+
|
|
77
|
+
{/* Step indicator */}
|
|
78
|
+
<div className="text-muted mb-6 flex gap-2 text-[10px] uppercase tracking-wider">
|
|
79
|
+
{(["provider", "auth", "model", "test", "done"] as Step[]).map((s) => (
|
|
80
|
+
<span key={s} className={step === s ? "text-accent" : ""}>{s}</span>
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Provider selection */}
|
|
85
|
+
{step === "provider" && (
|
|
86
|
+
<div className="space-y-3">
|
|
87
|
+
{providers.map((p) => (
|
|
88
|
+
<button
|
|
89
|
+
key={p.id}
|
|
90
|
+
onClick={() => { setSelected(p.id); setModel(p.models[0] || ""); setStep(p.id === "local" ? "auth" : "auth"); }}
|
|
91
|
+
className={`border-border hover:border-accent w-full rounded border p-3 text-left transition-colors ${selected === p.id ? "border-accent" : ""}`}
|
|
92
|
+
>
|
|
93
|
+
<div className="flex items-center justify-between">
|
|
94
|
+
<span className="text-foreground text-sm font-medium">{p.name}</span>
|
|
95
|
+
<span className="flex gap-1.5">
|
|
96
|
+
{p.tag && <span className="text-accent border-accent/30 rounded border px-1.5 py-0.5 text-[9px]">{p.tag}</span>}
|
|
97
|
+
{p.configured && <span className="rounded border border-green-700/30 px-1.5 py-0.5 text-[9px] text-accent">configured</span>}
|
|
98
|
+
</span>
|
|
99
|
+
</div>
|
|
100
|
+
</button>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
{/* Auth step */}
|
|
106
|
+
{step === "auth" && selectedProvider && (
|
|
107
|
+
<div className="space-y-4">
|
|
108
|
+
<button onClick={() => setStep("provider")} className="text-muted hover:text-foreground text-xs">← back</button>
|
|
109
|
+
<h3 className="text-foreground text-sm font-medium">{selectedProvider.name}</h3>
|
|
110
|
+
|
|
111
|
+
{selected === "local" ? (
|
|
112
|
+
<div className="space-y-3">
|
|
113
|
+
<div>
|
|
114
|
+
<label className="text-muted mb-1.5 block text-xs uppercase tracking-wider">Base URL</label>
|
|
115
|
+
<input
|
|
116
|
+
type="text"
|
|
117
|
+
value={baseUrl}
|
|
118
|
+
onChange={(e) => setBaseUrl(e.target.value)}
|
|
119
|
+
placeholder="http://localhost:11434"
|
|
120
|
+
className="bg-surface border-border text-foreground w-full rounded border px-3 py-2 text-sm outline-none focus:border-accent"
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
<div>
|
|
124
|
+
<label className="text-muted mb-1.5 block text-xs uppercase tracking-wider">Model Name</label>
|
|
125
|
+
<input
|
|
126
|
+
type="text"
|
|
127
|
+
value={model}
|
|
128
|
+
onChange={(e) => setModel(e.target.value)}
|
|
129
|
+
placeholder="llama3.2"
|
|
130
|
+
className="bg-surface border-border text-foreground w-full rounded border px-3 py-2 text-sm outline-none focus:border-accent"
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
) : (
|
|
135
|
+
<div className="space-y-4">
|
|
136
|
+
{/* OAuth option */}
|
|
137
|
+
{(selected === "anthropic" || selected === "openai") && (
|
|
138
|
+
<div>
|
|
139
|
+
<button
|
|
140
|
+
onClick={async () => {
|
|
141
|
+
try {
|
|
142
|
+
const res = await authFetch(`${API_BASE}/api/oauth/${selected}/start`);
|
|
143
|
+
const data = await res.json();
|
|
144
|
+
if (data.authUrl) {
|
|
145
|
+
window.open(data.authUrl, "oauth", "width=600,height=700");
|
|
146
|
+
// Poll for completion
|
|
147
|
+
const poll = setInterval(async () => {
|
|
148
|
+
const status = await authFetch(`${API_BASE}/api/oauth/${selected}/status`).then((r) => r.json());
|
|
149
|
+
if (status.complete) {
|
|
150
|
+
clearInterval(poll);
|
|
151
|
+
setStep("model");
|
|
152
|
+
}
|
|
153
|
+
}, 1500);
|
|
154
|
+
setTimeout(() => clearInterval(poll), 120000);
|
|
155
|
+
}
|
|
156
|
+
} catch { /* ignore */ }
|
|
157
|
+
}}
|
|
158
|
+
className="border-accent text-accent hover:bg-accent/10 w-full rounded border px-4 py-2 text-sm font-medium transition-colors"
|
|
159
|
+
>
|
|
160
|
+
connect with OAuth (recommended)
|
|
161
|
+
</button>
|
|
162
|
+
<div className="text-muted my-3 flex items-center gap-2 text-[10px]">
|
|
163
|
+
<div className="border-border flex-1 border-t" />
|
|
164
|
+
<span>or use API key</span>
|
|
165
|
+
<div className="border-border flex-1 border-t" />
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
{/* API key input */}
|
|
171
|
+
<div>
|
|
172
|
+
<label className="text-muted mb-1.5 block text-xs uppercase tracking-wider">API Key</label>
|
|
173
|
+
<input
|
|
174
|
+
type="password"
|
|
175
|
+
value={apiKey}
|
|
176
|
+
onChange={(e) => setApiKey(e.target.value)}
|
|
177
|
+
placeholder={`paste your ${selectedProvider.name} API key`}
|
|
178
|
+
className="bg-surface border-border text-foreground w-full rounded border px-3 py-2 text-sm outline-none focus:border-accent"
|
|
179
|
+
/>
|
|
180
|
+
{selectedProvider.configured && (
|
|
181
|
+
<p className="text-muted mt-1.5 text-[10px]">key already saved — leave blank to keep current</p>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
<button
|
|
188
|
+
onClick={() => setStep("model")}
|
|
189
|
+
disabled={selected === "local" ? !model.trim() : (!apiKey.trim() && !selectedProvider.configured)}
|
|
190
|
+
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"
|
|
191
|
+
>
|
|
192
|
+
next
|
|
193
|
+
</button>
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
|
|
197
|
+
{/* Model selection */}
|
|
198
|
+
{step === "model" && selectedProvider && (
|
|
199
|
+
<div className="space-y-4">
|
|
200
|
+
<button onClick={() => setStep("auth")} className="text-muted hover:text-foreground text-xs">← back</button>
|
|
201
|
+
<h3 className="text-foreground text-sm font-medium">Select Model</h3>
|
|
202
|
+
|
|
203
|
+
{selected === "local" ? (
|
|
204
|
+
<p className="text-muted text-xs">Using: <span className="text-foreground font-medium">{model}</span></p>
|
|
205
|
+
) : (
|
|
206
|
+
<div className="space-y-2">
|
|
207
|
+
{selectedProvider.models.map((m) => (
|
|
208
|
+
<button
|
|
209
|
+
key={m}
|
|
210
|
+
onClick={() => setModel(m)}
|
|
211
|
+
className={`border-border w-full rounded border px-3 py-2 text-left text-sm transition-colors ${model === m ? "border-accent text-accent" : "text-foreground hover:border-accent/50"}`}
|
|
212
|
+
>
|
|
213
|
+
{m}
|
|
214
|
+
</button>
|
|
215
|
+
))}
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
<button
|
|
220
|
+
onClick={() => setStep("test")}
|
|
221
|
+
disabled={!model}
|
|
222
|
+
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"
|
|
223
|
+
>
|
|
224
|
+
test connection
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
|
|
229
|
+
{/* Test step */}
|
|
230
|
+
{step === "test" && (
|
|
231
|
+
<div className="space-y-4">
|
|
232
|
+
<button onClick={() => setStep("model")} className="text-muted hover:text-foreground text-xs">← back</button>
|
|
233
|
+
<h3 className="text-foreground text-sm font-medium">Test Connection</h3>
|
|
234
|
+
<p className="text-muted text-xs">{selectedProvider?.name} / {model}</p>
|
|
235
|
+
|
|
236
|
+
{!testResult && (
|
|
237
|
+
<button
|
|
238
|
+
onClick={handleTest}
|
|
239
|
+
disabled={testing}
|
|
240
|
+
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"
|
|
241
|
+
>
|
|
242
|
+
{testing ? "testing..." : "run test"}
|
|
243
|
+
</button>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
{testResult && (
|
|
247
|
+
<div className={`rounded border p-3 text-xs ${testResult.success ? "border-accent/30 text-accent" : "border-error/30 text-error"}`}>
|
|
248
|
+
{testResult.success ? "connected" : "failed"}: {testResult.message}
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
|
|
252
|
+
{testResult?.success && (
|
|
253
|
+
<button
|
|
254
|
+
onClick={handleSave}
|
|
255
|
+
disabled={saving}
|
|
256
|
+
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"
|
|
257
|
+
>
|
|
258
|
+
{saving ? "saving..." : "save & continue"}
|
|
259
|
+
</button>
|
|
260
|
+
)}
|
|
261
|
+
|
|
262
|
+
{testResult && !testResult.success && (
|
|
263
|
+
<button
|
|
264
|
+
onClick={() => { setTestResult(null); setStep("auth"); }}
|
|
265
|
+
className="text-muted hover:text-foreground w-full py-2 text-xs transition-colors"
|
|
266
|
+
>
|
|
267
|
+
go back and fix
|
|
268
|
+
</button>
|
|
269
|
+
)}
|
|
270
|
+
|
|
271
|
+
{error && <p className="text-error text-xs">{error}</p>}
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
|
|
275
|
+
{/* Done */}
|
|
276
|
+
{step === "done" && (
|
|
277
|
+
<div className="space-y-4 text-center">
|
|
278
|
+
<div className="text-accent text-2xl">✓</div>
|
|
279
|
+
<p className="text-foreground text-sm font-medium">LLM configured</p>
|
|
280
|
+
<p className="text-muted text-xs">{selectedProvider?.name} / {model}</p>
|
|
281
|
+
<button
|
|
282
|
+
onClick={onComplete}
|
|
283
|
+
className="border-accent text-accent hover:bg-accent/10 w-full rounded border px-4 py-2 text-sm font-medium transition-colors"
|
|
284
|
+
>
|
|
285
|
+
continue to wallet setup
|
|
286
|
+
</button>
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}
|