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.
Files changed (46) hide show
  1. package/.env.example +30 -0
  2. package/.mcp.json +51 -0
  3. package/README.md +224 -0
  4. package/client/App.tsx +446 -0
  5. package/client/components/ChatWindow.tsx +790 -0
  6. package/client/components/FileExplorer.tsx +218 -0
  7. package/client/components/FilePreviewModal.tsx +179 -0
  8. package/client/components/PublishDialog.tsx +307 -0
  9. package/client/components/SettingsPage.tsx +452 -0
  10. package/client/components/Sidebar.tsx +198 -0
  11. package/client/components/ToolUseBlock.tsx +140 -0
  12. package/client/index.html +12 -0
  13. package/client/index.tsx +10 -0
  14. package/client/styles/globals.css +48 -0
  15. package/demo/01-welcome.png +0 -0
  16. package/demo/02-pipeline-cards.png +0 -0
  17. package/demo/03-custom-topic-fill.png +0 -0
  18. package/demo/04-topic-typed.png +0 -0
  19. package/demo/05-loading-state.png +0 -0
  20. package/demo/06-tool-calls.png +0 -0
  21. package/demo/07-history-rich.png +0 -0
  22. package/demo/09-en-cards.png +0 -0
  23. package/demo/10-ja-cards.png +0 -0
  24. package/demo/capture-remaining.mjs +73 -0
  25. package/demo/capture.mjs +110 -0
  26. package/demo/demo-walkthrough-2.webm +0 -0
  27. package/demo/demo-walkthrough.webm +0 -0
  28. package/package.json +48 -0
  29. package/postcss.config.js +6 -0
  30. package/scripts/threads_api.py +536 -0
  31. package/server/ai-client.ts +356 -0
  32. package/server/db.ts +299 -0
  33. package/server/mcp-config.ts +85 -0
  34. package/server/routes/accounts.ts +88 -0
  35. package/server/routes/files.ts +175 -0
  36. package/server/routes/publish.ts +77 -0
  37. package/server/routes/sessions.ts +59 -0
  38. package/server/routes/settings.ts +220 -0
  39. package/server/server.ts +261 -0
  40. package/server/services/social-publisher.ts +74 -0
  41. package/server/services/studio-mcp.ts +107 -0
  42. package/server/session.ts +167 -0
  43. package/server/types.ts +86 -0
  44. package/tailwind.config.js +8 -0
  45. package/tsconfig.json +16 -0
  46. package/vite.config.ts +19 -0
