fluxy-bot 0.8.9 → 0.9.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/README.md +53 -84
- package/bin/cli.js +1717 -6
- package/components.json +18 -18
- package/package.json +98 -109
- package/postcss.config.js +3 -3
- package/scripts/install.ps1 +1 -1
- package/scripts/install.sh +1 -1
- package/scripts/postinstall.js +47 -58
- package/shared/ai.ts +118 -202
- package/shared/config.ts +37 -39
- package/shared/logger.ts +6 -14
- package/shared/paths.ts +10 -16
- package/shared/relay.ts +89 -98
- package/supervisor/backend.ts +77 -89
- package/supervisor/chat/ARCHITECTURE.md +13 -14
- package/supervisor/chat/OnboardWizard.tsx +2194 -3419
- package/supervisor/chat/fluxy-main.tsx +495 -633
- package/supervisor/chat/fluxy.html +24 -36
- package/supervisor/chat/onboard-main.tsx +8 -8
- package/supervisor/chat/onboard.html +10 -13
- package/supervisor/chat/src/components/Chat/AudioBubble.tsx +94 -108
- package/supervisor/chat/src/components/Chat/ChatView.tsx +14 -36
- package/supervisor/chat/src/components/Chat/ImageLightbox.tsx +71 -89
- package/supervisor/chat/src/components/Chat/InputBar.tsx +498 -728
- package/supervisor/chat/src/components/Chat/MessageBubble.tsx +154 -246
- package/supervisor/chat/src/components/Chat/MessageList.tsx +114 -138
- package/supervisor/chat/src/components/Chat/TypingIndicator.tsx +38 -50
- package/supervisor/chat/src/components/LoginScreen.tsx +245 -296
- package/supervisor/chat/src/hooks/useChat.ts +210 -259
- package/supervisor/chat/src/hooks/useFluxyChat.ts +232 -322
- package/supervisor/chat/src/hooks/useSpeechRecognition.ts +126 -148
- package/supervisor/chat/src/lib/auth.ts +20 -23
- package/supervisor/chat/src/lib/ws-client.ts +113 -125
- package/supervisor/chat/src/styles/globals.css +76 -85
- package/supervisor/file-saver.ts +39 -52
- package/supervisor/fluxy-agent.ts +213 -292
- package/supervisor/index.ts +895 -1263
- package/supervisor/scheduler.ts +237 -299
- package/supervisor/tunnel.ts +113 -167
- package/supervisor/vite-dev.ts +51 -61
- package/supervisor/widget.js +135 -147
- package/supervisor/worker.ts +45 -49
- package/tsconfig.json +18 -24
- package/vite.config.ts +48 -55
- package/vite.fluxy.config.ts +34 -34
- package/worker/claude-auth.ts +202 -265
- package/worker/codex-auth.ts +144 -181
- package/worker/db.ts +63 -191
- package/worker/index.ts +601 -842
- package/workspace/MYSELF.md +1 -1
- package/workspace/PULSE.json +6 -6
- package/workspace/backend/index.ts +16 -19
- package/workspace/client/index.html +47 -60
- package/workspace/client/public/manifest.json +21 -21
- package/workspace/client/public/sw.js +29 -31
- package/workspace/client/src/App.tsx +91 -136
- package/workspace/client/src/components/Dashboard/DashboardPage.tsx +68 -72
- package/workspace/client/src/components/ErrorBoundary.tsx +11 -11
- package/workspace/client/src/components/Layout/DashboardLayout.tsx +35 -35
- package/workspace/client/src/components/Layout/Footer.tsx +12 -12
- package/workspace/client/src/components/Layout/MobileNav.tsx +23 -23
- package/workspace/client/src/components/Layout/Sidebar.tsx +81 -81
- package/workspace/client/src/components/ui/avatar.tsx +85 -85
- package/workspace/client/src/components/ui/badge.tsx +40 -41
- package/workspace/client/src/components/ui/button.tsx +56 -57
- package/workspace/client/src/components/ui/card.tsx +75 -75
- package/workspace/client/src/components/ui/dialog.tsx +114 -117
- package/workspace/client/src/components/ui/dropdown-menu.tsx +191 -199
- package/workspace/client/src/components/ui/input.tsx +17 -17
- package/workspace/client/src/components/ui/scroll-area.tsx +47 -47
- package/workspace/client/src/components/ui/select.tsx +145 -151
- package/workspace/client/src/components/ui/separator.tsx +21 -21
- package/workspace/client/src/components/ui/sheet.tsx +102 -102
- package/workspace/client/src/components/ui/skeleton.tsx +10 -10
- package/workspace/client/src/components/ui/switch.tsx +26 -26
- package/workspace/client/src/components/ui/tabs.tsx +69 -69
- package/workspace/client/src/components/ui/textarea.tsx +14 -14
- package/workspace/client/src/components/ui/tooltip.tsx +37 -37
- package/workspace/client/src/lib/utils.ts +1 -1
- package/workspace/client/src/main.tsx +3 -3
- package/workspace/client/src/styles/globals.css +76 -85
- package/workspace/skills/code-reviewer/.claude-plugin/plugin.json +3 -3
- package/workspace/skills/code-reviewer/skills/code-reviewer/SKILL.md +0 -6
- package/workspace/skills/daily-standup/.claude-plugin/plugin.json +3 -3
- package/workspace/skills/daily-standup/skills/daily-standup/SKILL.md +0 -7
- package/workspace/skills/workspace-helper/.claude-plugin/plugin.json +3 -3
- package/workspace/skills/workspace-helper/skills/workspace-helper/SKILL.md +0 -2
- package/bin/cli.backup.js +0 -2138
- package/cli/commands/daemon.ts +0 -42
- package/cli/commands/init.ts +0 -32
- package/cli/commands/start.ts +0 -113
- package/cli/commands/tunnel.temp.ts +0 -206
- package/cli/commands/tunnel.ts +0 -227
- package/cli/commands/update.ts +0 -163
- package/cli/core/base-adapter.ts +0 -156
- package/cli/core/cloudflared.ts +0 -113
- package/cli/core/config.ts +0 -73
- package/cli/core/os-detector.ts +0 -43
- package/cli/core/server.ts +0 -109
- package/cli/core/types.ts +0 -15
- package/cli/index.ts +0 -72
- package/cli/platforms/darwin.ts +0 -146
- package/cli/platforms/index.ts +0 -21
- package/cli/platforms/linux.ts +0 -156
- package/cli/platforms/win32.ts +0 -34
- package/cli/utils/ui.ts +0 -37
|
@@ -1,667 +1,529 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import ReactDOM from 'react-dom/client';
|
|
3
|
-
import {
|
|
4
|
-
ArrowLeft,
|
|
5
|
-
MoreVertical,
|
|
6
|
-
Trash2,
|
|
7
|
-
Wand2,
|
|
8
|
-
Download,
|
|
9
|
-
X,
|
|
10
|
-
Share,
|
|
11
|
-
Bell,
|
|
12
|
-
BellRing,
|
|
13
|
-
BellOff
|
|
14
|
-
} from 'lucide-react';
|
|
3
|
+
import { ArrowLeft, MoreVertical, Trash2, Wand2, Download, X, Share, Bell, BellRing, BellOff } from 'lucide-react';
|
|
15
4
|
import { WsClient } from './src/lib/ws-client';
|
|
16
5
|
import { useFluxyChat } from './src/hooks/useFluxyChat';
|
|
17
6
|
import OnboardWizard from './OnboardWizard';
|
|
18
7
|
import LoginScreen from './src/components/LoginScreen';
|
|
19
|
-
import {
|
|
20
|
-
getAuthToken,
|
|
21
|
-
setAuthToken,
|
|
22
|
-
clearAuthToken,
|
|
23
|
-
authFetch,
|
|
24
|
-
onAuthFailure
|
|
25
|
-
} from './src/lib/auth';
|
|
8
|
+
import { getAuthToken, setAuthToken, clearAuthToken, authFetch, onAuthFailure } from './src/lib/auth';
|
|
26
9
|
import MessageList from './src/components/Chat/MessageList';
|
|
27
10
|
import InputBar from './src/components/Chat/InputBar';
|
|
28
11
|
import './src/styles/globals.css';
|
|
29
12
|
|
|
30
13
|
function FluxyApp() {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
14
|
+
const clientRef = useRef<WsClient | null>(null);
|
|
15
|
+
const [connected, setConnected] = useState(false);
|
|
16
|
+
const [botName, setBotName] = useState('Fluxy');
|
|
17
|
+
const [whisperEnabled, setWhisperEnabled] = useState(false);
|
|
18
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
19
|
+
const [showWizard, setShowWizard] = useState(false);
|
|
20
|
+
const [reloadTrigger, setReloadTrigger] = useState(0);
|
|
21
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
22
|
+
const wasConnected = useRef(false);
|
|
23
|
+
|
|
24
|
+
// Push notifications
|
|
25
|
+
const [pushState, setPushState] = useState<'loading' | 'unsupported' | 'denied' | 'subscribed' | 'unsubscribed'>('loading');
|
|
26
|
+
|
|
27
|
+
function urlBase64ToUint8Array(base64String: string) {
|
|
28
|
+
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
|
29
|
+
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
|
30
|
+
const raw = atob(base64);
|
|
31
|
+
const arr = new Uint8Array(raw.length);
|
|
32
|
+
for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
|
|
33
|
+
return arr;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function subscribePush() {
|
|
37
|
+
try {
|
|
38
|
+
const permission = await Notification.requestPermission();
|
|
39
|
+
if (permission !== 'granted') { setPushState('denied'); return; }
|
|
40
|
+
|
|
41
|
+
const reg = await navigator.serviceWorker.ready;
|
|
42
|
+
const res = await fetch('/api/push/vapid-public-key');
|
|
43
|
+
const { publicKey } = await res.json();
|
|
44
|
+
|
|
45
|
+
const subscription = await reg.pushManager.subscribe({
|
|
46
|
+
userVisibleOnly: true,
|
|
47
|
+
applicationServerKey: urlBase64ToUint8Array(publicKey),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const sub = subscription.toJSON();
|
|
51
|
+
await authFetch('/api/push/subscribe', {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: { 'Content-Type': 'application/json' },
|
|
54
|
+
body: JSON.stringify({ endpoint: sub.endpoint, keys: sub.keys }),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
setPushState('subscribed');
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error('[push] Subscribe failed:', err);
|
|
55
60
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const sub = subscription.toJSON();
|
|
75
|
-
await authFetch('/api/push/subscribe', {
|
|
76
|
-
method: 'POST',
|
|
77
|
-
headers: { 'Content-Type': 'application/json' },
|
|
78
|
-
body: JSON.stringify({ endpoint: sub.endpoint, keys: sub.keys })
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
setPushState('subscribed');
|
|
82
|
-
} catch (err) {
|
|
83
|
-
console.error('[push] Subscribe failed:', err);
|
|
84
|
-
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function unsubscribePush() {
|
|
64
|
+
try {
|
|
65
|
+
const reg = await navigator.serviceWorker.ready;
|
|
66
|
+
const subscription = await reg.pushManager.getSubscription();
|
|
67
|
+
if (subscription) {
|
|
68
|
+
const endpoint = subscription.endpoint;
|
|
69
|
+
await subscription.unsubscribe();
|
|
70
|
+
await authFetch('/api/push/unsubscribe', {
|
|
71
|
+
method: 'DELETE',
|
|
72
|
+
headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
body: JSON.stringify({ endpoint }),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
setPushState('unsubscribed');
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error('[push] Unsubscribe failed:', err);
|
|
85
79
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Install App (PWA)
|
|
83
|
+
const [showIosModal, setShowIosModal] = useState(false);
|
|
84
|
+
const isIos = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
85
|
+
const isStandalone = window.matchMedia('(display-mode: standalone)').matches || (navigator as any).standalone;
|
|
86
|
+
const isMobile = /Android|iPhone|iPad|iPod|webOS|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
87
|
+
const isInIframe = window.self !== window.top;
|
|
88
|
+
|
|
89
|
+
// Listen for install result from parent (when in iframe)
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const handler = (e: MessageEvent) => {
|
|
92
|
+
if (e.data?.type === 'fluxy:show-ios-install') {
|
|
93
|
+
setShowIosModal(true);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
window.addEventListener('message', handler);
|
|
97
|
+
return () => window.removeEventListener('message', handler);
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
// Auth state
|
|
101
|
+
const [authChecked, setAuthChecked] = useState(false);
|
|
102
|
+
const [authRequired, setAuthRequired] = useState(false);
|
|
103
|
+
const [authenticated, setAuthenticated] = useState(false);
|
|
104
|
+
const [totpEnabled, setTotpEnabled] = useState(false);
|
|
105
|
+
|
|
106
|
+
// Check auth on mount
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
(async () => {
|
|
109
|
+
try {
|
|
110
|
+
const res = await fetch('/api/onboard/status');
|
|
111
|
+
const data = await res.json();
|
|
112
|
+
if (data.totpEnabled) setTotpEnabled(true);
|
|
113
|
+
|
|
114
|
+
if (!data.portalConfigured) {
|
|
115
|
+
// No password set — skip auth entirely
|
|
116
|
+
setAuthRequired(false);
|
|
117
|
+
setAuthenticated(true);
|
|
118
|
+
setAuthChecked(true);
|
|
119
|
+
return;
|
|
103
120
|
}
|
|
104
|
-
}
|
|
105
121
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const handler = (e: MessageEvent) => {
|
|
121
|
-
if (e.data?.type === 'fluxy:show-ios-install') {
|
|
122
|
-
setShowIosModal(true);
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
window.addEventListener('message', handler);
|
|
126
|
-
return () => window.removeEventListener('message', handler);
|
|
127
|
-
}, []);
|
|
128
|
-
|
|
129
|
-
// Auth state
|
|
130
|
-
const [authChecked, setAuthChecked] = useState(false);
|
|
131
|
-
const [authRequired, setAuthRequired] = useState(false);
|
|
132
|
-
const [authenticated, setAuthenticated] = useState(false);
|
|
133
|
-
const [totpEnabled, setTotpEnabled] = useState(false);
|
|
134
|
-
|
|
135
|
-
// Check auth on mount
|
|
136
|
-
useEffect(() => {
|
|
137
|
-
(async () => {
|
|
138
|
-
try {
|
|
139
|
-
const res = await fetch('/api/onboard/status');
|
|
140
|
-
const data = await res.json();
|
|
141
|
-
if (data.totpEnabled) setTotpEnabled(true);
|
|
142
|
-
|
|
143
|
-
if (!data.portalConfigured) {
|
|
144
|
-
// No password set — skip auth entirely
|
|
145
|
-
setAuthRequired(false);
|
|
146
|
-
setAuthenticated(true);
|
|
147
|
-
setAuthChecked(true);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
setAuthRequired(true);
|
|
152
|
-
|
|
153
|
-
// Check if we have a valid token in localStorage
|
|
154
|
-
const token = getAuthToken();
|
|
155
|
-
if (token) {
|
|
156
|
-
const vRes = await fetch(
|
|
157
|
-
`/api/portal/validate-token?token=${encodeURIComponent(token)}`
|
|
158
|
-
);
|
|
159
|
-
const vData = await vRes.json();
|
|
160
|
-
if (vData.valid) {
|
|
161
|
-
setAuthenticated(true);
|
|
162
|
-
setAuthChecked(true);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
clearAuthToken();
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
setAuthenticated(false);
|
|
169
|
-
setAuthChecked(true);
|
|
170
|
-
} catch {
|
|
171
|
-
// Worker not ready — skip auth, let it retry later
|
|
172
|
-
setAuthenticated(true);
|
|
173
|
-
setAuthChecked(true);
|
|
174
|
-
}
|
|
175
|
-
})();
|
|
176
|
-
}, []);
|
|
122
|
+
setAuthRequired(true);
|
|
123
|
+
|
|
124
|
+
// Check if we have a valid token in localStorage
|
|
125
|
+
const token = getAuthToken();
|
|
126
|
+
if (token) {
|
|
127
|
+
const vRes = await fetch(`/api/portal/validate-token?token=${encodeURIComponent(token)}`);
|
|
128
|
+
const vData = await vRes.json();
|
|
129
|
+
if (vData.valid) {
|
|
130
|
+
setAuthenticated(true);
|
|
131
|
+
setAuthChecked(true);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
clearAuthToken();
|
|
135
|
+
}
|
|
177
136
|
|
|
178
|
-
|
|
179
|
-
|
|
137
|
+
setAuthenticated(false);
|
|
138
|
+
setAuthChecked(true);
|
|
139
|
+
} catch {
|
|
140
|
+
// Worker not ready — skip auth, let it retry later
|
|
180
141
|
setAuthenticated(true);
|
|
142
|
+
setAuthChecked(true);
|
|
143
|
+
}
|
|
144
|
+
})();
|
|
145
|
+
}, []);
|
|
146
|
+
|
|
147
|
+
const handleLogin = (token: string) => {
|
|
148
|
+
setAuthToken(token);
|
|
149
|
+
setAuthenticated(true);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Handle mid-session token expiry (authFetch gets 401)
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
onAuthFailure(() => {
|
|
155
|
+
setAuthenticated(false);
|
|
156
|
+
setAuthRequired(true);
|
|
157
|
+
});
|
|
158
|
+
}, []);
|
|
159
|
+
|
|
160
|
+
// Check push state on mount
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (!authenticated) return;
|
|
163
|
+
(async () => {
|
|
164
|
+
if (!('PushManager' in window) || !('serviceWorker' in navigator)) {
|
|
165
|
+
setPushState('unsupported');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (Notification.permission === 'denied') {
|
|
169
|
+
setPushState('denied');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const reg = await navigator.serviceWorker.ready;
|
|
174
|
+
const subscription = await reg.pushManager.getSubscription();
|
|
175
|
+
if (subscription) {
|
|
176
|
+
const res = await fetch(`/api/push/status?endpoint=${encodeURIComponent(subscription.endpoint)}`);
|
|
177
|
+
const data = await res.json();
|
|
178
|
+
setPushState(data.subscribed ? 'subscribed' : 'unsubscribed');
|
|
179
|
+
} else {
|
|
180
|
+
setPushState('unsubscribed');
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
setPushState('unsupported');
|
|
184
|
+
}
|
|
185
|
+
})();
|
|
186
|
+
}, [authenticated]);
|
|
187
|
+
|
|
188
|
+
// Connect WebSocket only when authenticated
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (!authenticated) return;
|
|
191
|
+
|
|
192
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
193
|
+
const host = location.host;
|
|
194
|
+
const client = new WsClient(`${proto}//${host}/fluxy/ws`, getAuthToken);
|
|
195
|
+
clientRef.current = client;
|
|
196
|
+
|
|
197
|
+
const unsub = client.onStatus((isConnected) => {
|
|
198
|
+
setConnected(isConnected);
|
|
199
|
+
// On reconnect, trigger a reload from DB to catch missed messages
|
|
200
|
+
if (isConnected && wasConnected.current) {
|
|
201
|
+
setReloadTrigger((n) => n + 1);
|
|
202
|
+
}
|
|
203
|
+
wasConnected.current = isConnected;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Forward rebuild/HMR events to parent (dashboard) via postMessage
|
|
207
|
+
const unsubRebuilding = client.on('app:rebuilding', () => {
|
|
208
|
+
console.log('[fluxy] app:rebuilding received');
|
|
209
|
+
window.parent?.postMessage({ type: 'fluxy:rebuilding' }, '*');
|
|
210
|
+
});
|
|
211
|
+
const unsubRebuilt = client.on('app:rebuilt', () => {
|
|
212
|
+
console.log('[fluxy] app:rebuilt received');
|
|
213
|
+
window.parent?.postMessage({ type: 'fluxy:rebuilt' }, '*');
|
|
214
|
+
});
|
|
215
|
+
const unsubBuildError = client.on('app:build-error', (data: { error: string }) => {
|
|
216
|
+
console.log('[fluxy] app:build-error received:', data.error);
|
|
217
|
+
window.parent?.postMessage({ type: 'fluxy:build-error', error: data.error }, '*');
|
|
218
|
+
});
|
|
219
|
+
const unsubHmr = client.on('app:hmr-update', () => {
|
|
220
|
+
console.log('[fluxy] Vite HMR update — changes applied automatically');
|
|
221
|
+
window.parent?.postMessage({ type: 'fluxy:hmr-update' }, '*');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
client.connect();
|
|
225
|
+
|
|
226
|
+
return () => {
|
|
227
|
+
unsub();
|
|
228
|
+
unsubRebuilding();
|
|
229
|
+
unsubRebuilt();
|
|
230
|
+
unsubBuildError();
|
|
231
|
+
unsubHmr();
|
|
232
|
+
client.disconnect();
|
|
181
233
|
};
|
|
234
|
+
}, [authenticated]);
|
|
235
|
+
|
|
236
|
+
// Try to load settings (will work when worker is up, fail silently when down)
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
if (!authenticated) return;
|
|
239
|
+
authFetch('/api/settings')
|
|
240
|
+
.then((r) => r.json())
|
|
241
|
+
.then((s) => {
|
|
242
|
+
if (s.agent_name) setBotName(s.agent_name);
|
|
243
|
+
if (s.whisper_enabled === 'true') setWhisperEnabled(true);
|
|
244
|
+
})
|
|
245
|
+
.catch(() => {});
|
|
246
|
+
}, [authenticated]);
|
|
247
|
+
|
|
248
|
+
// Close menu on outside click
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
if (!menuOpen) return;
|
|
251
|
+
const handler = (e: MouseEvent) => {
|
|
252
|
+
if (menuRef.current && !menuRef.current.contains(e.target as Node)) setMenuOpen(false);
|
|
253
|
+
};
|
|
254
|
+
document.addEventListener('mousedown', handler);
|
|
255
|
+
return () => document.removeEventListener('mousedown', handler);
|
|
256
|
+
}, [menuOpen]);
|
|
182
257
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
onAuthFailure(() => {
|
|
186
|
-
setAuthenticated(false);
|
|
187
|
-
setAuthRequired(true);
|
|
188
|
-
});
|
|
189
|
-
}, []);
|
|
190
|
-
|
|
191
|
-
// Check push state on mount
|
|
192
|
-
useEffect(() => {
|
|
193
|
-
if (!authenticated) return;
|
|
194
|
-
(async () => {
|
|
195
|
-
if (!('PushManager' in window) || !('serviceWorker' in navigator)) {
|
|
196
|
-
setPushState('unsupported');
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
if (Notification.permission === 'denied') {
|
|
200
|
-
setPushState('denied');
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
try {
|
|
204
|
-
const reg = await navigator.serviceWorker.ready;
|
|
205
|
-
const subscription = await reg.pushManager.getSubscription();
|
|
206
|
-
if (subscription) {
|
|
207
|
-
const res = await fetch(
|
|
208
|
-
`/api/push/status?endpoint=${encodeURIComponent(subscription.endpoint)}`
|
|
209
|
-
);
|
|
210
|
-
const data = await res.json();
|
|
211
|
-
setPushState(
|
|
212
|
-
data.subscribed ? 'subscribed' : 'unsubscribed'
|
|
213
|
-
);
|
|
214
|
-
} else {
|
|
215
|
-
setPushState('unsubscribed');
|
|
216
|
-
}
|
|
217
|
-
} catch {
|
|
218
|
-
setPushState('unsupported');
|
|
219
|
-
}
|
|
220
|
-
})();
|
|
221
|
-
}, [authenticated]);
|
|
222
|
-
|
|
223
|
-
// Connect WebSocket only when authenticated
|
|
224
|
-
useEffect(() => {
|
|
225
|
-
if (!authenticated) return;
|
|
226
|
-
|
|
227
|
-
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
228
|
-
const host = location.host;
|
|
229
|
-
const client = new WsClient(`${proto}//${host}/fluxy/ws`, getAuthToken);
|
|
230
|
-
clientRef.current = client;
|
|
231
|
-
|
|
232
|
-
const unsub = client.onStatus(isConnected => {
|
|
233
|
-
setConnected(isConnected);
|
|
234
|
-
// On reconnect, trigger a reload from DB to catch missed messages
|
|
235
|
-
if (isConnected && wasConnected.current) {
|
|
236
|
-
setReloadTrigger(n => n + 1);
|
|
237
|
-
}
|
|
238
|
-
wasConnected.current = isConnected;
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
// Forward rebuild/HMR events to parent (dashboard) via postMessage
|
|
242
|
-
const unsubRebuilding = client.on('app:rebuilding', () => {
|
|
243
|
-
console.log('[fluxy] app:rebuilding received');
|
|
244
|
-
window.parent?.postMessage({ type: 'fluxy:rebuilding' }, '*');
|
|
245
|
-
});
|
|
246
|
-
const unsubRebuilt = client.on('app:rebuilt', () => {
|
|
247
|
-
console.log('[fluxy] app:rebuilt received');
|
|
248
|
-
window.parent?.postMessage({ type: 'fluxy:rebuilt' }, '*');
|
|
249
|
-
});
|
|
250
|
-
const unsubBuildError = client.on(
|
|
251
|
-
'app:build-error',
|
|
252
|
-
(data: { error: string }) => {
|
|
253
|
-
console.log('[fluxy] app:build-error received:', data.error);
|
|
254
|
-
window.parent?.postMessage(
|
|
255
|
-
{ type: 'fluxy:build-error', error: data.error },
|
|
256
|
-
'*'
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
);
|
|
260
|
-
const unsubHmr = client.on('app:hmr-update', () => {
|
|
261
|
-
console.log(
|
|
262
|
-
'[fluxy] Vite HMR update — changes applied automatically'
|
|
263
|
-
);
|
|
264
|
-
window.parent?.postMessage({ type: 'fluxy:hmr-update' }, '*');
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
client.connect();
|
|
268
|
-
|
|
269
|
-
return () => {
|
|
270
|
-
unsub();
|
|
271
|
-
unsubRebuilding();
|
|
272
|
-
unsubRebuilt();
|
|
273
|
-
unsubBuildError();
|
|
274
|
-
unsubHmr();
|
|
275
|
-
client.disconnect();
|
|
276
|
-
};
|
|
277
|
-
}, [authenticated]);
|
|
278
|
-
|
|
279
|
-
// Try to load settings (will work when worker is up, fail silently when down)
|
|
280
|
-
useEffect(() => {
|
|
281
|
-
if (!authenticated) return;
|
|
282
|
-
authFetch('/api/settings')
|
|
283
|
-
.then(r => r.json())
|
|
284
|
-
.then(s => {
|
|
285
|
-
if (s.agent_name) setBotName(s.agent_name);
|
|
286
|
-
if (s.whisper_enabled === 'true') setWhisperEnabled(true);
|
|
287
|
-
})
|
|
288
|
-
.catch(() => {});
|
|
289
|
-
}, [authenticated]);
|
|
290
|
-
|
|
291
|
-
// Close menu on outside click
|
|
292
|
-
useEffect(() => {
|
|
293
|
-
if (!menuOpen) return;
|
|
294
|
-
const handler = (e: MouseEvent) => {
|
|
295
|
-
if (menuRef.current && !menuRef.current.contains(e.target as Node))
|
|
296
|
-
setMenuOpen(false);
|
|
297
|
-
};
|
|
298
|
-
document.addEventListener('mousedown', handler);
|
|
299
|
-
return () => document.removeEventListener('mousedown', handler);
|
|
300
|
-
}, [menuOpen]);
|
|
301
|
-
|
|
302
|
-
const {
|
|
303
|
-
messages,
|
|
304
|
-
streaming,
|
|
305
|
-
streamBuffer,
|
|
306
|
-
tools,
|
|
307
|
-
hasMore,
|
|
308
|
-
loadOlder,
|
|
309
|
-
sendMessage,
|
|
310
|
-
stopStreaming,
|
|
311
|
-
clearContext
|
|
312
|
-
} = useFluxyChat(clientRef.current, reloadTrigger, authenticated);
|
|
313
|
-
|
|
314
|
-
// Auth gate: show spinner while checking, login screen if needed
|
|
315
|
-
if (!authChecked) {
|
|
316
|
-
return (
|
|
317
|
-
<div className="flex items-center justify-center h-dvh">
|
|
318
|
-
<div className="w-6 h-6 border-2 border-white/10 border-t-white/50 rounded-full animate-spin" />
|
|
319
|
-
</div>
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (authRequired && !authenticated) {
|
|
324
|
-
return <LoginScreen onLogin={handleLogin} totpEnabled={totpEnabled} />;
|
|
325
|
-
}
|
|
258
|
+
const { messages, streaming, streamBuffer, tools, hasMore, loadOlder, sendMessage, stopStreaming, clearContext } =
|
|
259
|
+
useFluxyChat(clientRef.current, reloadTrigger, authenticated);
|
|
326
260
|
|
|
261
|
+
// Auth gate: show spinner while checking, login screen if needed
|
|
262
|
+
if (!authChecked) {
|
|
327
263
|
return (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
264
|
+
<div className="flex items-center justify-center h-dvh">
|
|
265
|
+
<div className="w-6 h-6 border-2 border-white/10 border-t-white/50 rounded-full animate-spin" />
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (authRequired && !authenticated) {
|
|
271
|
+
return <LoginScreen onLogin={handleLogin} totpEnabled={totpEnabled} />;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<div className="flex flex-col h-dvh overflow-hidden">
|
|
276
|
+
{/* Header */}
|
|
277
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b border-border shrink-0">
|
|
278
|
+
<button
|
|
279
|
+
onClick={() => window.parent?.postMessage({ type: 'fluxy:close' }, '*')}
|
|
280
|
+
className="flex items-center justify-center h-7 w-7 -ml-1 rounded-full text-muted-foreground hover:text-foreground hover:bg-white/[0.06] transition-colors"
|
|
281
|
+
aria-label="Close chat"
|
|
282
|
+
>
|
|
283
|
+
<ArrowLeft className="h-5 w-5" />
|
|
284
|
+
</button>
|
|
285
|
+
<img src="/fluxy.png" alt={botName} className="h-5 w-auto" />
|
|
286
|
+
<span className="text-sm font-semibold">{botName}</span>
|
|
287
|
+
<div className={`h-2 w-2 rounded-full ${connected ? 'bg-green-500' : 'bg-red-500'}`} />
|
|
288
|
+
{pushState !== 'loading' && pushState !== 'unsupported' && (
|
|
289
|
+
<button
|
|
290
|
+
onClick={() => {
|
|
291
|
+
if (pushState === 'subscribed') unsubscribePush();
|
|
292
|
+
else if (pushState === 'unsubscribed') subscribePush();
|
|
293
|
+
}}
|
|
294
|
+
disabled={pushState === 'denied'}
|
|
295
|
+
className="flex items-center justify-center h-7 w-7 rounded-full text-muted-foreground hover:text-foreground hover:bg-white/[0.06] transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
296
|
+
title={
|
|
297
|
+
pushState === 'denied' ? 'Notifications blocked — enable in browser settings' :
|
|
298
|
+
pushState === 'subscribed' ? 'Disable push notifications' : 'Enable push notifications'
|
|
299
|
+
}
|
|
300
|
+
>
|
|
301
|
+
{pushState === 'denied' ? (
|
|
302
|
+
<BellOff className="h-4 w-4" />
|
|
303
|
+
) : pushState === 'subscribed' ? (
|
|
304
|
+
<BellRing className="h-4 w-4 text-blue-400" />
|
|
305
|
+
) : (
|
|
306
|
+
<Bell className="h-4 w-4" />
|
|
307
|
+
)}
|
|
308
|
+
</button>
|
|
309
|
+
)}
|
|
310
|
+
<div className="flex-1" />
|
|
311
|
+
<div className="relative" ref={menuRef}>
|
|
312
|
+
<button
|
|
313
|
+
onClick={() => setMenuOpen((v) => !v)}
|
|
314
|
+
className="flex items-center justify-center h-7 w-7 rounded-full text-muted-foreground hover:text-foreground hover:bg-white/[0.06] transition-colors"
|
|
315
|
+
>
|
|
316
|
+
<MoreVertical className="h-4 w-4" />
|
|
317
|
+
</button>
|
|
318
|
+
{menuOpen && (
|
|
319
|
+
<div className="absolute right-0 top-full mt-1 min-w-[160px] rounded-md border border-border bg-popover py-1 shadow-lg z-50">
|
|
320
|
+
<button
|
|
321
|
+
onClick={() => { setShowWizard(true); setMenuOpen(false); }}
|
|
322
|
+
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-white/[0.06] transition-colors"
|
|
323
|
+
>
|
|
324
|
+
<Wand2 className="h-4 w-4" />
|
|
325
|
+
Setup Wizard
|
|
326
|
+
</button>
|
|
327
|
+
<button
|
|
328
|
+
onClick={() => { clearContext(); setMenuOpen(false); }}
|
|
329
|
+
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-white/[0.06] transition-colors"
|
|
330
|
+
>
|
|
331
|
+
<Trash2 className="h-4 w-4" />
|
|
332
|
+
Clear context
|
|
333
|
+
</button>
|
|
334
|
+
{isMobile && !isStandalone && (
|
|
331
335
|
<button
|
|
332
|
-
|
|
333
|
-
|
|
336
|
+
onClick={() => {
|
|
337
|
+
setMenuOpen(false);
|
|
338
|
+
if (isInIframe) {
|
|
339
|
+
// Ask parent window to handle install
|
|
340
|
+
window.parent.postMessage({ type: 'fluxy:install-app' }, '*');
|
|
341
|
+
} else {
|
|
342
|
+
// Direct page — show iOS instructions (beforeinstallprompt won't help here)
|
|
343
|
+
setShowIosModal(true);
|
|
334
344
|
}
|
|
335
|
-
|
|
336
|
-
|
|
345
|
+
}}
|
|
346
|
+
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-white/[0.06] transition-colors"
|
|
337
347
|
>
|
|
338
|
-
|
|
348
|
+
<Download className="h-4 w-4" />
|
|
349
|
+
Install App
|
|
339
350
|
</button>
|
|
340
|
-
|
|
341
|
-
<span className="text-sm font-semibold">{botName}</span>
|
|
342
|
-
<div
|
|
343
|
-
className={`h-2 w-2 rounded-full ${connected ? 'bg-green-500' : 'bg-red-500'}`}
|
|
344
|
-
/>
|
|
345
|
-
{pushState !== 'loading' && pushState !== 'unsupported' && (
|
|
346
|
-
<button
|
|
347
|
-
onClick={() => {
|
|
348
|
-
if (pushState === 'subscribed') unsubscribePush();
|
|
349
|
-
else if (pushState === 'unsubscribed')
|
|
350
|
-
subscribePush();
|
|
351
|
-
}}
|
|
352
|
-
disabled={pushState === 'denied'}
|
|
353
|
-
className="flex items-center justify-center h-7 w-7 rounded-full text-muted-foreground hover:text-foreground hover:bg-white/[0.06] transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
354
|
-
title={
|
|
355
|
-
pushState === 'denied'
|
|
356
|
-
? 'Notifications blocked — enable in browser settings'
|
|
357
|
-
: pushState === 'subscribed'
|
|
358
|
-
? 'Disable push notifications'
|
|
359
|
-
: 'Enable push notifications'
|
|
360
|
-
}
|
|
361
|
-
>
|
|
362
|
-
{pushState === 'denied' ? (
|
|
363
|
-
<BellOff className="h-4 w-4" />
|
|
364
|
-
) : pushState === 'subscribed' ? (
|
|
365
|
-
<BellRing className="h-4 w-4 text-blue-400" />
|
|
366
|
-
) : (
|
|
367
|
-
<Bell className="h-4 w-4" />
|
|
368
|
-
)}
|
|
369
|
-
</button>
|
|
370
|
-
)}
|
|
371
|
-
<div className="flex-1" />
|
|
372
|
-
<div className="relative" ref={menuRef}>
|
|
373
|
-
<button
|
|
374
|
-
onClick={() => setMenuOpen(v => !v)}
|
|
375
|
-
className="flex items-center justify-center h-7 w-7 rounded-full text-muted-foreground hover:text-foreground hover:bg-white/[0.06] transition-colors"
|
|
376
|
-
>
|
|
377
|
-
<MoreVertical className="h-4 w-4" />
|
|
378
|
-
</button>
|
|
379
|
-
{menuOpen && (
|
|
380
|
-
<div className="absolute right-0 top-full mt-1 min-w-[160px] rounded-md border border-border bg-popover py-1 shadow-lg z-50">
|
|
381
|
-
<button
|
|
382
|
-
onClick={() => {
|
|
383
|
-
setShowWizard(true);
|
|
384
|
-
setMenuOpen(false);
|
|
385
|
-
}}
|
|
386
|
-
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-white/[0.06] transition-colors"
|
|
387
|
-
>
|
|
388
|
-
<Wand2 className="h-4 w-4" />
|
|
389
|
-
Setup Wizard
|
|
390
|
-
</button>
|
|
391
|
-
<button
|
|
392
|
-
onClick={() => {
|
|
393
|
-
clearContext();
|
|
394
|
-
setMenuOpen(false);
|
|
395
|
-
}}
|
|
396
|
-
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-white/[0.06] transition-colors"
|
|
397
|
-
>
|
|
398
|
-
<Trash2 className="h-4 w-4" />
|
|
399
|
-
Clear context
|
|
400
|
-
</button>
|
|
401
|
-
{isMobile && !isStandalone && (
|
|
402
|
-
<button
|
|
403
|
-
onClick={() => {
|
|
404
|
-
setMenuOpen(false);
|
|
405
|
-
if (isInIframe) {
|
|
406
|
-
// Ask parent window to handle install
|
|
407
|
-
window.parent.postMessage(
|
|
408
|
-
{ type: 'fluxy:install-app' },
|
|
409
|
-
'*'
|
|
410
|
-
);
|
|
411
|
-
} else {
|
|
412
|
-
// Direct page — show iOS instructions (beforeinstallprompt won't help here)
|
|
413
|
-
setShowIosModal(true);
|
|
414
|
-
}
|
|
415
|
-
}}
|
|
416
|
-
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-white/[0.06] transition-colors"
|
|
417
|
-
>
|
|
418
|
-
<Download className="h-4 w-4" />
|
|
419
|
-
Install App
|
|
420
|
-
</button>
|
|
421
|
-
)}
|
|
422
|
-
</div>
|
|
423
|
-
)}
|
|
424
|
-
</div>
|
|
351
|
+
)}
|
|
425
352
|
</div>
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
353
|
+
)}
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
{/* Chat body */}
|
|
358
|
+
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
|
359
|
+
<MessageList messages={messages} streaming={streaming} streamBuffer={streamBuffer} tools={tools} hasMore={hasMore} onLoadOlder={loadOlder} />
|
|
360
|
+
<InputBar
|
|
361
|
+
onSend={sendMessage}
|
|
362
|
+
onStop={stopStreaming}
|
|
363
|
+
streaming={streaming}
|
|
364
|
+
whisperEnabled={whisperEnabled}
|
|
365
|
+
onTranscribe={(audio) => {
|
|
366
|
+
return new Promise((resolve, reject) => {
|
|
367
|
+
const client = clientRef.current;
|
|
368
|
+
if (!client?.connected) { reject(new Error('WebSocket not connected')); return; }
|
|
369
|
+
const unsub = client.on('whisper:result', (data) => {
|
|
370
|
+
unsub();
|
|
371
|
+
clearTimeout(timer);
|
|
372
|
+
resolve(data);
|
|
373
|
+
});
|
|
374
|
+
const timer = setTimeout(() => { unsub(); reject(new Error('Transcription timeout')); }, 30000);
|
|
375
|
+
client.send('whisper:transcribe', { audio });
|
|
376
|
+
});
|
|
377
|
+
}}
|
|
378
|
+
/>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
{/* Setup Wizard overlay */}
|
|
382
|
+
{showWizard && (
|
|
383
|
+
<OnboardWizard
|
|
384
|
+
onSave={(payload) => {
|
|
385
|
+
return new Promise((resolve, reject) => {
|
|
386
|
+
const client = clientRef.current;
|
|
387
|
+
if (!client?.connected) {
|
|
388
|
+
reject(new Error('WebSocket not connected'));
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const unsub = client.on('settings:saved', (data) => {
|
|
392
|
+
unsub();
|
|
393
|
+
clearTimeout(timer);
|
|
394
|
+
resolve(data);
|
|
395
|
+
});
|
|
396
|
+
const unsubErr = client.on('settings:save-error', (data) => {
|
|
397
|
+
unsubErr();
|
|
398
|
+
clearTimeout(timer);
|
|
399
|
+
reject(new Error(data.error || 'Save failed'));
|
|
400
|
+
});
|
|
401
|
+
const timer = setTimeout(() => { unsub(); unsubErr(); reject(new Error('Save timeout')); }, 10000);
|
|
402
|
+
client.send('settings:save', payload);
|
|
403
|
+
});
|
|
404
|
+
}}
|
|
405
|
+
onTunnelSwitch={(newMode) => {
|
|
406
|
+
return new Promise((resolve, reject) => {
|
|
407
|
+
const client = clientRef.current;
|
|
408
|
+
if (!client?.connected) { reject(new Error('Not connected')); return; }
|
|
409
|
+
const unsub = client.on('tunnel:switched', (data) => { unsub(); unsubErr(); clearTimeout(t); resolve(data); });
|
|
410
|
+
const unsubErr = client.on('tunnel:switch-error', (data) => { unsub(); unsubErr(); clearTimeout(t); reject(new Error(data.error)); });
|
|
411
|
+
const t = setTimeout(() => { unsub(); unsubErr(); reject(new Error('Timeout')); }, 30000);
|
|
412
|
+
client.send('tunnel:switch', { mode: newMode });
|
|
413
|
+
});
|
|
414
|
+
}}
|
|
415
|
+
onComplete={() => {
|
|
416
|
+
setShowWizard(false);
|
|
417
|
+
// Reload settings (bot name, whisper, etc.)
|
|
418
|
+
authFetch('/api/settings')
|
|
419
|
+
.then((r) => r.json())
|
|
420
|
+
.then((s) => {
|
|
421
|
+
if (s.agent_name) setBotName(s.agent_name);
|
|
422
|
+
setWhisperEnabled(s.whisper_enabled === 'true');
|
|
423
|
+
})
|
|
424
|
+
.catch(() => {});
|
|
425
|
+
// Notify dashboard so it can refresh
|
|
426
|
+
window.parent?.postMessage({ type: 'fluxy:onboard-complete' }, '*');
|
|
427
|
+
}}
|
|
428
|
+
/>
|
|
429
|
+
)}
|
|
430
|
+
|
|
431
|
+
{/* iOS Install Instructions Modal */}
|
|
432
|
+
{showIosModal && (
|
|
433
|
+
<div className="fixed inset-0 z-[300] flex items-end sm:items-center justify-center p-4">
|
|
434
|
+
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setShowIosModal(false)} />
|
|
435
|
+
<div className="relative w-full max-w-[360px] bg-[#1c1c1e] border border-white/[0.08] rounded-2xl shadow-2xl overflow-hidden">
|
|
436
|
+
<div className="flex items-center justify-between px-5 pt-5 pb-2">
|
|
437
|
+
<h2 className="text-[17px] font-semibold text-white">Install App</h2>
|
|
438
|
+
<button
|
|
439
|
+
onClick={() => setShowIosModal(false)}
|
|
440
|
+
className="h-7 w-7 rounded-full bg-white/[0.08] flex items-center justify-center text-white/50 hover:text-white/80 transition-colors"
|
|
441
|
+
>
|
|
442
|
+
<X className="h-4 w-4" />
|
|
443
|
+
</button>
|
|
462
444
|
</div>
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const timer = setTimeout(() => {
|
|
490
|
-
unsub();
|
|
491
|
-
unsubErr();
|
|
492
|
-
reject(new Error('Save timeout'));
|
|
493
|
-
}, 10000);
|
|
494
|
-
client.send('settings:save', payload);
|
|
495
|
-
});
|
|
496
|
-
}}
|
|
497
|
-
onTunnelSwitch={newMode => {
|
|
498
|
-
return new Promise((resolve, reject) => {
|
|
499
|
-
const client = clientRef.current;
|
|
500
|
-
if (!client?.connected) {
|
|
501
|
-
reject(new Error('Not connected'));
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
const unsub = client.on('tunnel:switched', data => {
|
|
505
|
-
unsub();
|
|
506
|
-
unsubErr();
|
|
507
|
-
clearTimeout(t);
|
|
508
|
-
resolve(data);
|
|
509
|
-
});
|
|
510
|
-
const unsubErr = client.on(
|
|
511
|
-
'tunnel:switch-error',
|
|
512
|
-
data => {
|
|
513
|
-
unsub();
|
|
514
|
-
unsubErr();
|
|
515
|
-
clearTimeout(t);
|
|
516
|
-
reject(new Error(data.error));
|
|
517
|
-
}
|
|
518
|
-
);
|
|
519
|
-
const t = setTimeout(() => {
|
|
520
|
-
unsub();
|
|
521
|
-
unsubErr();
|
|
522
|
-
reject(new Error('Timeout'));
|
|
523
|
-
}, 30000);
|
|
524
|
-
client.send('tunnel:switch', { mode: newMode });
|
|
525
|
-
});
|
|
526
|
-
}}
|
|
527
|
-
onComplete={() => {
|
|
528
|
-
setShowWizard(false);
|
|
529
|
-
// Reload settings (bot name, whisper, etc.)
|
|
530
|
-
authFetch('/api/settings')
|
|
531
|
-
.then(r => r.json())
|
|
532
|
-
.then(s => {
|
|
533
|
-
if (s.agent_name) setBotName(s.agent_name);
|
|
534
|
-
setWhisperEnabled(s.whisper_enabled === 'true');
|
|
535
|
-
})
|
|
536
|
-
.catch(() => {});
|
|
537
|
-
// Notify dashboard so it can refresh
|
|
538
|
-
window.parent?.postMessage(
|
|
539
|
-
{ type: 'fluxy:onboard-complete' },
|
|
540
|
-
'*'
|
|
541
|
-
);
|
|
542
|
-
}}
|
|
543
|
-
/>
|
|
544
|
-
)}
|
|
545
|
-
|
|
546
|
-
{/* iOS Install Instructions Modal */}
|
|
547
|
-
{showIosModal && (
|
|
548
|
-
<div className="fixed inset-0 z-[300] flex items-end sm:items-center justify-center p-4">
|
|
549
|
-
<div
|
|
550
|
-
role="presentation"
|
|
551
|
-
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
|
552
|
-
onClick={() => setShowIosModal(false)}
|
|
553
|
-
/>
|
|
554
|
-
<div className="relative w-full max-w-[360px] bg-[#1c1c1e] border border-white/[0.08] rounded-2xl shadow-2xl overflow-hidden">
|
|
555
|
-
<div className="flex items-center justify-between px-5 pt-5 pb-2">
|
|
556
|
-
<h2 className="text-[17px] font-semibold text-white">
|
|
557
|
-
Install App
|
|
558
|
-
</h2>
|
|
559
|
-
<button
|
|
560
|
-
onClick={() => setShowIosModal(false)}
|
|
561
|
-
className="h-7 w-7 rounded-full bg-white/[0.08] flex items-center justify-center text-white/50 hover:text-white/80 transition-colors"
|
|
562
|
-
>
|
|
563
|
-
<X className="h-4 w-4" />
|
|
564
|
-
</button>
|
|
565
|
-
</div>
|
|
566
|
-
<div className="px-5 pb-6 pt-1">
|
|
567
|
-
<p className="text-white/50 text-[13px] mb-5 leading-relaxed">
|
|
568
|
-
Add Fluxy to your home screen for a full-screen
|
|
569
|
-
app experience.
|
|
570
|
-
</p>
|
|
571
|
-
{isIos ? (
|
|
572
|
-
<div className="space-y-3.5">
|
|
573
|
-
<div className="flex items-start gap-3">
|
|
574
|
-
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-[#007AFF]/15 text-[#007AFF] text-[13px] font-semibold flex items-center justify-center mt-0.5">
|
|
575
|
-
1
|
|
576
|
-
</span>
|
|
577
|
-
<div className="flex-1">
|
|
578
|
-
<p className="text-white text-[14px] font-medium">
|
|
579
|
-
Tap the Share button
|
|
580
|
-
</p>
|
|
581
|
-
<p className="text-white/40 text-[12px] mt-0.5 flex items-center gap-1.5">
|
|
582
|
-
The{' '}
|
|
583
|
-
<Share className="h-3.5 w-3.5 inline" />{' '}
|
|
584
|
-
icon at the bottom of Safari
|
|
585
|
-
</p>
|
|
586
|
-
</div>
|
|
587
|
-
</div>
|
|
588
|
-
<div className="flex items-start gap-3">
|
|
589
|
-
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-[#007AFF]/15 text-[#007AFF] text-[13px] font-semibold flex items-center justify-center mt-0.5">
|
|
590
|
-
2
|
|
591
|
-
</span>
|
|
592
|
-
<div className="flex-1">
|
|
593
|
-
<p className="text-white text-[14px] font-medium">
|
|
594
|
-
Scroll down and tap
|
|
595
|
-
</p>
|
|
596
|
-
<p className="text-white/40 text-[12px] mt-0.5">
|
|
597
|
-
"Add to Home Screen"
|
|
598
|
-
</p>
|
|
599
|
-
</div>
|
|
600
|
-
</div>
|
|
601
|
-
<div className="flex items-start gap-3">
|
|
602
|
-
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-[#007AFF]/15 text-[#007AFF] text-[13px] font-semibold flex items-center justify-center mt-0.5">
|
|
603
|
-
3
|
|
604
|
-
</span>
|
|
605
|
-
<div className="flex-1">
|
|
606
|
-
<p className="text-white text-[14px] font-medium">
|
|
607
|
-
Tap "Add"
|
|
608
|
-
</p>
|
|
609
|
-
<p className="text-white/40 text-[12px] mt-0.5">
|
|
610
|
-
Fluxy will appear on your home
|
|
611
|
-
screen
|
|
612
|
-
</p>
|
|
613
|
-
</div>
|
|
614
|
-
</div>
|
|
615
|
-
</div>
|
|
616
|
-
) : (
|
|
617
|
-
<div className="space-y-3.5">
|
|
618
|
-
<div className="flex items-start gap-3">
|
|
619
|
-
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-[#007AFF]/15 text-[#007AFF] text-[13px] font-semibold flex items-center justify-center mt-0.5">
|
|
620
|
-
1
|
|
621
|
-
</span>
|
|
622
|
-
<div className="flex-1">
|
|
623
|
-
<p className="text-white text-[14px] font-medium">
|
|
624
|
-
Open browser menu
|
|
625
|
-
</p>
|
|
626
|
-
<p className="text-white/40 text-[12px] mt-0.5">
|
|
627
|
-
Tap the three-dot menu in your
|
|
628
|
-
browser
|
|
629
|
-
</p>
|
|
630
|
-
</div>
|
|
631
|
-
</div>
|
|
632
|
-
<div className="flex items-start gap-3">
|
|
633
|
-
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-[#007AFF]/15 text-[#007AFF] text-[13px] font-semibold flex items-center justify-center mt-0.5">
|
|
634
|
-
2
|
|
635
|
-
</span>
|
|
636
|
-
<div className="flex-1">
|
|
637
|
-
<p className="text-white text-[14px] font-medium">
|
|
638
|
-
Tap "Install app" or "Add to
|
|
639
|
-
Home screen"
|
|
640
|
-
</p>
|
|
641
|
-
<p className="text-white/40 text-[12px] mt-0.5">
|
|
642
|
-
Fluxy will be installed as a
|
|
643
|
-
standalone app
|
|
644
|
-
</p>
|
|
645
|
-
</div>
|
|
646
|
-
</div>
|
|
647
|
-
</div>
|
|
648
|
-
)}
|
|
649
|
-
<button
|
|
650
|
-
onClick={() => setShowIosModal(false)}
|
|
651
|
-
className="w-full mt-5 py-2.5 bg-white/[0.06] hover:bg-white/[0.1] text-white text-[14px] font-medium rounded-xl transition-colors"
|
|
652
|
-
>
|
|
653
|
-
Got it
|
|
654
|
-
</button>
|
|
655
|
-
</div>
|
|
445
|
+
<div className="px-5 pb-6 pt-1">
|
|
446
|
+
<p className="text-white/50 text-[13px] mb-5 leading-relaxed">
|
|
447
|
+
Add Fluxy to your home screen for a full-screen app experience.
|
|
448
|
+
</p>
|
|
449
|
+
{isIos ? (
|
|
450
|
+
<div className="space-y-3.5">
|
|
451
|
+
<div className="flex items-start gap-3">
|
|
452
|
+
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-[#007AFF]/15 text-[#007AFF] text-[13px] font-semibold flex items-center justify-center mt-0.5">1</span>
|
|
453
|
+
<div className="flex-1">
|
|
454
|
+
<p className="text-white text-[14px] font-medium">
|
|
455
|
+
Tap the Share button
|
|
456
|
+
</p>
|
|
457
|
+
<p className="text-white/40 text-[12px] mt-0.5 flex items-center gap-1.5">
|
|
458
|
+
The <Share className="h-3.5 w-3.5 inline" /> icon at the bottom of Safari
|
|
459
|
+
</p>
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
<div className="flex items-start gap-3">
|
|
463
|
+
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-[#007AFF]/15 text-[#007AFF] text-[13px] font-semibold flex items-center justify-center mt-0.5">2</span>
|
|
464
|
+
<div className="flex-1">
|
|
465
|
+
<p className="text-white text-[14px] font-medium">
|
|
466
|
+
Scroll down and tap
|
|
467
|
+
</p>
|
|
468
|
+
<p className="text-white/40 text-[12px] mt-0.5">
|
|
469
|
+
"Add to Home Screen"
|
|
470
|
+
</p>
|
|
656
471
|
</div>
|
|
472
|
+
</div>
|
|
473
|
+
<div className="flex items-start gap-3">
|
|
474
|
+
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-[#007AFF]/15 text-[#007AFF] text-[13px] font-semibold flex items-center justify-center mt-0.5">3</span>
|
|
475
|
+
<div className="flex-1">
|
|
476
|
+
<p className="text-white text-[14px] font-medium">
|
|
477
|
+
Tap "Add"
|
|
478
|
+
</p>
|
|
479
|
+
<p className="text-white/40 text-[12px] mt-0.5">
|
|
480
|
+
Fluxy will appear on your home screen
|
|
481
|
+
</p>
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
657
484
|
</div>
|
|
658
|
-
|
|
485
|
+
) : (
|
|
486
|
+
<div className="space-y-3.5">
|
|
487
|
+
<div className="flex items-start gap-3">
|
|
488
|
+
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-[#007AFF]/15 text-[#007AFF] text-[13px] font-semibold flex items-center justify-center mt-0.5">1</span>
|
|
489
|
+
<div className="flex-1">
|
|
490
|
+
<p className="text-white text-[14px] font-medium">
|
|
491
|
+
Open browser menu
|
|
492
|
+
</p>
|
|
493
|
+
<p className="text-white/40 text-[12px] mt-0.5">
|
|
494
|
+
Tap the three-dot menu in your browser
|
|
495
|
+
</p>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
<div className="flex items-start gap-3">
|
|
499
|
+
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-[#007AFF]/15 text-[#007AFF] text-[13px] font-semibold flex items-center justify-center mt-0.5">2</span>
|
|
500
|
+
<div className="flex-1">
|
|
501
|
+
<p className="text-white text-[14px] font-medium">
|
|
502
|
+
Tap "Install app" or "Add to Home screen"
|
|
503
|
+
</p>
|
|
504
|
+
<p className="text-white/40 text-[12px] mt-0.5">
|
|
505
|
+
Fluxy will be installed as a standalone app
|
|
506
|
+
</p>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
510
|
+
)}
|
|
511
|
+
<button
|
|
512
|
+
onClick={() => setShowIosModal(false)}
|
|
513
|
+
className="w-full mt-5 py-2.5 bg-white/[0.06] hover:bg-white/[0.1] text-white text-[14px] font-medium rounded-xl transition-colors"
|
|
514
|
+
>
|
|
515
|
+
Got it
|
|
516
|
+
</button>
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
659
519
|
</div>
|
|
660
|
-
|
|
520
|
+
)}
|
|
521
|
+
</div>
|
|
522
|
+
);
|
|
661
523
|
}
|
|
662
524
|
|
|
663
525
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
526
|
+
<React.StrictMode>
|
|
527
|
+
<FluxyApp />
|
|
528
|
+
</React.StrictMode>,
|
|
667
529
|
);
|