create-tether-app 0.1.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/dist/index.d.ts +1 -0
- package/dist/index.js +729 -0
- package/package.json +59 -0
- package/template/.env.example +18 -0
- package/template/README.md.template +123 -0
- package/template/backend/app/__init__.py.template +5 -0
- package/template/backend/app/main.py +66 -0
- package/template/backend/app/routes/__init__.py +3 -0
- package/template/backend/app/routes/chat.py +151 -0
- package/template/backend/app/routes/health.py +28 -0
- package/template/backend/app/routes/models.py +126 -0
- package/template/backend/app/services/__init__.py +3 -0
- package/template/backend/app/services/llm.py +526 -0
- package/template/backend/pyproject.toml.template +34 -0
- package/template/backend/scripts/build.py +112 -0
- package/template/frontend/App.css +58 -0
- package/template/frontend/App.tsx +62 -0
- package/template/frontend/components/Chat.css +220 -0
- package/template/frontend/components/Chat.tsx +284 -0
- package/template/frontend/components/ChatMessage.css +206 -0
- package/template/frontend/components/ChatMessage.tsx +62 -0
- package/template/frontend/components/ModelStatus.css +62 -0
- package/template/frontend/components/ModelStatus.tsx +103 -0
- package/template/frontend/hooks/useApi.ts +334 -0
- package/template/frontend/index.css +92 -0
- package/template/frontend/main.tsx +10 -0
- package/template/frontend/vite-env.d.ts +1 -0
- package/template/index.html.template +13 -0
- package/template/package.json.template +33 -0
- package/template/postcss.config.js.template +6 -0
- package/template/public/tether.svg +15 -0
- package/template/src-tauri/.cargo/config.toml +66 -0
- package/template/src-tauri/Cargo.lock +4764 -0
- package/template/src-tauri/Cargo.toml +24 -0
- package/template/src-tauri/build.rs +3 -0
- package/template/src-tauri/capabilities/default.json +40 -0
- package/template/src-tauri/icons/128x128.png +0 -0
- package/template/src-tauri/icons/128x128@2x.png +0 -0
- package/template/src-tauri/icons/32x32.png +0 -0
- package/template/src-tauri/icons/icon.icns +0 -0
- package/template/src-tauri/icons/icon.ico +0 -0
- package/template/src-tauri/src/main.rs +65 -0
- package/template/src-tauri/src/sidecar.rs +110 -0
- package/template/src-tauri/tauri.conf.json.template +44 -0
- package/template/tailwind.config.js.template +19 -0
- package/template/tsconfig.json +21 -0
- package/template/tsconfig.node.json +11 -0
- package/template/vite.config.ts +27 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { invoke } from "@tauri-apps/api/core";
|
|
3
|
+
|
|
4
|
+
// Types
|
|
5
|
+
export interface ChatMessage {
|
|
6
|
+
role: "user" | "assistant" | "system";
|
|
7
|
+
content: string;
|
|
8
|
+
images?: string[];
|
|
9
|
+
thinking?: string;
|
|
10
|
+
timestamp?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ChatRequest {
|
|
14
|
+
message: string;
|
|
15
|
+
images?: string[];
|
|
16
|
+
history?: ChatMessage[];
|
|
17
|
+
model?: string;
|
|
18
|
+
temperature?: number;
|
|
19
|
+
max_tokens?: number;
|
|
20
|
+
think?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ChatResponse {
|
|
24
|
+
response: string;
|
|
25
|
+
thinking?: string;
|
|
26
|
+
tokens_used?: number;
|
|
27
|
+
model?: string;
|
|
28
|
+
finish_reason?: "stop" | "length" | "error";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface HealthResponse {
|
|
32
|
+
status: "healthy" | "unhealthy";
|
|
33
|
+
model_loaded: boolean;
|
|
34
|
+
version: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ModelsResponse {
|
|
38
|
+
available: boolean;
|
|
39
|
+
current_model: string | null;
|
|
40
|
+
models: string[];
|
|
41
|
+
backend: string;
|
|
42
|
+
error: string | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SwitchModelResponse {
|
|
46
|
+
success: boolean;
|
|
47
|
+
previous_model: string | null;
|
|
48
|
+
current_model: string;
|
|
49
|
+
message: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type ConnectionStatus =
|
|
53
|
+
| "connecting"
|
|
54
|
+
| "loading-model"
|
|
55
|
+
| "connected"
|
|
56
|
+
| "disconnected"
|
|
57
|
+
| "error";
|
|
58
|
+
|
|
59
|
+
// Configuration
|
|
60
|
+
let API_URL = "http://127.0.0.1:8000";
|
|
61
|
+
const MAX_RETRIES = 30;
|
|
62
|
+
const RETRY_DELAY = 1000;
|
|
63
|
+
const REQUEST_TIMEOUT = 120000; // 2 minutes for thinking models
|
|
64
|
+
const HEALTH_CHECK_INTERVAL = 10000; // Check health every 10 seconds
|
|
65
|
+
|
|
66
|
+
// Get the API port from Tauri
|
|
67
|
+
async function getApiUrl(): Promise<string> {
|
|
68
|
+
try {
|
|
69
|
+
const port = await invoke<number>("get_api_port");
|
|
70
|
+
API_URL = `http://127.0.0.1:${port}`;
|
|
71
|
+
return API_URL;
|
|
72
|
+
} catch {
|
|
73
|
+
// Fallback for development without Tauri
|
|
74
|
+
return API_URL;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// API functions
|
|
79
|
+
async function apiFetch<T>(
|
|
80
|
+
path: string,
|
|
81
|
+
options: RequestInit = {},
|
|
82
|
+
): Promise<T> {
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const response = await fetch(`${API_URL}${path}`, {
|
|
88
|
+
...options,
|
|
89
|
+
signal: controller.signal,
|
|
90
|
+
headers: {
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
...options.headers,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
const error = await response
|
|
98
|
+
.json()
|
|
99
|
+
.catch(() => ({ error: "Unknown error" }));
|
|
100
|
+
throw new Error(error.error || `HTTP ${response.status}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return response.json();
|
|
104
|
+
} finally {
|
|
105
|
+
clearTimeout(timeoutId);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function checkHealth(): Promise<HealthResponse> {
|
|
110
|
+
return apiFetch<HealthResponse>("/health");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function fetchModels(): Promise<ModelsResponse> {
|
|
114
|
+
return apiFetch<ModelsResponse>("/models");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function switchModel(model: string): Promise<SwitchModelResponse> {
|
|
118
|
+
return apiFetch<SwitchModelResponse>("/models/switch", {
|
|
119
|
+
method: "POST",
|
|
120
|
+
body: JSON.stringify({ model }),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function waitForBackend(): Promise<boolean> {
|
|
125
|
+
// First, get the correct API URL from Tauri
|
|
126
|
+
await getApiUrl();
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < MAX_RETRIES; i++) {
|
|
129
|
+
try {
|
|
130
|
+
const health = await checkHealth();
|
|
131
|
+
if (health.status === "healthy") {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
// Continue retrying
|
|
136
|
+
}
|
|
137
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function sendChatRequest(request: ChatRequest): Promise<ChatResponse> {
|
|
143
|
+
return apiFetch<ChatResponse>("/chat", {
|
|
144
|
+
method: "POST",
|
|
145
|
+
body: JSON.stringify(request),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Hooks
|
|
150
|
+
export function useBackendStatus() {
|
|
151
|
+
const [status, setStatus] = useState<ConnectionStatus>("connecting");
|
|
152
|
+
const [health, setHealth] = useState<HealthResponse | null>(null);
|
|
153
|
+
const [modelInfo, setModelInfo] = useState<ModelsResponse | null>(null);
|
|
154
|
+
const [error, setError] = useState<Error | null>(null);
|
|
155
|
+
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
let mounted = true;
|
|
158
|
+
let healthCheckInterval: ReturnType<typeof setInterval> | null = null;
|
|
159
|
+
|
|
160
|
+
const connect = async () => {
|
|
161
|
+
setStatus("connecting");
|
|
162
|
+
setError(null);
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const ready = await waitForBackend();
|
|
166
|
+
if (!mounted) return;
|
|
167
|
+
|
|
168
|
+
if (ready) {
|
|
169
|
+
// Backend is reachable, now check model status
|
|
170
|
+
setStatus("loading-model");
|
|
171
|
+
|
|
172
|
+
const [healthData, modelsData] = await Promise.all([
|
|
173
|
+
checkHealth(),
|
|
174
|
+
fetchModels().catch(() => null),
|
|
175
|
+
]);
|
|
176
|
+
if (!mounted) return;
|
|
177
|
+
setHealth(healthData);
|
|
178
|
+
setModelInfo(modelsData);
|
|
179
|
+
|
|
180
|
+
// Check if model is loaded
|
|
181
|
+
if (healthData.model_loaded) {
|
|
182
|
+
setStatus("connected");
|
|
183
|
+
} else if (modelsData?.error) {
|
|
184
|
+
setStatus("error");
|
|
185
|
+
setError(new Error(modelsData.error));
|
|
186
|
+
} else {
|
|
187
|
+
// Model not loaded but no error - still connected
|
|
188
|
+
setStatus("connected");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Start periodic health checks
|
|
192
|
+
healthCheckInterval = setInterval(async () => {
|
|
193
|
+
if (!mounted) return;
|
|
194
|
+
try {
|
|
195
|
+
const healthData = await checkHealth();
|
|
196
|
+
if (!mounted) return;
|
|
197
|
+
setHealth(healthData);
|
|
198
|
+
// If we were disconnected but now healthy, reconnect
|
|
199
|
+
setStatus((prev) =>
|
|
200
|
+
prev === "disconnected" ? "connected" : prev,
|
|
201
|
+
);
|
|
202
|
+
} catch {
|
|
203
|
+
if (!mounted) return;
|
|
204
|
+
setStatus("disconnected");
|
|
205
|
+
setError(new Error("Lost connection to backend"));
|
|
206
|
+
}
|
|
207
|
+
}, HEALTH_CHECK_INTERVAL);
|
|
208
|
+
} else {
|
|
209
|
+
setStatus("error");
|
|
210
|
+
setError(
|
|
211
|
+
new Error("Backend failed to start. Check terminal for errors."),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
} catch (err) {
|
|
215
|
+
if (!mounted) return;
|
|
216
|
+
setStatus("error");
|
|
217
|
+
setError(err instanceof Error ? err : new Error("Connection failed"));
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
connect();
|
|
222
|
+
|
|
223
|
+
return () => {
|
|
224
|
+
mounted = false;
|
|
225
|
+
if (healthCheckInterval) {
|
|
226
|
+
clearInterval(healthCheckInterval);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}, []);
|
|
230
|
+
|
|
231
|
+
const retry = useCallback(async () => {
|
|
232
|
+
setStatus("connecting");
|
|
233
|
+
setError(null);
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const ready = await waitForBackend();
|
|
237
|
+
if (ready) {
|
|
238
|
+
const [healthData, modelsData] = await Promise.all([
|
|
239
|
+
checkHealth(),
|
|
240
|
+
fetchModels().catch(() => null),
|
|
241
|
+
]);
|
|
242
|
+
setHealth(healthData);
|
|
243
|
+
setModelInfo(modelsData);
|
|
244
|
+
setStatus("connected");
|
|
245
|
+
} else {
|
|
246
|
+
setStatus("error");
|
|
247
|
+
setError(new Error("Backend failed to become healthy"));
|
|
248
|
+
}
|
|
249
|
+
} catch (err) {
|
|
250
|
+
setStatus("error");
|
|
251
|
+
setError(err instanceof Error ? err : new Error("Connection failed"));
|
|
252
|
+
}
|
|
253
|
+
}, []);
|
|
254
|
+
|
|
255
|
+
const changeModel = useCallback(async (model: string) => {
|
|
256
|
+
try {
|
|
257
|
+
const result = await switchModel(model);
|
|
258
|
+
if (result.success) {
|
|
259
|
+
// Refresh model info
|
|
260
|
+
const modelsData = await fetchModels().catch(() => null);
|
|
261
|
+
setModelInfo(modelsData);
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
} catch (err) {
|
|
265
|
+
throw err instanceof Error ? err : new Error("Failed to switch model");
|
|
266
|
+
}
|
|
267
|
+
}, []);
|
|
268
|
+
|
|
269
|
+
return { status, health, modelInfo, error, retry, changeModel };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function useChat() {
|
|
273
|
+
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
274
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
275
|
+
const [error, setError] = useState<Error | null>(null);
|
|
276
|
+
|
|
277
|
+
const sendMessage = useCallback(
|
|
278
|
+
async (
|
|
279
|
+
content: string,
|
|
280
|
+
options?: Partial<Omit<ChatRequest, "message">>,
|
|
281
|
+
) => {
|
|
282
|
+
const userMessage: ChatMessage = {
|
|
283
|
+
role: "user",
|
|
284
|
+
content,
|
|
285
|
+
images: options?.images,
|
|
286
|
+
timestamp: Date.now(),
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
290
|
+
setIsLoading(true);
|
|
291
|
+
setError(null);
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const response = await sendChatRequest({
|
|
295
|
+
message: content,
|
|
296
|
+
images: options?.images,
|
|
297
|
+
history: messages,
|
|
298
|
+
...options,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const assistantMessage: ChatMessage = {
|
|
302
|
+
role: "assistant",
|
|
303
|
+
content: response.response,
|
|
304
|
+
thinking: response.thinking,
|
|
305
|
+
timestamp: Date.now(),
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
setMessages((prev) => [...prev, assistantMessage]);
|
|
309
|
+
return response;
|
|
310
|
+
} catch (err) {
|
|
311
|
+
const error = err instanceof Error ? err : new Error("Chat failed");
|
|
312
|
+
setError(error);
|
|
313
|
+
throw error;
|
|
314
|
+
} finally {
|
|
315
|
+
setIsLoading(false);
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
[messages],
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const clearMessages = useCallback(() => {
|
|
322
|
+
setMessages([]);
|
|
323
|
+
setError(null);
|
|
324
|
+
}, []);
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
messages,
|
|
328
|
+
isLoading,
|
|
329
|
+
error,
|
|
330
|
+
sendMessage,
|
|
331
|
+
clearMessages,
|
|
332
|
+
setMessages,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--color-bg: #0f0f0f;
|
|
3
|
+
--color-surface: #1a1a1a;
|
|
4
|
+
--color-surface-hover: #2a2a2a;
|
|
5
|
+
--color-border: #333;
|
|
6
|
+
--color-text: #f0f0f0;
|
|
7
|
+
--color-text-muted: #888;
|
|
8
|
+
--color-primary: #646cff;
|
|
9
|
+
--color-primary-hover: #535bf2;
|
|
10
|
+
--color-success: #4ade80;
|
|
11
|
+
--color-error: #f87171;
|
|
12
|
+
--color-warning: #fbbf24;
|
|
13
|
+
--radius: 8px;
|
|
14
|
+
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
* {
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
margin: 0;
|
|
20
|
+
padding: 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
html,
|
|
24
|
+
body {
|
|
25
|
+
height: 100%;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
body {
|
|
29
|
+
font-family: Inter, system-ui, -apple-system, sans-serif;
|
|
30
|
+
background-color: var(--color-bg);
|
|
31
|
+
color: var(--color-text);
|
|
32
|
+
line-height: 1.5;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#root {
|
|
36
|
+
height: 100%;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
button {
|
|
40
|
+
font-family: inherit;
|
|
41
|
+
font-size: 0.9rem;
|
|
42
|
+
font-weight: 500;
|
|
43
|
+
padding: 0.5rem 1rem;
|
|
44
|
+
border: none;
|
|
45
|
+
border-radius: var(--radius);
|
|
46
|
+
background-color: var(--color-primary);
|
|
47
|
+
color: white;
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
transition: background-color 0.2s;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
button:hover {
|
|
53
|
+
background-color: var(--color-primary-hover);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
button:disabled {
|
|
57
|
+
opacity: 0.5;
|
|
58
|
+
cursor: not-allowed;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
input,
|
|
62
|
+
textarea {
|
|
63
|
+
font-family: inherit;
|
|
64
|
+
font-size: 1rem;
|
|
65
|
+
padding: 0.75rem;
|
|
66
|
+
border: 1px solid var(--color-border);
|
|
67
|
+
border-radius: var(--radius);
|
|
68
|
+
background-color: var(--color-surface);
|
|
69
|
+
color: var(--color-text);
|
|
70
|
+
outline: none;
|
|
71
|
+
transition: border-color 0.2s;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
input:focus,
|
|
75
|
+
textarea:focus {
|
|
76
|
+
border-color: var(--color-primary);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.spinner {
|
|
80
|
+
width: 24px;
|
|
81
|
+
height: 24px;
|
|
82
|
+
border: 2px solid var(--color-border);
|
|
83
|
+
border-top-color: var(--color-primary);
|
|
84
|
+
border-radius: 50%;
|
|
85
|
+
animation: spin 0.8s linear infinite;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@keyframes spin {
|
|
89
|
+
to {
|
|
90
|
+
transform: rotate(360deg);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/tether.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>{{PROJECT_NAME}}</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/frontend/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"dev:py": "cd backend && uv run uvicorn app.main:app --reload --port 8000",
|
|
9
|
+
"dev:all": "concurrently \"pnpm dev\" \"pnpm dev:py\"",
|
|
10
|
+
"build": "tsc && vite build",
|
|
11
|
+
"build:app": "pnpm python:build && pnpm build && pnpm tauri build",
|
|
12
|
+
"preview": "vite preview",
|
|
13
|
+
"tauri": "tauri",
|
|
14
|
+
"tauri:dev": "tauri dev",
|
|
15
|
+
"tauri:build": "tauri build",
|
|
16
|
+
"python:build": "cd backend && uv sync --extra build && uv run python scripts/build.py"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"react": "^18.3.0",
|
|
20
|
+
"react-dom": "^18.3.0",
|
|
21
|
+
"react-markdown": "^9.0.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@tauri-apps/api": "^2.0.0",
|
|
25
|
+
"@tauri-apps/cli": "^2.0.0",
|
|
26
|
+
"@types/react": "^18.3.0",
|
|
27
|
+
"@types/react-dom": "^18.3.0",
|
|
28
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
29
|
+
"concurrently": "^9.0.0",
|
|
30
|
+
"typescript": "^5.4.0",
|
|
31
|
+
"vite": "^5.4.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" style="stop-color:#646cff;stop-opacity:1" />
|
|
5
|
+
<stop offset="100%" style="stop-color:#535bf2;stop-opacity:1" />
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<circle cx="50" cy="50" r="45" fill="url(#grad)"/>
|
|
9
|
+
<path d="M30 35 L50 25 L70 35 L70 65 L50 75 L30 65 Z" fill="none" stroke="white" stroke-width="3"/>
|
|
10
|
+
<circle cx="50" cy="50" r="8" fill="white"/>
|
|
11
|
+
<line x1="50" y1="42" x2="50" y2="25" stroke="white" stroke-width="2"/>
|
|
12
|
+
<line x1="50" y1="58" x2="50" y2="75" stroke="white" stroke-width="2"/>
|
|
13
|
+
<line x1="42" y1="50" x2="30" y2="50" stroke="white" stroke-width="2"/>
|
|
14
|
+
<line x1="58" y1="50" x2="70" y2="50" stroke="white" stroke-width="2"/>
|
|
15
|
+
</svg>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Cargo configuration for faster development builds
|
|
2
|
+
# See docs/development.md for more optimization tips
|
|
3
|
+
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# PROFILE SETTINGS (apply to all platforms)
|
|
6
|
+
# =============================================================================
|
|
7
|
+
|
|
8
|
+
[profile.dev]
|
|
9
|
+
opt-level = 0 # Skip optimizations (faster compile)
|
|
10
|
+
lto = false # No link-time optimization
|
|
11
|
+
incremental = true # Incremental compilation
|
|
12
|
+
codegen-units = 256 # More parallelism (faster compile, slower runtime)
|
|
13
|
+
|
|
14
|
+
[profile.dev.package."*"]
|
|
15
|
+
opt-level = 2 # Optimize dependencies (they rarely change)
|
|
16
|
+
|
|
17
|
+
[profile.release]
|
|
18
|
+
lto = "thin" # Thin LTO (balance of speed and size)
|
|
19
|
+
strip = true # Strip debug symbols
|
|
20
|
+
codegen-units = 1 # Better optimization
|
|
21
|
+
|
|
22
|
+
# =============================================================================
|
|
23
|
+
# PLATFORM-SPECIFIC OPTIMIZATIONS (auto-applied based on target)
|
|
24
|
+
# =============================================================================
|
|
25
|
+
|
|
26
|
+
# macOS - Apple Silicon
|
|
27
|
+
[target.aarch64-apple-darwin]
|
|
28
|
+
rustflags = ["-C", "split-debuginfo=unpacked"]
|
|
29
|
+
|
|
30
|
+
# macOS - Intel
|
|
31
|
+
[target.x86_64-apple-darwin]
|
|
32
|
+
rustflags = ["-C", "split-debuginfo=unpacked"]
|
|
33
|
+
|
|
34
|
+
# Linux - x86_64
|
|
35
|
+
[target.x86_64-unknown-linux-gnu]
|
|
36
|
+
rustflags = ["-C", "split-debuginfo=unpacked"]
|
|
37
|
+
|
|
38
|
+
# Linux - ARM64 (Raspberry Pi, AWS Graviton, etc.)
|
|
39
|
+
[target.aarch64-unknown-linux-gnu]
|
|
40
|
+
rustflags = ["-C", "split-debuginfo=unpacked"]
|
|
41
|
+
|
|
42
|
+
# Windows - No split-debuginfo (not supported), uses default linker
|
|
43
|
+
# [target.x86_64-pc-windows-msvc]
|
|
44
|
+
# Uses default settings
|
|
45
|
+
|
|
46
|
+
# =============================================================================
|
|
47
|
+
# OPTIONAL: FASTER LINKERS
|
|
48
|
+
# =============================================================================
|
|
49
|
+
# Uncomment your platform section below AND modify the rustflags above
|
|
50
|
+
# to combine with split-debuginfo.
|
|
51
|
+
#
|
|
52
|
+
# macOS (requires: brew install llvm):
|
|
53
|
+
# Add to aarch64-apple-darwin or x86_64-apple-darwin above:
|
|
54
|
+
# rustflags = ["-C", "split-debuginfo=unpacked", "-C", "link-arg=-fuse-ld=/opt/homebrew/opt/llvm/bin/ld64.lld"]
|
|
55
|
+
#
|
|
56
|
+
# Linux (requires: sudo apt install mold OR sudo apt install lld):
|
|
57
|
+
# Add to x86_64-unknown-linux-gnu above:
|
|
58
|
+
# rustflags = ["-C", "split-debuginfo=unpacked", "-C", "link-arg=-fuse-ld=mold"]
|
|
59
|
+
#
|
|
60
|
+
# =============================================================================
|
|
61
|
+
# SCCACHE (Recommended for all platforms)
|
|
62
|
+
# =============================================================================
|
|
63
|
+
# Caches compilation results across all Rust projects.
|
|
64
|
+
# Install: brew install sccache (macOS) or cargo install sccache
|
|
65
|
+
# Enable: export RUSTC_WRAPPER=sccache (add to shell profile)
|
|
66
|
+
# =============================================================================
|