create-byoky-app 0.4.10
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/dist/index.d.ts +1 -0
- package/dist/index.js +149 -0
- package/package.json +27 -0
- package/templates/backend-relay/README.md.tpl +37 -0
- package/templates/backend-relay/client/index.html.tpl +13 -0
- package/templates/backend-relay/client/main.ts.tpl +155 -0
- package/templates/backend-relay/client/style.css.tpl +137 -0
- package/templates/backend-relay/package.json.tpl +25 -0
- package/templates/backend-relay/server/index.ts.tpl +150 -0
- package/templates/backend-relay/tsconfig.json.tpl +16 -0
- package/templates/chat/README.md.tpl +28 -0
- package/templates/chat/next.config.ts.tpl +5 -0
- package/templates/chat/package.json.tpl +22 -0
- package/templates/chat/src/app/globals.css.tpl +29 -0
- package/templates/chat/src/app/layout.tsx.tpl +22 -0
- package/templates/chat/src/app/page.tsx.tpl +346 -0
- package/templates/chat/tsconfig.json.tpl +21 -0
- package/templates/multi-provider/README.md.tpl +29 -0
- package/templates/multi-provider/index.html.tpl +13 -0
- package/templates/multi-provider/package.json.tpl +18 -0
- package/templates/multi-provider/src/main.ts.tpl +202 -0
- package/templates/multi-provider/src/style.css.tpl +141 -0
- package/templates/multi-provider/tsconfig.json.tpl +17 -0
- package/templates/multi-provider/vite.config.ts.tpl +8 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 byoky contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import * as readline from "readline";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
var __dirname = path.dirname(__filename);
|
|
10
|
+
var TEMPLATES = [
|
|
11
|
+
{
|
|
12
|
+
id: "chat",
|
|
13
|
+
name: "AI Chat (Next.js)",
|
|
14
|
+
description: "Multi-provider chat with streaming"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: "multi-provider",
|
|
18
|
+
name: "Multi-Provider (Vite)",
|
|
19
|
+
description: "Use multiple AI providers with fallback"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "backend-relay",
|
|
23
|
+
name: "Backend Relay (Express)",
|
|
24
|
+
description: "Server-side LLM calls through user's wallet"
|
|
25
|
+
}
|
|
26
|
+
];
|
|
27
|
+
function printBanner() {
|
|
28
|
+
console.log();
|
|
29
|
+
console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
30
|
+
console.log(" \u2551 create-byoky-app \u2551");
|
|
31
|
+
console.log(" \u2551 Build AI apps in minutes \u2551");
|
|
32
|
+
console.log(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
33
|
+
console.log();
|
|
34
|
+
}
|
|
35
|
+
function ask(rl, question) {
|
|
36
|
+
return new Promise((resolve2) => {
|
|
37
|
+
rl.question(question, (answer) => {
|
|
38
|
+
resolve2(answer.trim());
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function isValidProjectName(name) {
|
|
43
|
+
return /^[a-zA-Z0-9_-]+$/.test(name) && name.length > 0;
|
|
44
|
+
}
|
|
45
|
+
function getTemplatesDir() {
|
|
46
|
+
const fromDist = path.resolve(__dirname, "..", "templates");
|
|
47
|
+
if (fs.existsSync(fromDist)) return fromDist;
|
|
48
|
+
const fromSrc = path.resolve(__dirname, "..", "..", "templates");
|
|
49
|
+
if (fs.existsSync(fromSrc)) return fromSrc;
|
|
50
|
+
throw new Error("Could not find templates directory");
|
|
51
|
+
}
|
|
52
|
+
function copyTemplateDir(srcDir, destDir, projectName) {
|
|
53
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
56
|
+
const destName = entry.name.endsWith(".tpl") ? entry.name.slice(0, -4) : entry.name;
|
|
57
|
+
const destPath = path.join(destDir, destName);
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
60
|
+
copyTemplateDir(srcPath, destPath, projectName);
|
|
61
|
+
} else {
|
|
62
|
+
let content = fs.readFileSync(srcPath, "utf-8");
|
|
63
|
+
content = content.replaceAll("{{PROJECT_NAME}}", projectName);
|
|
64
|
+
fs.writeFileSync(destPath, content, "utf-8");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function main() {
|
|
69
|
+
printBanner();
|
|
70
|
+
const rl = readline.createInterface({
|
|
71
|
+
input: process.stdin,
|
|
72
|
+
output: process.stdout
|
|
73
|
+
});
|
|
74
|
+
try {
|
|
75
|
+
let projectName = process.argv[2];
|
|
76
|
+
if (!projectName) {
|
|
77
|
+
projectName = await ask(rl, " Project name: ");
|
|
78
|
+
}
|
|
79
|
+
if (!projectName) {
|
|
80
|
+
console.error("\n Error: Project name is required.\n");
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
if (!isValidProjectName(projectName)) {
|
|
84
|
+
console.error("\n Error: Project name can only contain letters, numbers, hyphens, and underscores.\n");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
88
|
+
if (fs.existsSync(targetDir)) {
|
|
89
|
+
console.error(`
|
|
90
|
+
Error: Directory "${projectName}" already exists.
|
|
91
|
+
`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
console.log("\n Choose a template:\n");
|
|
95
|
+
for (let i = 0; i < TEMPLATES.length; i++) {
|
|
96
|
+
const t = TEMPLATES[i];
|
|
97
|
+
console.log(` ${i + 1}. ${t.name}`);
|
|
98
|
+
console.log(` ${t.description}
|
|
99
|
+
`);
|
|
100
|
+
}
|
|
101
|
+
const choice = await ask(rl, " Template (1-3): ");
|
|
102
|
+
const choiceNum = parseInt(choice, 10);
|
|
103
|
+
if (isNaN(choiceNum) || choiceNum < 1 || choiceNum > TEMPLATES.length) {
|
|
104
|
+
console.error("\n Error: Invalid template choice.\n");
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
const template = TEMPLATES[choiceNum - 1];
|
|
108
|
+
const templateId = template.id;
|
|
109
|
+
console.log(`
|
|
110
|
+
Creating ${projectName} with ${template.name}...
|
|
111
|
+
`);
|
|
112
|
+
const templatesDir = getTemplatesDir();
|
|
113
|
+
const templateDir = path.join(templatesDir, templateId);
|
|
114
|
+
if (!fs.existsSync(templateDir)) {
|
|
115
|
+
console.error(`
|
|
116
|
+
Error: Template "${templateId}" not found.
|
|
117
|
+
`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
121
|
+
try {
|
|
122
|
+
copyTemplateDir(templateDir, targetDir, projectName);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
console.log(` \u2713 Created ${projectName}!`);
|
|
128
|
+
console.log();
|
|
129
|
+
console.log(" Next steps:");
|
|
130
|
+
console.log(` cd ${projectName}`);
|
|
131
|
+
console.log(" npm install");
|
|
132
|
+
console.log(" npm run dev");
|
|
133
|
+
console.log();
|
|
134
|
+
console.log(" Docs: https://byoky.com/dev");
|
|
135
|
+
console.log();
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (err instanceof Error) {
|
|
138
|
+
console.error(`
|
|
139
|
+
Error: ${err.message}
|
|
140
|
+
`);
|
|
141
|
+
} else {
|
|
142
|
+
console.error("\n An unexpected error occurred.\n");
|
|
143
|
+
}
|
|
144
|
+
process.exit(1);
|
|
145
|
+
} finally {
|
|
146
|
+
rl.close();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-byoky-app",
|
|
3
|
+
"version": "0.4.10",
|
|
4
|
+
"description": "Scaffold a new app powered by Byoky",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-byoky-app": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"templates"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/MichaelLod/byoky.git",
|
|
17
|
+
"directory": "packages/create-byoky-app"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.0.0",
|
|
21
|
+
"typescript": "^5.7.0"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
25
|
+
"typecheck": "tsc --noEmit"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
Backend relay demo powered by [Byoky](https://byoky.com) — make server-side LLM calls through the user's wallet.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
1. Install the [Byoky wallet extension](https://byoky.com) and add your API key(s)
|
|
8
|
+
2. Install dependencies:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
3. Start both the server and client:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm run dev
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This starts:
|
|
21
|
+
- **Express server** on [http://localhost:3001](http://localhost:3001) (API + WebSocket relay)
|
|
22
|
+
- **Vite dev server** on [http://localhost:5173](http://localhost:5173) (frontend)
|
|
23
|
+
|
|
24
|
+
4. Open the frontend, click **Connect Wallet**, and try generating a response
|
|
25
|
+
|
|
26
|
+
## How it works
|
|
27
|
+
|
|
28
|
+
1. The browser connects to the Byoky wallet using `@byoky/sdk`
|
|
29
|
+
2. `session.createRelay(wsUrl)` opens a WebSocket to the Express server
|
|
30
|
+
3. The server uses `ByokyServer` to accept the connection and get a `ByokyClient`
|
|
31
|
+
4. `client.createFetch(providerId)` returns a fetch function that proxies API calls back through the user's wallet
|
|
32
|
+
5. The server makes LLM calls without ever seeing the user's API key
|
|
33
|
+
|
|
34
|
+
## Learn more
|
|
35
|
+
|
|
36
|
+
- [Byoky Documentation](https://byoky.com/dev)
|
|
37
|
+
- [Express Documentation](https://expressjs.com)
|
|
@@ -0,0 +1,13 @@
|
|
|
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>{{PROJECT_NAME}}</title>
|
|
7
|
+
<link rel="stylesheet" href="./style.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="./main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Byoky, isExtensionInstalled, getStoreUrl } from '@byoky/sdk';
|
|
2
|
+
import type { ByokySession } from '@byoky/sdk';
|
|
3
|
+
import type { RelayConnection } from '@byoky/sdk';
|
|
4
|
+
|
|
5
|
+
const SERVER_URL = 'http://localhost:3001';
|
|
6
|
+
const WS_RELAY_URL = 'ws://localhost:3001/ws/relay';
|
|
7
|
+
|
|
8
|
+
const app = document.getElementById('app')!;
|
|
9
|
+
const byoky = new Byoky();
|
|
10
|
+
let session: ByokySession | null = null;
|
|
11
|
+
let relay: RelayConnection | null = null;
|
|
12
|
+
|
|
13
|
+
function render(): void {
|
|
14
|
+
if (!session) {
|
|
15
|
+
renderConnect();
|
|
16
|
+
} else {
|
|
17
|
+
renderDashboard();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function renderConnect(): void {
|
|
22
|
+
app.innerHTML = `
|
|
23
|
+
<h1>{{PROJECT_NAME}}</h1>
|
|
24
|
+
<p class="subtitle">Server-side LLM calls through your Byoky wallet</p>
|
|
25
|
+
<button id="connect-btn">Connect Wallet</button>
|
|
26
|
+
<div id="error"></div>
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
document.getElementById('connect-btn')!.addEventListener('click', async () => {
|
|
30
|
+
const btn = document.getElementById('connect-btn') as HTMLButtonElement;
|
|
31
|
+
btn.disabled = true;
|
|
32
|
+
btn.textContent = 'Connecting...';
|
|
33
|
+
const errorEl = document.getElementById('error')!;
|
|
34
|
+
errorEl.innerHTML = '';
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
session = await byoky.connect({
|
|
38
|
+
providers: [{ id: 'anthropic', required: false }],
|
|
39
|
+
modal: true,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Open a relay to the backend server
|
|
43
|
+
relay = session.createRelay(WS_RELAY_URL);
|
|
44
|
+
|
|
45
|
+
session.onDisconnect(() => {
|
|
46
|
+
session = null;
|
|
47
|
+
relay = null;
|
|
48
|
+
render();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
render();
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (!isExtensionInstalled()) {
|
|
54
|
+
const url = getStoreUrl();
|
|
55
|
+
errorEl.innerHTML = `<p class="error">Byoky wallet not found.${
|
|
56
|
+
url ? ` <a href="${url}" target="_blank" style="color:#818cf8">Install extension</a>` : ''
|
|
57
|
+
}</p>`;
|
|
58
|
+
} else {
|
|
59
|
+
errorEl.innerHTML = `<p class="error">${err instanceof Error ? err.message : 'Connection failed'}</p>`;
|
|
60
|
+
}
|
|
61
|
+
btn.disabled = false;
|
|
62
|
+
btn.textContent = 'Connect Wallet';
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function renderDashboard(): void {
|
|
68
|
+
if (!session) return;
|
|
69
|
+
|
|
70
|
+
const providers = Object.entries(session.providers)
|
|
71
|
+
.filter(([, p]) => p.available)
|
|
72
|
+
.map(([id]) => id);
|
|
73
|
+
|
|
74
|
+
app.innerHTML = `
|
|
75
|
+
<h1>{{PROJECT_NAME}}</h1>
|
|
76
|
+
<div class="status-bar">
|
|
77
|
+
<div class="status-dot"></div>
|
|
78
|
+
<span>Wallet connected — relay active</span>
|
|
79
|
+
<button id="disconnect-btn" style="margin-left:auto;background:transparent;color:var(--text-muted);border:1px solid var(--border);padding:6px 12px;font-size:0.85rem;">Disconnect</button>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div class="card">
|
|
83
|
+
<h2>Server-Side Generation</h2>
|
|
84
|
+
<p class="info" style="margin-bottom:12px">
|
|
85
|
+
Your prompt is sent to the Express server, which makes the LLM call through the relay
|
|
86
|
+
using provider: <strong>${providers[0] ?? 'none'}</strong>
|
|
87
|
+
</p>
|
|
88
|
+
<textarea id="prompt" placeholder="Enter a prompt...">Explain what a backend relay is in two sentences.</textarea>
|
|
89
|
+
<button id="generate-btn">Generate on Server</button>
|
|
90
|
+
<div id="response"></div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="card">
|
|
94
|
+
<h2>How it works</h2>
|
|
95
|
+
<p class="info">1. Browser connects to Byoky wallet via the SDK</p>
|
|
96
|
+
<p class="info">2. <code>session.createRelay()</code> opens a WebSocket to your Express server</p>
|
|
97
|
+
<p class="info">3. The server receives a <code>ByokyClient</code> with <code>createFetch()</code></p>
|
|
98
|
+
<p class="info">4. Server-side fetch calls are proxied through the wallet — keys never leave the extension</p>
|
|
99
|
+
</div>
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
document.getElementById('disconnect-btn')!.addEventListener('click', () => {
|
|
103
|
+
relay?.close();
|
|
104
|
+
session?.disconnect();
|
|
105
|
+
session = null;
|
|
106
|
+
relay = null;
|
|
107
|
+
render();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
document.getElementById('generate-btn')!.addEventListener('click', async () => {
|
|
111
|
+
const btn = document.getElementById('generate-btn') as HTMLButtonElement;
|
|
112
|
+
const prompt = (document.getElementById('prompt') as HTMLTextAreaElement).value.trim();
|
|
113
|
+
const responseEl = document.getElementById('response')!;
|
|
114
|
+
|
|
115
|
+
if (!prompt) return;
|
|
116
|
+
btn.disabled = true;
|
|
117
|
+
btn.textContent = 'Generating...';
|
|
118
|
+
responseEl.innerHTML = '<div class="response-box">Waiting for server...</div>';
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const res = await fetch(`${SERVER_URL}/api/generate`, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: { 'Content-Type': 'application/json' },
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
sessionId: session?.sessionKey,
|
|
126
|
+
prompt,
|
|
127
|
+
}),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const data = await res.json();
|
|
131
|
+
|
|
132
|
+
if (!res.ok) {
|
|
133
|
+
throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
responseEl.innerHTML = `
|
|
137
|
+
<div class="response-box">${escapeHtml(data.content)}</div>
|
|
138
|
+
<p class="info">Provider: ${data.provider}</p>
|
|
139
|
+
`;
|
|
140
|
+
} catch (err) {
|
|
141
|
+
responseEl.innerHTML = `<p class="error">${err instanceof Error ? escapeHtml(err.message) : 'Request failed'}</p>`;
|
|
142
|
+
} finally {
|
|
143
|
+
btn.disabled = false;
|
|
144
|
+
btn.textContent = 'Generate on Server';
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function escapeHtml(str: string): string {
|
|
150
|
+
const div = document.createElement('div');
|
|
151
|
+
div.textContent = str;
|
|
152
|
+
return div.innerHTML;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
render();
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
*,
|
|
2
|
+
*::before,
|
|
3
|
+
*::after {
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
margin: 0;
|
|
6
|
+
padding: 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
:root {
|
|
10
|
+
--bg: #0a0a0a;
|
|
11
|
+
--bg-secondary: #141414;
|
|
12
|
+
--bg-tertiary: #1e1e1e;
|
|
13
|
+
--text: #e0e0e0;
|
|
14
|
+
--text-muted: #888;
|
|
15
|
+
--accent: #6366f1;
|
|
16
|
+
--accent-hover: #818cf8;
|
|
17
|
+
--border: #2a2a2a;
|
|
18
|
+
--success: #4ade80;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
body {
|
|
22
|
+
background: var(--bg);
|
|
23
|
+
color: var(--text);
|
|
24
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
25
|
+
-webkit-font-smoothing: antialiased;
|
|
26
|
+
min-height: 100vh;
|
|
27
|
+
display: flex;
|
|
28
|
+
justify-content: center;
|
|
29
|
+
padding: 48px 24px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#app {
|
|
33
|
+
width: 100%;
|
|
34
|
+
max-width: 640px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
h1 {
|
|
38
|
+
font-size: 2rem;
|
|
39
|
+
font-weight: 700;
|
|
40
|
+
margin-bottom: 8px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.subtitle {
|
|
44
|
+
color: var(--text-muted);
|
|
45
|
+
margin-bottom: 32px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
button {
|
|
49
|
+
background: var(--accent);
|
|
50
|
+
color: #fff;
|
|
51
|
+
border: none;
|
|
52
|
+
border-radius: 8px;
|
|
53
|
+
padding: 10px 24px;
|
|
54
|
+
font-size: 0.95rem;
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
transition: background 0.15s;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
button:hover {
|
|
61
|
+
background: var(--accent-hover);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
button:disabled {
|
|
65
|
+
opacity: 0.5;
|
|
66
|
+
cursor: not-allowed;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.status-bar {
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
gap: 8px;
|
|
73
|
+
margin-bottom: 24px;
|
|
74
|
+
padding: 12px 16px;
|
|
75
|
+
background: var(--bg-secondary);
|
|
76
|
+
border: 1px solid var(--border);
|
|
77
|
+
border-radius: 8px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.status-dot {
|
|
81
|
+
width: 8px;
|
|
82
|
+
height: 8px;
|
|
83
|
+
border-radius: 50%;
|
|
84
|
+
background: var(--success);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.card {
|
|
88
|
+
background: var(--bg-secondary);
|
|
89
|
+
border: 1px solid var(--border);
|
|
90
|
+
border-radius: 8px;
|
|
91
|
+
padding: 20px;
|
|
92
|
+
margin-bottom: 16px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.card h2 {
|
|
96
|
+
font-size: 1.1rem;
|
|
97
|
+
margin-bottom: 12px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
textarea {
|
|
101
|
+
width: 100%;
|
|
102
|
+
background: var(--bg-tertiary);
|
|
103
|
+
color: var(--text);
|
|
104
|
+
border: 1px solid var(--border);
|
|
105
|
+
border-radius: 6px;
|
|
106
|
+
padding: 12px;
|
|
107
|
+
font-size: 0.95rem;
|
|
108
|
+
font-family: inherit;
|
|
109
|
+
resize: vertical;
|
|
110
|
+
min-height: 80px;
|
|
111
|
+
outline: none;
|
|
112
|
+
margin-bottom: 12px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.response-box {
|
|
116
|
+
background: var(--bg-tertiary);
|
|
117
|
+
border: 1px solid var(--border);
|
|
118
|
+
border-radius: 6px;
|
|
119
|
+
padding: 12px;
|
|
120
|
+
margin-top: 12px;
|
|
121
|
+
font-size: 0.9rem;
|
|
122
|
+
line-height: 1.5;
|
|
123
|
+
white-space: pre-wrap;
|
|
124
|
+
color: var(--text-muted);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.error {
|
|
128
|
+
color: #f87171;
|
|
129
|
+
font-size: 0.9rem;
|
|
130
|
+
margin-top: 12px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.info {
|
|
134
|
+
color: var(--text-muted);
|
|
135
|
+
font-size: 0.85rem;
|
|
136
|
+
margin-top: 8px;
|
|
137
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
|
|
8
|
+
"dev:server": "tsx watch server/index.ts",
|
|
9
|
+
"dev:client": "vite client",
|
|
10
|
+
"build:client": "vite build client"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@byoky/sdk": "^0.4.9",
|
|
14
|
+
"express": "^4.21.0",
|
|
15
|
+
"ws": "^8.18.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/express": "^5.0.0",
|
|
19
|
+
"@types/ws": "^8.5.0",
|
|
20
|
+
"concurrently": "^9.1.0",
|
|
21
|
+
"tsx": "^4.19.0",
|
|
22
|
+
"typescript": "^5.7.0",
|
|
23
|
+
"vite": "^6.0.0"
|
|
24
|
+
}
|
|
25
|
+
}
|