@@ -0,0 +1,218 @@
1
+ import React, { useState, useEffect } from "react";
2
+
3
+ interface FileEntry {
4
+ name: string;
5
+ path: string;
6
+ type: "file" | "directory";
7
+ size?: number;
8
+ modified?: string;
9
+ children?: FileEntry[];
10
+ }
11
+
12
+ interface FileExplorerProps {
13
+ sessionId: string | null;
14
+ isVisible: boolean;
15
+ onToggle: () => void;
16
+ onPreviewFile: (relativePath: string) => void;
17
+ }
18
+
19
+ const IMAGE_EXTS = ["png", "jpg", "jpeg", "gif", "webp", "svg"];
20
+
21
+ function FileIcon({ type, name }: { type: string; name: string }) {
22
+ if (type === "directory") return <span className="text-yellow-500">D</span>;
23
+ const ext = name.split(".").pop()?.toLowerCase();
24
+ switch (ext) {
25
+ case "ts":
26
+ case "tsx":
27
+ return <span className="text-blue-400">T</span>;
28
+ case "js":
29
+ case "jsx":
30
+ return <span className="text-yellow-400">J</span>;
31
+ case "py":
32
+ return <span className="text-green-400">P</span>;
33
+ case "md":
34
+ return <span className="text-gray-400">M</span>;
35
+ case "json":
36
+ return <span className="text-orange-400">{"{}"}</span>;
37
+ case "png":
38
+ case "jpg":
39
+ case "jpeg":
40
+ case "gif":
41
+ case "webp":
42
+ case "svg":
43
+ return <span className="text-pink-400">I</span>;
44
+ case "pdf":
45
+ return <span className="text-red-400">P</span>;
46
+ case "mp3":
47
+ case "wav":
48
+ case "m4a":
49
+ return <span className="text-purple-400">A</span>;
50
+ case "mp4":
51
+ case "webm":
52
+ return <span className="text-indigo-400">V</span>;
53
+ default:
54
+ return <span className="text-gray-400">F</span>;
55
+ }
56
+ }
57
+
58
+ function formatSize(bytes: number): string {
59
+ if (bytes < 1024) return bytes + "B";
60
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + "KB";
61
+ return (bytes / (1024 * 1024)).toFixed(1) + "MB";
62
+ }
63
+
64
+ function FileTreeNode({
65
+ entry,
66
+ depth,
67
+ onSelect,
68
+ sessionId,
69
+ }: {
70
+ entry: FileEntry;
71
+ depth: number;
72
+ onSelect: (path: string) => void;
73
+ sessionId: string;
74
+ }) {
75
+ const [isOpen, setIsOpen] = useState(depth < 1);
76
+ const ext = entry.name.split(".").pop()?.toLowerCase() || "";
77
+ const isImage = IMAGE_EXTS.includes(ext);
78
+
79
+ return (
80
+ <div>
81
+ <button
82
+ type="button"
83
+ aria-expanded={entry.type === "directory" ? isOpen : undefined}
84
+ className="w-full flex items-center gap-1.5 py-1 px-1.5 hover:bg-gray-100 rounded cursor-pointer text-xs group text-left"
85
+ style={{ paddingLeft: depth * 14 + 6 }}
86
+ onClick={() => {
87
+ if (entry.type === "directory") {
88
+ setIsOpen(!isOpen);
89
+ } else {
90
+ onSelect(entry.path);
91
+ }
92
+ }}
93
+ >
94
+ {entry.type === "directory" && (
95
+ <span className="text-gray-400 w-3 text-center text-[10px]">
96
+ {isOpen ? "▼" : "▶"}
97
+ </span>
98
+ )}
99
+ <FileIcon type={entry.type} name={entry.name} />
100
+ <span className="truncate flex-1">{entry.name}</span>
101
+ {entry.type === "file" && entry.size != null && (
102
+ <span className="text-gray-400 text-[10px] shrink-0">
103
+ {formatSize(entry.size)}
104
+ </span>
105
+ )}
106
+ </button>
107
+
108
+ {/* Inline image thumbnail for image files */}
109
+ {entry.type === "file" && isImage && (
110
+ <button
111
+ type="button"
112
+ className="ml-8 mr-2 my-1 cursor-pointer block"
113
+ onClick={() => onSelect(entry.path)}
114
+ aria-label={`Preview ${entry.name}`}
115
+ >
116
+ <img
117
+ src={`/api/sessions/${encodeURIComponent(sessionId)}/files/${entry.path.split("/").map(encodeURIComponent).join("/")}`}
118
+ alt={entry.name}
119
+ className="max-h-16 rounded border border-gray-200 hover:border-blue-300 transition-colors"
120
+ loading="lazy"
121
+ />
122
+ </button>
123
+ )}
124
+
125
+ {entry.type === "directory" && isOpen && entry.children && (
126
+ <div>
127
+ {entry.children.map((child) => (
128
+ <FileTreeNode
129
+ key={child.path}
130
+ entry={child}
131
+ depth={depth + 1}
132
+ onSelect={onSelect}
133
+ sessionId={sessionId}
134
+ />
135
+ ))}
136
+ </div>
137
+ )}
138
+ </div>
139
+ );
140
+ }
141
+
142
+ export function FileExplorer({ sessionId, isVisible, onToggle, onPreviewFile }: FileExplorerProps) {
143
+ const [tree, setTree] = useState<FileEntry[]>([]);
144
+ const [workspace, setWorkspace] = useState("");
145
+ const [loadingTree, setLoadingTree] = useState(false);
146
+
147
+ useEffect(() => {
148
+ if (!sessionId || !isVisible) return;
149
+
150
+ const controller = new AbortController();
151
+ setLoadingTree(true);
152
+
153
+ fetch(`/api/sessions/${encodeURIComponent(sessionId)}/files`, {
154
+ signal: controller.signal,
155
+ })
156
+ .then((res) => {
157
+ if (!res.ok) throw new Error("Failed to load");
158
+ return res.json();
159
+ })
160
+ .then((data) => {
161
+ setTree(data.tree || []);
162
+ setWorkspace(data.workspace || "");
163
+ })
164
+ .catch((err) => {
165
+ if (err.name !== "AbortError") {
166
+ console.error("Failed to load file tree:", err);
167
+ }
168
+ })
169
+ .finally(() => setLoadingTree(false));
170
+
171
+ return () => controller.abort();
172
+ }, [sessionId, isVisible]);
173
+
174
+ if (!isVisible) return null;
175
+
176
+ return (
177
+ <div className="w-96 border-l border-gray-200 flex flex-col bg-white shrink-0">
178
+ {/* Header */}
179
+ <div className="px-3 py-2 border-b border-gray-200 flex items-center justify-between">
180
+ <h3 className="text-xs font-semibold text-gray-600 uppercase">Files</h3>
181
+ <button
182
+ onClick={onToggle}
183
+ className="text-xs text-gray-400 hover:text-gray-600"
184
+ >
185
+ Close
186
+ </button>
187
+ </div>
188
+
189
+ {/* Workspace path */}
190
+ <div className="px-3 py-1 text-[10px] text-gray-400 truncate border-b border-gray-100">
191
+ {workspace}
192
+ </div>
193
+
194
+ {/* Tree */}
195
+ <div role="list" aria-label="File explorer" className="flex-1 overflow-y-auto p-1.5">
196
+ {loadingTree ? (
197
+ <div className="text-xs text-gray-400 text-center mt-4">
198
+ Loading...
199
+ </div>
200
+ ) : tree.length === 0 ? (
201
+ <div className="text-xs text-gray-400 text-center mt-4">
202
+ No files found
203
+ </div>
204
+ ) : (
205
+ tree.map((entry) => (
206
+ <FileTreeNode
207
+ key={entry.path}
208
+ entry={entry}
209
+ depth={0}
210
+ onSelect={onPreviewFile}
211
+ sessionId={sessionId!}
212
+ />
213
+ ))
214
+ )}
215
+ </div>
216
+ </div>
217
+ );
218
+ }
@@ -0,0 +1,179 @@
1
+ import React, { useState, useEffect } from "react";
2
+
3
+ interface FilePreviewModalProps {
4
+ sessionId: string;
5
+ filePath: string; // relative path from workspace
6
+ onClose: () => void;
7
+ }
8
+
9
+ const IMAGE_EXTS = ["png", "jpg", "jpeg", "gif", "webp", "svg"];
10
+
11
+ /** Reject paths containing ".." traversal segments; strip "." */
12
+ function sanitizePath(p: string): string | null {
13
+ const segs = p.split("/").filter((s) => s !== "" && s !== ".");
14
+ if (segs.some((s) => s === "..")) return null; // reject traversal
15
+ return segs.join("/");
16
+ }
17
+
18
+ export function FilePreviewModal({ sessionId, filePath, onClose }: FilePreviewModalProps) {
19
+ const [content, setContent] = useState<string | null>(null);
20
+ const [loading, setLoading] = useState(true);
21
+ const [fileType, setFileType] = useState<"image" | "pdf" | "audio" | "video" | "text" | "binary">("text");
22
+
23
+ const safePath = sanitizePath(filePath);
24
+
25
+ // Reject traversal paths immediately
26
+ if (!safePath) {
27
+ return (
28
+ <div className="fixed inset-0 z-50 bg-black/60 flex items-center justify-center p-6" onClick={onClose}>
29
+ <div role="dialog" aria-modal="true" className="bg-white rounded-xl shadow-2xl p-8 text-center">
30
+ <p className="text-red-600 font-medium">Invalid file path (traversal rejected)</p>
31
+ <button onClick={onClose} className="mt-4 px-4 py-2 bg-gray-200 rounded-lg text-sm">Close</button>
32
+ </div>
33
+ </div>
34
+ );
35
+ }
36
+
37
+ const ext = safePath.split(".").pop()?.toLowerCase() || "";
38
+ const encodedPath = safePath.split("/").map(encodeURIComponent).join("/");
39
+ const fileUrl = `/api/sessions/${encodeURIComponent(sessionId)}/files/${encodedPath}`;
40
+
41
+ useEffect(() => {
42
+ const controller = new AbortController();
43
+
44
+ const load = async () => {
45
+ setLoading(true);
46
+
47
+ if (IMAGE_EXTS.includes(ext)) {
48
+ setFileType("image");
49
+ setContent(fileUrl);
50
+ setLoading(false);
51
+ return;
52
+ }
53
+
54
+ if (ext === "pdf") {
55
+ setFileType("pdf");
56
+ setContent(fileUrl);
57
+ setLoading(false);
58
+ return;
59
+ }
60
+
61
+ if (["mp3", "wav", "m4a", "ogg"].includes(ext)) {
62
+ setFileType("audio");
63
+ setContent(fileUrl);
64
+ setLoading(false);
65
+ return;
66
+ }
67
+
68
+ if (["mp4", "webm"].includes(ext)) {
69
+ setFileType("video");
70
+ setContent(fileUrl);
71
+ setLoading(false);
72
+ return;
73
+ }
74
+
75
+ try {
76
+ const res = await fetch(fileUrl, { signal: controller.signal });
77
+ if (!res.ok) {
78
+ const err = await res.json().catch(() => ({}));
79
+ setFileType("text");
80
+ setContent(`[Error: ${err.error || res.statusText}]`);
81
+ setLoading(false);
82
+ return;
83
+ }
84
+ if (res.headers.get("content-type")?.includes("json")) {
85
+ const data = await res.json();
86
+ setFileType("text");
87
+ setContent(data.content ?? "[No content]");
88
+ } else {
89
+ setFileType("binary");
90
+ setContent(fileUrl);
91
+ }
92
+ } catch (err) {
93
+ if (err instanceof DOMException && err.name === "AbortError") return;
94
+ setFileType("text");
95
+ setContent("[Error loading file]");
96
+ }
97
+ setLoading(false);
98
+ };
99
+
100
+ load();
101
+ return () => controller.abort();
102
+ }, [sessionId, filePath]);
103
+
104
+ useEffect(() => {
105
+ const handleEsc = (e: KeyboardEvent) => {
106
+ if (e.key === "Escape") onClose();
107
+ };
108
+ window.addEventListener("keydown", handleEsc);
109
+ return () => window.removeEventListener("keydown", handleEsc);
110
+ }, [onClose]);
111
+
112
+ const fileName = safePath.split("/").pop() || safePath;
113
+
114
+ return (
115
+ <div
116
+ className="fixed inset-0 z-50 bg-black/60 flex items-center justify-center p-6"
117
+ onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
118
+ >
119
+ <div role="dialog" aria-modal="true" aria-label={fileName} className="bg-white rounded-xl shadow-2xl max-w-5xl w-full max-h-[90vh] flex flex-col overflow-hidden">
120
+ {/* Header */}
121
+ <div className="px-4 py-3 border-b border-gray-200 flex items-center justify-between shrink-0">
122
+ <div className="flex items-center gap-2 min-w-0">
123
+ <span className="text-xs font-mono px-2 py-0.5 rounded bg-gray-100 text-gray-500 uppercase shrink-0">
124
+ {ext}
125
+ </span>
126
+ <span className="text-sm font-medium text-gray-700 truncate">{fileName}</span>
127
+ <span className="text-xs text-gray-400 truncate hidden sm:inline">{safePath}</span>
128
+ </div>
129
+ <button
130
+ onClick={onClose}
131
+ className="text-gray-400 hover:text-gray-600 text-lg px-2 shrink-0"
132
+ >
133
+
134
+ </button>
135
+ </div>
136
+
137
+ {/* Content */}
138
+ <div className="flex-1 overflow-auto">
139
+ {loading ? (
140
+ <div className="text-center text-gray-400 py-16">Loading...</div>
141
+ ) : fileType === "image" ? (
142
+ <div className="flex items-center justify-center p-6 bg-gray-50">
143
+ <img
144
+ src={content!}
145
+ alt={fileName}
146
+ className="max-w-full max-h-[75vh] rounded shadow-lg"
147
+ />
148
+ </div>
149
+ ) : fileType === "pdf" ? (
150
+ <iframe src={content!} className="w-full h-[80vh]" title={fileName} />
151
+ ) : fileType === "audio" ? (
152
+ <div className="flex items-center justify-center p-12">
153
+ <audio controls src={content!} className="w-full max-w-lg" />
154
+ </div>
155
+ ) : fileType === "video" ? (
156
+ <div className="flex items-center justify-center p-6 bg-black">
157
+ <video controls src={content!} className="max-w-full max-h-[75vh]" />
158
+ </div>
159
+ ) : fileType === "binary" ? (
160
+ <div className="text-center py-16">
161
+ <p className="text-gray-500 mb-3">Binary file — preview not available</p>
162
+ <a
163
+ href={content!}
164
+ download={fileName}
165
+ className="inline-flex items-center gap-1.5 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-500 text-sm"
166
+ >
167
+ Download {fileName}
168
+ </a>
169
+ </div>
170
+ ) : (
171
+ <pre className="text-sm text-gray-700 whitespace-pre-wrap break-words font-mono p-6 leading-relaxed">
172
+ {content}
173
+ </pre>
174
+ )}
175
+ </div>
176
+ </div>
177
+ </div>
178
+ );
179
+ }