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.
Files changed (48) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +729 -0
  3. package/package.json +59 -0
  4. package/template/.env.example +18 -0
  5. package/template/README.md.template +123 -0
  6. package/template/backend/app/__init__.py.template +5 -0
  7. package/template/backend/app/main.py +66 -0
  8. package/template/backend/app/routes/__init__.py +3 -0
  9. package/template/backend/app/routes/chat.py +151 -0
  10. package/template/backend/app/routes/health.py +28 -0
  11. package/template/backend/app/routes/models.py +126 -0
  12. package/template/backend/app/services/__init__.py +3 -0
  13. package/template/backend/app/services/llm.py +526 -0
  14. package/template/backend/pyproject.toml.template +34 -0
  15. package/template/backend/scripts/build.py +112 -0
  16. package/template/frontend/App.css +58 -0
  17. package/template/frontend/App.tsx +62 -0
  18. package/template/frontend/components/Chat.css +220 -0
  19. package/template/frontend/components/Chat.tsx +284 -0
  20. package/template/frontend/components/ChatMessage.css +206 -0
  21. package/template/frontend/components/ChatMessage.tsx +62 -0
  22. package/template/frontend/components/ModelStatus.css +62 -0
  23. package/template/frontend/components/ModelStatus.tsx +103 -0
  24. package/template/frontend/hooks/useApi.ts +334 -0
  25. package/template/frontend/index.css +92 -0
  26. package/template/frontend/main.tsx +10 -0
  27. package/template/frontend/vite-env.d.ts +1 -0
  28. package/template/index.html.template +13 -0
  29. package/template/package.json.template +33 -0
  30. package/template/postcss.config.js.template +6 -0
  31. package/template/public/tether.svg +15 -0
  32. package/template/src-tauri/.cargo/config.toml +66 -0
  33. package/template/src-tauri/Cargo.lock +4764 -0
  34. package/template/src-tauri/Cargo.toml +24 -0
  35. package/template/src-tauri/build.rs +3 -0
  36. package/template/src-tauri/capabilities/default.json +40 -0
  37. package/template/src-tauri/icons/128x128.png +0 -0
  38. package/template/src-tauri/icons/128x128@2x.png +0 -0
  39. package/template/src-tauri/icons/32x32.png +0 -0
  40. package/template/src-tauri/icons/icon.icns +0 -0
  41. package/template/src-tauri/icons/icon.ico +0 -0
  42. package/template/src-tauri/src/main.rs +65 -0
  43. package/template/src-tauri/src/sidecar.rs +110 -0
  44. package/template/src-tauri/tauri.conf.json.template +44 -0
  45. package/template/tailwind.config.js.template +19 -0
  46. package/template/tsconfig.json +21 -0
  47. package/template/tsconfig.node.json +11 -0
  48. 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,10 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import App from "./App";
4
+ import "./index.css";
5
+
6
+ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ );
@@ -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,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
@@ -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
+ # =============================================================================