claude-world-studio 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.
- package/.env.example +30 -0
- package/.mcp.json +51 -0
- package/README.md +224 -0
- package/client/App.tsx +446 -0
- package/client/components/ChatWindow.tsx +790 -0
- package/client/components/FileExplorer.tsx +218 -0
- package/client/components/FilePreviewModal.tsx +179 -0
- package/client/components/PublishDialog.tsx +307 -0
- package/client/components/SettingsPage.tsx +452 -0
- package/client/components/Sidebar.tsx +198 -0
- package/client/components/ToolUseBlock.tsx +140 -0
- package/client/index.html +12 -0
- package/client/index.tsx +10 -0
- package/client/styles/globals.css +48 -0
- package/demo/01-welcome.png +0 -0
- package/demo/02-pipeline-cards.png +0 -0
- package/demo/03-custom-topic-fill.png +0 -0
- package/demo/04-topic-typed.png +0 -0
- package/demo/05-loading-state.png +0 -0
- package/demo/06-tool-calls.png +0 -0
- package/demo/07-history-rich.png +0 -0
- package/demo/09-en-cards.png +0 -0
- package/demo/10-ja-cards.png +0 -0
- package/demo/capture-remaining.mjs +73 -0
- package/demo/capture.mjs +110 -0
- package/demo/demo-walkthrough-2.webm +0 -0
- package/demo/demo-walkthrough.webm +0 -0
- package/package.json +48 -0
- package/postcss.config.js +6 -0
- package/scripts/threads_api.py +536 -0
- package/server/ai-client.ts +356 -0
- package/server/db.ts +299 -0
- package/server/mcp-config.ts +85 -0
- package/server/routes/accounts.ts +88 -0
- package/server/routes/files.ts +175 -0
- package/server/routes/publish.ts +77 -0
- package/server/routes/sessions.ts +59 -0
- package/server/routes/settings.ts +220 -0
- package/server/server.ts +261 -0
- package/server/services/social-publisher.ts +74 -0
- package/server/services/studio-mcp.ts +107 -0
- package/server/session.ts +167 -0
- package/server/types.ts +86 -0
- package/tailwind.config.js +8 -0
- package/tsconfig.json +16 -0
- package/vite.config.ts +19 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
const MAX_CHARS = 500;
|
|
4
|
+
const MAX_POLL_OPTION_CHARS = 25;
|
|
5
|
+
|
|
6
|
+
interface Account {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
handle: string;
|
|
10
|
+
platform: string;
|
|
11
|
+
style: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface PublishDialogProps {
|
|
15
|
+
isOpen: boolean;
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
initialText?: string;
|
|
18
|
+
sessionId?: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function PublishDialog({
|
|
22
|
+
isOpen,
|
|
23
|
+
onClose,
|
|
24
|
+
initialText = "",
|
|
25
|
+
sessionId,
|
|
26
|
+
}: PublishDialogProps) {
|
|
27
|
+
const [accounts, setAccounts] = useState<Account[]>([]);
|
|
28
|
+
const [selectedAccountId, setSelectedAccountId] = useState("");
|
|
29
|
+
const [text, setText] = useState(initialText);
|
|
30
|
+
const [publishing, setPublishing] = useState(false);
|
|
31
|
+
const [result, setResult] = useState<{ success: boolean; message: string } | null>(null);
|
|
32
|
+
|
|
33
|
+
// Advanced options
|
|
34
|
+
const [imageUrl, setImageUrl] = useState("");
|
|
35
|
+
const [pollEnabled, setPollEnabled] = useState(false);
|
|
36
|
+
const [pollOptions, setPollOptions] = useState(["", ""]);
|
|
37
|
+
const [tag, setTag] = useState("");
|
|
38
|
+
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!isOpen) return;
|
|
42
|
+
setText(initialText);
|
|
43
|
+
setResult(null);
|
|
44
|
+
setPublishing(false);
|
|
45
|
+
setImageUrl("");
|
|
46
|
+
setPollEnabled(false);
|
|
47
|
+
setPollOptions(["", ""]);
|
|
48
|
+
setTag("");
|
|
49
|
+
setShowAdvanced(false);
|
|
50
|
+
|
|
51
|
+
fetch("/api/accounts")
|
|
52
|
+
.then((r) => r.ok ? r.json() : [])
|
|
53
|
+
.then((data: Account[]) => {
|
|
54
|
+
setAccounts(data);
|
|
55
|
+
if (data.length > 0 && !selectedAccountId) {
|
|
56
|
+
setSelectedAccountId(data[0].id);
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
.catch(() => setAccounts([]));
|
|
60
|
+
}, [isOpen]);
|
|
61
|
+
|
|
62
|
+
if (!isOpen) return null;
|
|
63
|
+
|
|
64
|
+
const isOverLimit = text.length > MAX_CHARS;
|
|
65
|
+
const selectedAccount = accounts.find((a) => a.id === selectedAccountId);
|
|
66
|
+
|
|
67
|
+
// Poll validation
|
|
68
|
+
const validPollOptions = pollOptions.filter((o) => o.trim());
|
|
69
|
+
const isPollValid = !pollEnabled || (validPollOptions.length >= 2 && pollOptions.every((o) => o.length <= MAX_POLL_OPTION_CHARS));
|
|
70
|
+
const hasPollOverLength = pollEnabled && pollOptions.some((o) => o.length > MAX_POLL_OPTION_CHARS);
|
|
71
|
+
|
|
72
|
+
// Advanced options badge
|
|
73
|
+
const advancedBadges: string[] = [];
|
|
74
|
+
if (imageUrl.trim()) advancedBadges.push("img");
|
|
75
|
+
if (tag.trim()) advancedBadges.push("tag");
|
|
76
|
+
if (pollEnabled && validPollOptions.length >= 2) advancedBadges.push("poll");
|
|
77
|
+
|
|
78
|
+
const handlePublish = async () => {
|
|
79
|
+
if (!text.trim() || !selectedAccountId) return;
|
|
80
|
+
setPublishing(true);
|
|
81
|
+
setResult(null);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const res = await fetch("/api/publish", {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { "Content-Type": "application/json" },
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
accountId: selectedAccountId,
|
|
89
|
+
text,
|
|
90
|
+
sessionId,
|
|
91
|
+
imageUrl: imageUrl.trim() || undefined,
|
|
92
|
+
pollOptions: pollEnabled ? pollOptions.filter((o) => o.trim()).join("|") : undefined,
|
|
93
|
+
tag: tag.trim() || undefined,
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
const data = await res.json();
|
|
97
|
+
|
|
98
|
+
if (data.success) {
|
|
99
|
+
setResult({
|
|
100
|
+
success: true,
|
|
101
|
+
message: `Published! ${data.postUrl ? `URL: ${data.postUrl}` : `ID: ${data.postId}`}`,
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
setResult({ success: false, message: data.error || "Publish failed" });
|
|
105
|
+
}
|
|
106
|
+
} catch (err) {
|
|
107
|
+
setResult({ success: false, message: (err as Error).message });
|
|
108
|
+
}
|
|
109
|
+
setPublishing(false);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
114
|
+
<div className="bg-white rounded-lg shadow-xl w-full max-w-lg mx-4 max-h-[90vh] overflow-y-auto">
|
|
115
|
+
{/* Header */}
|
|
116
|
+
<div className="px-4 py-3 border-b border-gray-200 flex items-center justify-between">
|
|
117
|
+
<h3 className="font-semibold text-gray-800">Publish to Social</h3>
|
|
118
|
+
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-lg leading-none">×</button>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Body */}
|
|
122
|
+
<div className="p-4 space-y-4">
|
|
123
|
+
{/* Account selector */}
|
|
124
|
+
{accounts.length === 0 ? (
|
|
125
|
+
<div className="p-3 bg-amber-50 border border-amber-200 rounded-lg text-sm text-amber-700">
|
|
126
|
+
No accounts configured. Add accounts in Settings.
|
|
127
|
+
</div>
|
|
128
|
+
) : (
|
|
129
|
+
<div>
|
|
130
|
+
<label className="text-sm font-medium text-gray-700 block mb-1">
|
|
131
|
+
Account
|
|
132
|
+
</label>
|
|
133
|
+
<select
|
|
134
|
+
value={selectedAccountId}
|
|
135
|
+
onChange={(e) => setSelectedAccountId(e.target.value)}
|
|
136
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
137
|
+
>
|
|
138
|
+
{accounts.map((a) => (
|
|
139
|
+
<option key={a.id} value={a.id}>
|
|
140
|
+
{a.handle} ({a.platform}) {a.style ? `- ${a.style}` : ""}
|
|
141
|
+
</option>
|
|
142
|
+
))}
|
|
143
|
+
</select>
|
|
144
|
+
{selectedAccount?.style && (
|
|
145
|
+
<div className="text-xs text-gray-400 mt-1">
|
|
146
|
+
Style: {selectedAccount.style}
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
{/* Content */}
|
|
153
|
+
<div>
|
|
154
|
+
<label className="text-sm font-medium text-gray-700 block mb-1">
|
|
155
|
+
Content
|
|
156
|
+
</label>
|
|
157
|
+
<textarea
|
|
158
|
+
value={text}
|
|
159
|
+
onChange={(e) => setText(e.target.value)}
|
|
160
|
+
rows={5}
|
|
161
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
|
162
|
+
placeholder="Enter your post content..."
|
|
163
|
+
/>
|
|
164
|
+
<div className={`text-xs mt-1 text-right ${isOverLimit ? "text-red-500 font-medium" : "text-gray-400"}`}>
|
|
165
|
+
{text.length} / {MAX_CHARS} chars
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
{/* Advanced Options Toggle */}
|
|
170
|
+
<div>
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
onClick={() => setShowAdvanced(!showAdvanced)}
|
|
174
|
+
className="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-800 transition-colors"
|
|
175
|
+
>
|
|
176
|
+
<span className="text-xs">{showAdvanced ? "\u25BC" : "\u25B6"}</span>
|
|
177
|
+
<span>Advanced Options</span>
|
|
178
|
+
{!showAdvanced && advancedBadges.length > 0 && (
|
|
179
|
+
<span className="text-xs bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded">
|
|
180
|
+
{advancedBadges.join(" \u00B7 ")}
|
|
181
|
+
</span>
|
|
182
|
+
)}
|
|
183
|
+
</button>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
{/* Advanced Options Panel */}
|
|
187
|
+
{showAdvanced && (
|
|
188
|
+
<div className="space-y-4 pl-2 border-l-2 border-gray-100">
|
|
189
|
+
{/* Image URL */}
|
|
190
|
+
<div>
|
|
191
|
+
<label className="text-sm font-medium text-gray-700 block mb-1">
|
|
192
|
+
Image URL
|
|
193
|
+
</label>
|
|
194
|
+
<input
|
|
195
|
+
type="url"
|
|
196
|
+
value={imageUrl}
|
|
197
|
+
onChange={(e) => setImageUrl(e.target.value)}
|
|
198
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
199
|
+
placeholder="https://example.com/image.png"
|
|
200
|
+
/>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
{/* Poll */}
|
|
204
|
+
<div>
|
|
205
|
+
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 mb-2 cursor-pointer">
|
|
206
|
+
<input
|
|
207
|
+
type="checkbox"
|
|
208
|
+
checked={pollEnabled}
|
|
209
|
+
onChange={(e) => setPollEnabled(e.target.checked)}
|
|
210
|
+
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
211
|
+
/>
|
|
212
|
+
Poll
|
|
213
|
+
</label>
|
|
214
|
+
{pollEnabled && (
|
|
215
|
+
<div className="space-y-2">
|
|
216
|
+
{pollOptions.map((opt, i) => (
|
|
217
|
+
<div key={i} className="flex items-center gap-2">
|
|
218
|
+
<span className="text-xs text-gray-400 w-4">{String.fromCharCode(65 + i)}</span>
|
|
219
|
+
<input
|
|
220
|
+
type="text"
|
|
221
|
+
value={opt}
|
|
222
|
+
onChange={(e) => {
|
|
223
|
+
const next = [...pollOptions];
|
|
224
|
+
next[i] = e.target.value;
|
|
225
|
+
setPollOptions(next);
|
|
226
|
+
}}
|
|
227
|
+
maxLength={MAX_POLL_OPTION_CHARS}
|
|
228
|
+
className="flex-1 px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
229
|
+
placeholder={`Option ${String.fromCharCode(65 + i)}`}
|
|
230
|
+
/>
|
|
231
|
+
<span className={`text-xs w-8 text-right ${opt.length > MAX_POLL_OPTION_CHARS ? "text-red-500" : "text-gray-400"}`}>
|
|
232
|
+
{opt.length}/{MAX_POLL_OPTION_CHARS}
|
|
233
|
+
</span>
|
|
234
|
+
{pollOptions.length > 2 && (
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
onClick={() => setPollOptions(pollOptions.filter((_, j) => j !== i))}
|
|
238
|
+
className="text-gray-400 hover:text-red-500 text-sm"
|
|
239
|
+
>
|
|
240
|
+
×
|
|
241
|
+
</button>
|
|
242
|
+
)}
|
|
243
|
+
</div>
|
|
244
|
+
))}
|
|
245
|
+
{pollOptions.length < 4 && (
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
onClick={() => setPollOptions([...pollOptions, ""])}
|
|
249
|
+
className="text-xs text-blue-600 hover:text-blue-800"
|
|
250
|
+
>
|
|
251
|
+
+ Add option
|
|
252
|
+
</button>
|
|
253
|
+
)}
|
|
254
|
+
{pollEnabled && validPollOptions.length < 2 && (
|
|
255
|
+
<div className="text-xs text-amber-600">At least 2 non-empty options required</div>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
{/* Topic Tag */}
|
|
262
|
+
<div>
|
|
263
|
+
<label className="text-sm font-medium text-gray-700 block mb-1">
|
|
264
|
+
Topic Tag
|
|
265
|
+
</label>
|
|
266
|
+
<div className="flex items-center gap-1">
|
|
267
|
+
<span className="text-gray-400 text-sm">#</span>
|
|
268
|
+
<input
|
|
269
|
+
type="text"
|
|
270
|
+
value={tag}
|
|
271
|
+
onChange={(e) => setTag(e.target.value.replace(/^#/, ""))}
|
|
272
|
+
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
273
|
+
placeholder="AI"
|
|
274
|
+
/>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
|
|
280
|
+
{/* Result */}
|
|
281
|
+
{result && (
|
|
282
|
+
<div className={`p-3 rounded-lg text-sm ${result.success ? "bg-green-50 text-green-700" : "bg-red-50 text-red-700"}`}>
|
|
283
|
+
{result.message}
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
{/* Footer */}
|
|
289
|
+
<div className="px-4 py-3 border-t border-gray-200 flex justify-end gap-2">
|
|
290
|
+
<button
|
|
291
|
+
onClick={onClose}
|
|
292
|
+
className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
|
293
|
+
>
|
|
294
|
+
Cancel
|
|
295
|
+
</button>
|
|
296
|
+
<button
|
|
297
|
+
onClick={handlePublish}
|
|
298
|
+
disabled={!text.trim() || isOverLimit || !selectedAccountId || publishing || !isPollValid}
|
|
299
|
+
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
300
|
+
>
|
|
301
|
+
{publishing ? "Publishing..." : "Publish"}
|
|
302
|
+
</button>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
}
|