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,140 @@
1
+ import React, { useState } from "react";
2
+
3
+ interface ToolUseBlockProps {
4
+ toolName: string;
5
+ toolInput: Record<string, unknown>;
6
+ toolId: string;
7
+ onPreviewFile?: (absolutePath: string) => void;
8
+ }
9
+
10
+ // Color mapping for MCP server tools
11
+ function getToolColor(name: string): string {
12
+ if (name.startsWith("mcp__trend-pulse") || name.startsWith("mcp__trend_pulse")) {
13
+ return "border-emerald-300 bg-emerald-50";
14
+ }
15
+ if (name.startsWith("mcp__cf-browser") || name.startsWith("mcp__cf_browser")) {
16
+ return "border-blue-300 bg-blue-50";
17
+ }
18
+ if (name.startsWith("mcp__notebooklm")) {
19
+ return "border-purple-300 bg-purple-50";
20
+ }
21
+ return "border-gray-200 bg-gray-50";
22
+ }
23
+
24
+ function getToolBadge(name: string): { label: string; color: string } | null {
25
+ if (name.startsWith("mcp__trend-pulse") || name.startsWith("mcp__trend_pulse")) {
26
+ return { label: "trend-pulse", color: "bg-emerald-100 text-emerald-700" };
27
+ }
28
+ if (name.startsWith("mcp__cf-browser") || name.startsWith("mcp__cf_browser")) {
29
+ return { label: "cf-browser", color: "bg-blue-100 text-blue-700" };
30
+ }
31
+ if (name.startsWith("mcp__notebooklm")) {
32
+ return { label: "notebooklm", color: "bg-purple-100 text-purple-700" };
33
+ }
34
+ return null;
35
+ }
36
+
37
+ function getToolDisplayName(name: string): string {
38
+ return name
39
+ .replace(/^mcp__(trend[-_]pulse|cf[-_]browser|notebooklm)__/, "")
40
+ .replace(/_/g, " ");
41
+ }
42
+
43
+ function str(val: unknown): string {
44
+ return typeof val === "string" ? val : "";
45
+ }
46
+
47
+ function getToolSummary(name: string, input: Record<string, unknown>): string {
48
+ switch (name) {
49
+ case "Read":
50
+ case "Write":
51
+ case "Edit":
52
+ return str(input.file_path);
53
+ case "Bash": {
54
+ const cmd = str(input.command);
55
+ return cmd.length > 80 ? cmd.slice(0, 80) + "..." : cmd;
56
+ }
57
+ case "Grep":
58
+ return `"${str(input.pattern)}" in ${str(input.path) || "."}`;
59
+ case "Glob":
60
+ return str(input.pattern);
61
+ case "WebSearch":
62
+ return str(input.query);
63
+ case "WebFetch":
64
+ return str(input.url);
65
+ }
66
+
67
+ if (input.topic) return str(input.topic);
68
+ if (input.query) return str(input.query);
69
+ if (input.url) return str(input.url);
70
+ if (input.keyword) return str(input.keyword);
71
+ if (input.sources) return `sources: ${str(input.sources)}`;
72
+
73
+ return JSON.stringify(input).slice(0, 60);
74
+ }
75
+
76
+ const PREVIEWABLE_EXTS = [
77
+ "png", "jpg", "jpeg", "gif", "webp", "svg",
78
+ "pdf", "md", "txt", "json", "ts", "tsx", "js", "jsx", "py", "html", "css",
79
+ "mp3", "wav", "m4a", "mp4", "webm",
80
+ ];
81
+
82
+ function isPreviewable(filePath: string): boolean {
83
+ const ext = filePath.split(".").pop()?.toLowerCase() || "";
84
+ return PREVIEWABLE_EXTS.includes(ext);
85
+ }
86
+
87
+ export function ToolUseBlock({ toolName, toolInput, toolId, onPreviewFile }: ToolUseBlockProps) {
88
+ const [isExpanded, setIsExpanded] = useState(false);
89
+ const colorClass = getToolColor(toolName);
90
+ const badge = getToolBadge(toolName);
91
+
92
+ const filePath = toolInput.file_path as string | undefined;
93
+ const canPreview = onPreviewFile && filePath && isPreviewable(filePath) &&
94
+ (toolName === "Write" || toolName === "Read" || toolName === "Edit");
95
+
96
+ return (
97
+ <div className={`my-2 border rounded ${colorClass}`}>
98
+ <div className="p-2 flex items-center justify-between">
99
+ {/* Collapse toggle — clickable label area */}
100
+ <div
101
+ role="button"
102
+ tabIndex={0}
103
+ onClick={() => setIsExpanded(!isExpanded)}
104
+ onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setIsExpanded(!isExpanded); } }}
105
+ className="flex items-center gap-2 min-w-0 cursor-pointer hover:opacity-80 flex-1"
106
+ >
107
+ <span className="text-xs text-gray-400 shrink-0">{isExpanded ? "▼" : "▶"}</span>
108
+ {badge && (
109
+ <span className={`text-[10px] font-medium px-1.5 py-0.5 rounded ${badge.color}`}>
110
+ {badge.label}
111
+ </span>
112
+ )}
113
+ <span className="text-xs font-semibold text-gray-600 uppercase shrink-0">
114
+ {getToolDisplayName(toolName)}
115
+ </span>
116
+ <span className="text-xs text-gray-500 truncate">
117
+ {getToolSummary(toolName, toolInput)}
118
+ </span>
119
+ </div>
120
+ {/* Preview button — separate from toggle, no nesting */}
121
+ {canPreview && (
122
+ <button
123
+ type="button"
124
+ onClick={() => onPreviewFile!(filePath!)}
125
+ className="text-[10px] px-2 py-0.5 rounded bg-blue-100 text-blue-600 hover:bg-blue-200 transition-colors shrink-0 ml-2"
126
+ >
127
+ Preview
128
+ </button>
129
+ )}
130
+ </div>
131
+ {isExpanded && (
132
+ <div className="p-2 border-t border-gray-200/50">
133
+ <pre className="text-xs bg-white p-2 rounded overflow-x-auto max-h-64 overflow-y-auto">
134
+ {JSON.stringify(toolInput, null, 2)}
135
+ </pre>
136
+ </div>
137
+ )}
138
+ </div>
139
+ );
140
+ }
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Claude World Studio</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/index.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import App from "./App";
4
+ import "./styles/globals.css";
5
+
6
+ const container = document.getElementById("root");
7
+ if (container) {
8
+ const root = createRoot(container);
9
+ root.render(<App />);
10
+ }
@@ -0,0 +1,48 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /* Custom scrollbar */
6
+ ::-webkit-scrollbar {
7
+ width: 6px;
8
+ }
9
+ ::-webkit-scrollbar-track {
10
+ background: transparent;
11
+ }
12
+ ::-webkit-scrollbar-thumb {
13
+ background: #4b5563;
14
+ border-radius: 3px;
15
+ }
16
+ ::-webkit-scrollbar-thumb:hover {
17
+ background: #6b7280;
18
+ }
19
+
20
+ /* Pulse dot animation */
21
+ @keyframes pulse-dot {
22
+ 0%, 100% { opacity: 1; }
23
+ 50% { opacity: 0.3; }
24
+ }
25
+ .animate-pulse-dot {
26
+ animation: pulse-dot 1.5s ease-in-out infinite;
27
+ }
28
+
29
+ /* Typing indicator */
30
+ @keyframes typing {
31
+ 0% { opacity: 0.3; }
32
+ 20% { opacity: 1; }
33
+ 100% { opacity: 0.3; }
34
+ }
35
+ .typing-dot:nth-child(1) { animation: typing 1.4s infinite 0s; }
36
+ .typing-dot:nth-child(2) { animation: typing 1.4s infinite 0.2s; }
37
+ .typing-dot:nth-child(3) { animation: typing 1.4s infinite 0.4s; }
38
+
39
+ /* Workflow step connector */
40
+ .workflow-step + .workflow-step::before {
41
+ content: '';
42
+ position: absolute;
43
+ left: 50%;
44
+ top: -12px;
45
+ width: 2px;
46
+ height: 12px;
47
+ background: #d1d5db;
48
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,73 @@
1
+ import { chromium } from "playwright";
2
+ import { setTimeout } from "timers/promises";
3
+
4
+ const BASE = "http://localhost:5173";
5
+ const OUT = new URL(".", import.meta.url).pathname;
6
+
7
+ (async () => {
8
+ const browser = await chromium.launch({ headless: true });
9
+ const context = await browser.newContext({
10
+ viewport: { width: 1440, height: 900 },
11
+ recordVideo: { dir: OUT, size: { width: 1440, height: 900 } },
12
+ });
13
+ const page = await context.newPage();
14
+
15
+ // Wait for server
16
+ for (let i = 0; i < 30; i++) {
17
+ try { await page.goto(BASE, { timeout: 3000 }); break; }
18
+ catch { console.log(`Waiting... (${i + 1})`); await setTimeout(2000); }
19
+ }
20
+ await setTimeout(2000);
21
+
22
+ // Click existing session with rich content (Claude Code 4.6)
23
+ const session46 = page.locator('[class*="cursor-pointer"]').filter({ hasText: "Claude Code 4.6" }).first();
24
+ if (await session46.count() > 0) {
25
+ await session46.click();
26
+ await setTimeout(2000);
27
+ await page.screenshot({ path: `${OUT}07-history-rich.png`, fullPage: true });
28
+ console.log("✓ 07-history-rich.png");
29
+
30
+ // Open file explorer
31
+ const filesBtn = page.locator('button').filter({ hasText: /^檔案$|^Files$/ }).first();
32
+ await filesBtn.click();
33
+ await setTimeout(2000);
34
+ await page.screenshot({ path: `${OUT}08-file-explorer.png` });
35
+ console.log("✓ 08-file-explorer.png");
36
+ await filesBtn.click();
37
+ await setTimeout(500);
38
+ }
39
+
40
+ // Switch to EN
41
+ await page.click('button:has-text("EN")');
42
+ await setTimeout(1000);
43
+
44
+ // New session in EN
45
+ await page.click('button:has-text("New Session")');
46
+ await setTimeout(1500);
47
+ await page.screenshot({ path: `${OUT}09-en-cards.png` });
48
+ console.log("✓ 09-en-cards.png");
49
+
50
+ // Switch to JA
51
+ await page.click('button:has-text("JA")');
52
+ await setTimeout(500);
53
+ await page.click('button:has-text("New Session")');
54
+ await setTimeout(1500);
55
+ await page.screenshot({ path: `${OUT}10-ja-cards.png` });
56
+ console.log("✓ 10-ja-cards.png");
57
+
58
+ // Back to TW, show settings
59
+ await page.click('button:has-text("TW")');
60
+ await setTimeout(500);
61
+ const settingsBtn = page.locator('button:has-text("Settings")');
62
+ if (await settingsBtn.count() > 0) {
63
+ await settingsBtn.click();
64
+ await setTimeout(1000);
65
+ await page.screenshot({ path: `${OUT}11-settings.png` });
66
+ console.log("✓ 11-settings.png");
67
+ }
68
+
69
+ await page.close();
70
+ await context.close();
71
+ await browser.close();
72
+ console.log("\n✅ Remaining screenshots captured");
73
+ })();
@@ -0,0 +1,110 @@
1
+ import { chromium } from "playwright";
2
+ import { setTimeout } from "timers/promises";
3
+
4
+ const BASE = "http://localhost:5173";
5
+ const OUT = new URL(".", import.meta.url).pathname;
6
+
7
+ (async () => {
8
+ const browser = await chromium.launch({ headless: true });
9
+ const context = await browser.newContext({
10
+ viewport: { width: 1440, height: 900 },
11
+ recordVideo: { dir: OUT, size: { width: 1440, height: 900 } },
12
+ });
13
+ const page = await context.newPage();
14
+
15
+ // Wait for Vite to be ready (retry)
16
+ for (let i = 0; i < 20; i++) {
17
+ try {
18
+ await page.goto(BASE, { timeout: 5000 });
19
+ break;
20
+ } catch {
21
+ console.log(`Waiting for server... (${i + 1})`);
22
+ await setTimeout(2000);
23
+ }
24
+ }
25
+ // 1. Welcome screen
26
+ await setTimeout(2000);
27
+ await page.screenshot({ path: `${OUT}01-welcome.png` });
28
+ console.log("✓ 01-welcome.png");
29
+
30
+ // 2. Create new session → Empty chat with pipeline cards
31
+ await page.click('button:has-text("開始新 Session")');
32
+ await setTimeout(1500);
33
+ await page.screenshot({ path: `${OUT}02-pipeline-cards.png` });
34
+ console.log("✓ 02-pipeline-cards.png");
35
+
36
+ // 3. Click 指定主題 → input fill + hint
37
+ await page.click('button:has-text("指定主題發文")');
38
+ await setTimeout(1000);
39
+ await page.screenshot({ path: `${OUT}03-custom-topic-fill.png` });
40
+ console.log("✓ 03-custom-topic-fill.png");
41
+
42
+ // 4. Type topic and send
43
+ const textarea = page.locator("textarea");
44
+ await textarea.fill("AI Agent 自動化工作流 2026 最新趨勢");
45
+ await setTimeout(500);
46
+ await page.screenshot({ path: `${OUT}04-topic-typed.png` });
47
+ console.log("✓ 04-topic-typed.png");
48
+
49
+ await textarea.press("Enter");
50
+ await setTimeout(3000);
51
+ await page.screenshot({ path: `${OUT}05-loading-state.png` });
52
+ console.log("✓ 05-loading-state.png");
53
+
54
+ // 5. Wait for response with tool calls
55
+ await setTimeout(15000);
56
+ await page.screenshot({ path: `${OUT}06-tool-calls.png` });
57
+ console.log("✓ 06-tool-calls.png");
58
+
59
+ // 6. Wait more for full response
60
+ await setTimeout(30000);
61
+ await page.screenshot({ path: `${OUT}07-response.png`, fullPage: true });
62
+ console.log("✓ 07-response.png");
63
+
64
+ // 7. Open file explorer
65
+ const filesBtn = page.locator('button:has-text("Files"), button:has-text("檔案")').first();
66
+ await filesBtn.click();
67
+ await setTimeout(2000);
68
+ await page.screenshot({ path: `${OUT}08-file-explorer.png` });
69
+ console.log("✓ 08-file-explorer.png");
70
+ await filesBtn.click(); // close
71
+
72
+ // 8. Switch language to EN
73
+ await page.click('button:has-text("EN")');
74
+ await setTimeout(1500);
75
+ await page.screenshot({ path: `${OUT}09-english-mode.png` });
76
+ console.log("✓ 09-english-mode.png");
77
+
78
+ // 9. Create new session in EN to show EN cards
79
+ await page.click('button:has-text("New Session")');
80
+ await setTimeout(1500);
81
+ await page.screenshot({ path: `${OUT}10-en-pipeline-cards.png` });
82
+ console.log("✓ 10-en-pipeline-cards.png");
83
+
84
+ // 10. Switch to JA
85
+ await page.click('button:has-text("JA")');
86
+ await setTimeout(500);
87
+ await page.click('button:has-text("New Session")');
88
+ await setTimeout(1500);
89
+ await page.screenshot({ path: `${OUT}11-ja-pipeline-cards.png` });
90
+ console.log("✓ 11-ja-pipeline-cards.png");
91
+
92
+ // Switch back to TW
93
+ await page.click('button:has-text("TW")');
94
+ await setTimeout(500);
95
+
96
+ // 12. Click existing session with history to show rich content
97
+ const sessionItem = page.locator('[class*="cursor-pointer"]').filter({ hasText: "Claude Code 4.6" }).first();
98
+ if (await sessionItem.count() > 0) {
99
+ await sessionItem.click();
100
+ await setTimeout(2000);
101
+ await page.screenshot({ path: `${OUT}12-history-rich-content.png`, fullPage: true });
102
+ console.log("✓ 12-history-rich-content.png");
103
+ }
104
+
105
+ // Done
106
+ await page.close();
107
+ await context.close();
108
+ await browser.close();
109
+ console.log("\n✅ All screenshots captured in demo/");
110
+ })();
Binary file
Binary file
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "claude-world-studio",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Claude World Studio - Trend Discovery to Content Publishing Pipeline",
6
+ "scripts": {
7
+ "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
8
+ "dev:server": "tsx watch server/server.ts",
9
+ "dev:client": "vite --port 5173",
10
+ "start": "tsx server/server.ts",
11
+ "build": "vite build"
12
+ },
13
+ "dependencies": {
14
+ "@anthropic-ai/claude-agent-sdk": "^0.1.28",
15
+ "better-sqlite3": "^11.7.0",
16
+ "cors": "^2.8.5",
17
+ "dotenv": "^16.4.5",
18
+ "express": "^4.21.0",
19
+ "playwright": "^1.58.2",
20
+ "react": "^18.3.1",
21
+ "react-dom": "^18.3.1",
22
+ "react-markdown": "^10.1.0",
23
+ "react-use-websocket": "^4.13.0",
24
+ "rehype-sanitize": "^6.0.0",
25
+ "uuid": "^10.0.0",
26
+ "ws": "^8.18.0",
27
+ "zod": "^4.3.6"
28
+ },
29
+ "devDependencies": {
30
+ "@tailwindcss/typography": "^0.5.19",
31
+ "@types/better-sqlite3": "^7.6.12",
32
+ "@types/cors": "^2.8.17",
33
+ "@types/express": "^4.17.21",
34
+ "@types/node": "^22.0.0",
35
+ "@types/react": "^18.3.0",
36
+ "@types/react-dom": "^18.3.0",
37
+ "@types/uuid": "^10.0.0",
38
+ "@types/ws": "^8.5.12",
39
+ "@vitejs/plugin-react": "^4.3.0",
40
+ "autoprefixer": "^10.4.20",
41
+ "concurrently": "^9.0.0",
42
+ "postcss": "^8.4.47",
43
+ "tailwindcss": "^3.4.14",
44
+ "tsx": "^4.19.0",
45
+ "typescript": "^5.5.0",
46
+ "vite": "^5.4.0"
47
+ }
48
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };