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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
4
|
+
import { ByokyServer } from '@byoky/sdk/server';
|
|
5
|
+
import type { ByokyClient } from '@byoky/sdk/server';
|
|
6
|
+
|
|
7
|
+
const app = express();
|
|
8
|
+
app.use(express.json());
|
|
9
|
+
|
|
10
|
+
// CORS for local development
|
|
11
|
+
app.use((_req, res, next) => {
|
|
12
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
13
|
+
res.header('Access-Control-Allow-Headers', 'Content-Type');
|
|
14
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
15
|
+
next();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const server = createServer(app);
|
|
19
|
+
const wss = new WebSocketServer({ server, path: '/ws/relay' });
|
|
20
|
+
const byokyServer = new ByokyServer();
|
|
21
|
+
|
|
22
|
+
// Track connected clients
|
|
23
|
+
const clients = new Map<string, ByokyClient>();
|
|
24
|
+
|
|
25
|
+
wss.on('connection', async (ws: WebSocket) => {
|
|
26
|
+
console.log('[relay] New WebSocket connection');
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const client = await byokyServer.handleConnection(ws);
|
|
30
|
+
clients.set(client.sessionId, client);
|
|
31
|
+
console.log(`[relay] Client connected: ${client.sessionId}`);
|
|
32
|
+
|
|
33
|
+
const availableProviders = Object.entries(client.providers)
|
|
34
|
+
.filter(([, p]) => p.available)
|
|
35
|
+
.map(([id]) => id);
|
|
36
|
+
console.log(`[relay] Available providers: ${availableProviders.join(', ') || 'none'}`);
|
|
37
|
+
|
|
38
|
+
client.onClose(() => {
|
|
39
|
+
clients.delete(client.sessionId);
|
|
40
|
+
console.log(`[relay] Client disconnected: ${client.sessionId}`);
|
|
41
|
+
});
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error('[relay] Connection failed:', err instanceof Error ? err.message : err);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Health check
|
|
48
|
+
app.get('/api/health', (_req, res) => {
|
|
49
|
+
res.json({
|
|
50
|
+
ok: true,
|
|
51
|
+
clients: clients.size,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Example: Make an LLM call through the relay
|
|
56
|
+
app.post('/api/generate', async (req, res) => {
|
|
57
|
+
const { sessionId, prompt } = req.body as { sessionId?: string; prompt?: string };
|
|
58
|
+
|
|
59
|
+
if (!sessionId || !prompt) {
|
|
60
|
+
res.status(400).json({ error: 'Missing sessionId or prompt' });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const client = clients.get(sessionId);
|
|
65
|
+
if (!client || !client.connected) {
|
|
66
|
+
res.status(404).json({ error: 'Client not connected' });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Find the first available provider
|
|
71
|
+
const providerId = Object.entries(client.providers)
|
|
72
|
+
.find(([, p]) => p.available)?.[0];
|
|
73
|
+
|
|
74
|
+
if (!providerId) {
|
|
75
|
+
res.status(400).json({ error: 'No providers available' });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const proxyFetch = client.createFetch(providerId);
|
|
81
|
+
|
|
82
|
+
let url: string;
|
|
83
|
+
let headers: Record<string, string>;
|
|
84
|
+
let body: string;
|
|
85
|
+
|
|
86
|
+
if (providerId === 'anthropic') {
|
|
87
|
+
url = 'https://api.anthropic.com/v1/messages';
|
|
88
|
+
headers = {
|
|
89
|
+
'Content-Type': 'application/json',
|
|
90
|
+
'x-api-key': 'byoky',
|
|
91
|
+
'anthropic-version': '2023-06-01',
|
|
92
|
+
};
|
|
93
|
+
body = JSON.stringify({
|
|
94
|
+
model: 'claude-sonnet-4-20250514',
|
|
95
|
+
max_tokens: 256,
|
|
96
|
+
messages: [{ role: 'user', content: prompt }],
|
|
97
|
+
});
|
|
98
|
+
} else if (providerId === 'openai') {
|
|
99
|
+
url = 'https://api.openai.com/v1/chat/completions';
|
|
100
|
+
headers = {
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
'Authorization': 'Bearer byoky',
|
|
103
|
+
};
|
|
104
|
+
body = JSON.stringify({
|
|
105
|
+
model: 'gpt-4o-mini',
|
|
106
|
+
max_tokens: 256,
|
|
107
|
+
messages: [{ role: 'user', content: prompt }],
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent';
|
|
111
|
+
headers = { 'Content-Type': 'application/json' };
|
|
112
|
+
body = JSON.stringify({
|
|
113
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
114
|
+
generationConfig: { maxOutputTokens: 256 },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const response = await proxyFetch(url, { method: 'POST', headers, body });
|
|
119
|
+
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
const text = await response.text();
|
|
122
|
+
res.status(response.status).json({ error: text.slice(0, 500) });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const data = await response.json();
|
|
127
|
+
|
|
128
|
+
let content: string;
|
|
129
|
+
if (providerId === 'anthropic') {
|
|
130
|
+
content = data.content?.[0]?.text ?? '';
|
|
131
|
+
} else if (providerId === 'openai') {
|
|
132
|
+
content = data.choices?.[0]?.message?.content ?? '';
|
|
133
|
+
} else {
|
|
134
|
+
content = data.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
res.json({ provider: providerId, content });
|
|
138
|
+
} catch (err) {
|
|
139
|
+
res.status(500).json({
|
|
140
|
+
error: err instanceof Error ? err.message : 'Internal server error',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const PORT = parseInt(process.env.PORT ?? '3001', 10);
|
|
146
|
+
|
|
147
|
+
server.listen(PORT, () => {
|
|
148
|
+
console.log(`[server] Listening on http://localhost:${PORT}`);
|
|
149
|
+
console.log(`[server] WebSocket relay at ws://localhost:${PORT}/ws/relay`);
|
|
150
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["server", "client"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
AI chat app powered by [Byoky](https://byoky.com) — use your own API keys without exposing them to websites.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
1. Install the [Byoky wallet extension](https://byoky.com) and add your Anthropic API key
|
|
8
|
+
2. Install dependencies and start the dev server:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install
|
|
12
|
+
npm run dev
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
3. Open [http://localhost:3000](http://localhost:3000)
|
|
16
|
+
4. Click **Connect Wallet** and approve the connection
|
|
17
|
+
5. Start chatting
|
|
18
|
+
|
|
19
|
+
## How it works
|
|
20
|
+
|
|
21
|
+
- The app uses `@byoky/sdk` to connect to your Byoky wallet
|
|
22
|
+
- API calls are proxied through the wallet extension — your API key never leaves the extension
|
|
23
|
+
- Streaming responses are powered by the Anthropic SDK with a custom `fetch` from Byoky
|
|
24
|
+
|
|
25
|
+
## Learn more
|
|
26
|
+
|
|
27
|
+
- [Byoky Documentation](https://byoky.com/dev)
|
|
28
|
+
- [Next.js Documentation](https://nextjs.org/docs)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@anthropic-ai/sdk": "^0.39.0",
|
|
12
|
+
"@byoky/sdk": "^0.4.9",
|
|
13
|
+
"next": "^15.1.0",
|
|
14
|
+
"react": "^19.0.0",
|
|
15
|
+
"react-dom": "^19.0.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/react": "^19.0.0",
|
|
19
|
+
"@types/react-dom": "^19.0.0",
|
|
20
|
+
"typescript": "^5.7.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
--user-bg: #1a1a2e;
|
|
19
|
+
--assistant-bg: #141414;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
html,
|
|
23
|
+
body {
|
|
24
|
+
height: 100%;
|
|
25
|
+
background: var(--bg);
|
|
26
|
+
color: var(--text);
|
|
27
|
+
font-family: var(--font-inter), system-ui, -apple-system, sans-serif;
|
|
28
|
+
-webkit-font-smoothing: antialiased;
|
|
29
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import { Inter } from 'next/font/google';
|
|
3
|
+
import './globals.css';
|
|
4
|
+
|
|
5
|
+
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
|
|
6
|
+
|
|
7
|
+
export const metadata: Metadata = {
|
|
8
|
+
title: '{{PROJECT_NAME}}',
|
|
9
|
+
description: 'AI chat powered by Byoky',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default function RootLayout({
|
|
13
|
+
children,
|
|
14
|
+
}: {
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<html lang="en" className={inter.variable}>
|
|
19
|
+
<body>{children}</body>
|
|
20
|
+
</html>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
4
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
5
|
+
import { Byoky, isExtensionInstalled, getStoreUrl } from '@byoky/sdk';
|
|
6
|
+
import type { ByokySession } from '@byoky/sdk';
|
|
7
|
+
|
|
8
|
+
interface Message {
|
|
9
|
+
role: 'user' | 'assistant';
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const byoky = new Byoky();
|
|
14
|
+
|
|
15
|
+
export default function Home() {
|
|
16
|
+
const [session, setSession] = useState<ByokySession | null>(null);
|
|
17
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
18
|
+
const [input, setInput] = useState('');
|
|
19
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
22
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
|
|
24
|
+
const scrollToBottom = useCallback(() => {
|
|
25
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
scrollToBottom();
|
|
30
|
+
}, [messages, scrollToBottom]);
|
|
31
|
+
|
|
32
|
+
const handleConnect = async () => {
|
|
33
|
+
setError(null);
|
|
34
|
+
setIsConnecting(true);
|
|
35
|
+
try {
|
|
36
|
+
const s = await byoky.connect({
|
|
37
|
+
providers: [{ id: 'anthropic', required: true }],
|
|
38
|
+
modal: true,
|
|
39
|
+
});
|
|
40
|
+
setSession(s);
|
|
41
|
+
s.onDisconnect(() => {
|
|
42
|
+
setSession(null);
|
|
43
|
+
setError('Wallet disconnected');
|
|
44
|
+
});
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (!isExtensionInstalled()) {
|
|
47
|
+
const url = getStoreUrl();
|
|
48
|
+
setError(
|
|
49
|
+
url
|
|
50
|
+
? 'Byoky wallet not found. Install the extension to continue.'
|
|
51
|
+
: 'Byoky wallet not found.'
|
|
52
|
+
);
|
|
53
|
+
} else if (err instanceof Error) {
|
|
54
|
+
setError(err.message);
|
|
55
|
+
}
|
|
56
|
+
} finally {
|
|
57
|
+
setIsConnecting(false);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleSend = async () => {
|
|
62
|
+
if (!session || !input.trim() || isStreaming) return;
|
|
63
|
+
|
|
64
|
+
const userMessage: Message = { role: 'user', content: input.trim() };
|
|
65
|
+
const updatedMessages = [...messages, userMessage];
|
|
66
|
+
setMessages(updatedMessages);
|
|
67
|
+
setInput('');
|
|
68
|
+
setIsStreaming(true);
|
|
69
|
+
setError(null);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const proxyFetch = session.createFetch('anthropic');
|
|
73
|
+
const client = new Anthropic({
|
|
74
|
+
apiKey: 'byoky',
|
|
75
|
+
fetch: proxyFetch,
|
|
76
|
+
dangerouslyAllowBrowser: true,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const assistantMessage: Message = { role: 'assistant', content: '' };
|
|
80
|
+
setMessages([...updatedMessages, assistantMessage]);
|
|
81
|
+
|
|
82
|
+
const stream = client.messages.stream({
|
|
83
|
+
model: 'claude-sonnet-4-20250514',
|
|
84
|
+
max_tokens: 1024,
|
|
85
|
+
messages: updatedMessages.map((m) => ({
|
|
86
|
+
role: m.role,
|
|
87
|
+
content: m.content,
|
|
88
|
+
})),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
for await (const event of stream) {
|
|
92
|
+
if (
|
|
93
|
+
event.type === 'content_block_delta' &&
|
|
94
|
+
event.delta.type === 'text_delta'
|
|
95
|
+
) {
|
|
96
|
+
assistantMessage.content += event.delta.text;
|
|
97
|
+
setMessages((prev) => [
|
|
98
|
+
...prev.slice(0, -1),
|
|
99
|
+
{ ...assistantMessage },
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch (err) {
|
|
104
|
+
setError(err instanceof Error ? err.message : 'Failed to send message');
|
|
105
|
+
} finally {
|
|
106
|
+
setIsStreaming(false);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
111
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
handleSend();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (!session) {
|
|
118
|
+
return (
|
|
119
|
+
<div style={styles.connectContainer}>
|
|
120
|
+
<h1 style={styles.title}>{{PROJECT_NAME}}</h1>
|
|
121
|
+
<p style={styles.subtitle}>AI chat powered by your own API keys</p>
|
|
122
|
+
<button
|
|
123
|
+
onClick={handleConnect}
|
|
124
|
+
disabled={isConnecting}
|
|
125
|
+
style={{
|
|
126
|
+
...styles.connectButton,
|
|
127
|
+
opacity: isConnecting ? 0.6 : 1,
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
{isConnecting ? 'Connecting...' : 'Connect Wallet'}
|
|
131
|
+
</button>
|
|
132
|
+
{error && <p style={styles.error}>{error}</p>}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div style={styles.chatContainer}>
|
|
139
|
+
<header style={styles.header}>
|
|
140
|
+
<h1 style={styles.headerTitle}>{{PROJECT_NAME}}</h1>
|
|
141
|
+
<div style={styles.headerRight}>
|
|
142
|
+
<span style={styles.status}>Connected</span>
|
|
143
|
+
<button
|
|
144
|
+
onClick={() => {
|
|
145
|
+
session.disconnect();
|
|
146
|
+
setSession(null);
|
|
147
|
+
setMessages([]);
|
|
148
|
+
}}
|
|
149
|
+
style={styles.disconnectButton}
|
|
150
|
+
>
|
|
151
|
+
Disconnect
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
</header>
|
|
155
|
+
|
|
156
|
+
<div style={styles.messagesContainer}>
|
|
157
|
+
{messages.length === 0 && (
|
|
158
|
+
<div style={styles.emptyState}>
|
|
159
|
+
<p style={styles.emptyText}>Send a message to start chatting</p>
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
{messages.map((msg, i) => (
|
|
163
|
+
<div
|
|
164
|
+
key={i}
|
|
165
|
+
style={{
|
|
166
|
+
...styles.message,
|
|
167
|
+
backgroundColor:
|
|
168
|
+
msg.role === 'user'
|
|
169
|
+
? 'var(--user-bg)'
|
|
170
|
+
: 'var(--assistant-bg)',
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
<div style={styles.messageRole}>
|
|
174
|
+
{msg.role === 'user' ? 'You' : 'Assistant'}
|
|
175
|
+
</div>
|
|
176
|
+
<div style={styles.messageContent}>{msg.content}</div>
|
|
177
|
+
</div>
|
|
178
|
+
))}
|
|
179
|
+
<div ref={messagesEndRef} />
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div style={styles.inputContainer}>
|
|
183
|
+
{error && <p style={styles.inlineError}>{error}</p>}
|
|
184
|
+
<div style={styles.inputRow}>
|
|
185
|
+
<textarea
|
|
186
|
+
value={input}
|
|
187
|
+
onChange={(e) => setInput(e.target.value)}
|
|
188
|
+
onKeyDown={handleKeyDown}
|
|
189
|
+
placeholder="Type a message..."
|
|
190
|
+
rows={1}
|
|
191
|
+
style={styles.textarea}
|
|
192
|
+
/>
|
|
193
|
+
<button
|
|
194
|
+
onClick={handleSend}
|
|
195
|
+
disabled={isStreaming || !input.trim()}
|
|
196
|
+
style={{
|
|
197
|
+
...styles.sendButton,
|
|
198
|
+
opacity: isStreaming || !input.trim() ? 0.5 : 1,
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
{isStreaming ? '...' : 'Send'}
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
210
|
+
connectContainer: {
|
|
211
|
+
display: 'flex',
|
|
212
|
+
flexDirection: 'column',
|
|
213
|
+
alignItems: 'center',
|
|
214
|
+
justifyContent: 'center',
|
|
215
|
+
height: '100vh',
|
|
216
|
+
gap: '16px',
|
|
217
|
+
},
|
|
218
|
+
title: {
|
|
219
|
+
fontSize: '2rem',
|
|
220
|
+
fontWeight: 700,
|
|
221
|
+
},
|
|
222
|
+
subtitle: {
|
|
223
|
+
color: 'var(--text-muted)',
|
|
224
|
+
fontSize: '1.1rem',
|
|
225
|
+
marginBottom: '8px',
|
|
226
|
+
},
|
|
227
|
+
connectButton: {
|
|
228
|
+
background: 'var(--accent)',
|
|
229
|
+
color: '#fff',
|
|
230
|
+
border: 'none',
|
|
231
|
+
borderRadius: '8px',
|
|
232
|
+
padding: '12px 32px',
|
|
233
|
+
fontSize: '1rem',
|
|
234
|
+
fontWeight: 600,
|
|
235
|
+
cursor: 'pointer',
|
|
236
|
+
},
|
|
237
|
+
error: {
|
|
238
|
+
color: '#f87171',
|
|
239
|
+
fontSize: '0.9rem',
|
|
240
|
+
maxWidth: '400px',
|
|
241
|
+
textAlign: 'center' as const,
|
|
242
|
+
},
|
|
243
|
+
chatContainer: {
|
|
244
|
+
display: 'flex',
|
|
245
|
+
flexDirection: 'column',
|
|
246
|
+
height: '100vh',
|
|
247
|
+
maxWidth: '800px',
|
|
248
|
+
margin: '0 auto',
|
|
249
|
+
},
|
|
250
|
+
header: {
|
|
251
|
+
display: 'flex',
|
|
252
|
+
alignItems: 'center',
|
|
253
|
+
justifyContent: 'space-between',
|
|
254
|
+
padding: '16px 24px',
|
|
255
|
+
borderBottom: '1px solid var(--border)',
|
|
256
|
+
},
|
|
257
|
+
headerTitle: {
|
|
258
|
+
fontSize: '1.1rem',
|
|
259
|
+
fontWeight: 600,
|
|
260
|
+
},
|
|
261
|
+
headerRight: {
|
|
262
|
+
display: 'flex',
|
|
263
|
+
alignItems: 'center',
|
|
264
|
+
gap: '12px',
|
|
265
|
+
},
|
|
266
|
+
status: {
|
|
267
|
+
color: '#4ade80',
|
|
268
|
+
fontSize: '0.85rem',
|
|
269
|
+
},
|
|
270
|
+
disconnectButton: {
|
|
271
|
+
background: 'transparent',
|
|
272
|
+
color: 'var(--text-muted)',
|
|
273
|
+
border: '1px solid var(--border)',
|
|
274
|
+
borderRadius: '6px',
|
|
275
|
+
padding: '6px 12px',
|
|
276
|
+
fontSize: '0.85rem',
|
|
277
|
+
cursor: 'pointer',
|
|
278
|
+
},
|
|
279
|
+
messagesContainer: {
|
|
280
|
+
flex: 1,
|
|
281
|
+
overflow: 'auto',
|
|
282
|
+
padding: '24px',
|
|
283
|
+
},
|
|
284
|
+
emptyState: {
|
|
285
|
+
display: 'flex',
|
|
286
|
+
alignItems: 'center',
|
|
287
|
+
justifyContent: 'center',
|
|
288
|
+
height: '100%',
|
|
289
|
+
},
|
|
290
|
+
emptyText: {
|
|
291
|
+
color: 'var(--text-muted)',
|
|
292
|
+
},
|
|
293
|
+
message: {
|
|
294
|
+
padding: '16px 20px',
|
|
295
|
+
borderRadius: '8px',
|
|
296
|
+
marginBottom: '12px',
|
|
297
|
+
},
|
|
298
|
+
messageRole: {
|
|
299
|
+
fontSize: '0.8rem',
|
|
300
|
+
fontWeight: 600,
|
|
301
|
+
color: 'var(--text-muted)',
|
|
302
|
+
marginBottom: '6px',
|
|
303
|
+
textTransform: 'uppercase' as const,
|
|
304
|
+
letterSpacing: '0.05em',
|
|
305
|
+
},
|
|
306
|
+
messageContent: {
|
|
307
|
+
lineHeight: 1.6,
|
|
308
|
+
whiteSpace: 'pre-wrap' as const,
|
|
309
|
+
},
|
|
310
|
+
inputContainer: {
|
|
311
|
+
padding: '16px 24px',
|
|
312
|
+
borderTop: '1px solid var(--border)',
|
|
313
|
+
},
|
|
314
|
+
inlineError: {
|
|
315
|
+
color: '#f87171',
|
|
316
|
+
fontSize: '0.85rem',
|
|
317
|
+
marginBottom: '8px',
|
|
318
|
+
},
|
|
319
|
+
inputRow: {
|
|
320
|
+
display: 'flex',
|
|
321
|
+
gap: '8px',
|
|
322
|
+
},
|
|
323
|
+
textarea: {
|
|
324
|
+
flex: 1,
|
|
325
|
+
background: 'var(--bg-tertiary)',
|
|
326
|
+
color: 'var(--text)',
|
|
327
|
+
border: '1px solid var(--border)',
|
|
328
|
+
borderRadius: '8px',
|
|
329
|
+
padding: '12px 16px',
|
|
330
|
+
fontSize: '1rem',
|
|
331
|
+
resize: 'none' as const,
|
|
332
|
+
outline: 'none',
|
|
333
|
+
fontFamily: 'inherit',
|
|
334
|
+
},
|
|
335
|
+
sendButton: {
|
|
336
|
+
background: 'var(--accent)',
|
|
337
|
+
color: '#fff',
|
|
338
|
+
border: 'none',
|
|
339
|
+
borderRadius: '8px',
|
|
340
|
+
padding: '12px 24px',
|
|
341
|
+
fontSize: '1rem',
|
|
342
|
+
fontWeight: 600,
|
|
343
|
+
cursor: 'pointer',
|
|
344
|
+
whiteSpace: 'nowrap' as const,
|
|
345
|
+
},
|
|
346
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }],
|
|
17
|
+
"paths": { "@/*": ["./src/*"] }
|
|
18
|
+
},
|
|
19
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
20
|
+
"exclude": ["node_modules"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
Multi-provider demo powered by [Byoky](https://byoky.com) — use Anthropic, OpenAI, and Gemini through a single wallet.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
1. Install the [Byoky wallet extension](https://byoky.com) and add your API keys
|
|
8
|
+
2. Install dependencies and start the dev server:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install
|
|
12
|
+
npm run dev
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
3. Open [http://localhost:5173](http://localhost:5173)
|
|
16
|
+
4. Click **Connect Wallet** and approve the connection
|
|
17
|
+
5. Test each connected provider
|
|
18
|
+
|
|
19
|
+
## How it works
|
|
20
|
+
|
|
21
|
+
- The app requests access to all three providers (all optional)
|
|
22
|
+
- Byoky shows which providers the user has configured
|
|
23
|
+
- Each provider gets its own `createFetch()` that proxies API calls through the wallet
|
|
24
|
+
- API keys never leave the extension
|
|
25
|
+
|
|
26
|
+
## Learn more
|
|
27
|
+
|
|
28
|
+
- [Byoky Documentation](https://byoky.com/dev)
|
|
29
|
+
- [Vite Documentation](https://vite.dev)
|
|
@@ -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="/src/style.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="/src/main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|