fluxy-bot 0.8.2 → 0.8.4
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-fluxy/assets/{fluxy-C7pmHG9i.js → fluxy-V26b7AxV.js} +26 -26
- package/dist-fluxy/assets/globals-BrtAPuMR.css +1 -0
- package/dist-fluxy/assets/{globals-CP6y1O_V.js → globals-DYbVw4jJ.js} +5 -5
- package/dist-fluxy/assets/{onboard-CrwD-dFw.js → onboard-Dwc03fqY.js} +1 -1
- package/dist-fluxy/fluxy.html +3 -3
- package/dist-fluxy/onboard.html +3 -3
- package/package.json +4 -2
- package/supervisor/chat/OnboardWizard.tsx +28 -14
- package/supervisor/chat/src/components/Chat/InputBar.tsx +59 -25
- package/supervisor/chat/src/hooks/useSpeechRecognition.ts +132 -0
- package/supervisor/index.ts +6 -22
- package/worker/prompts/fluxy-system-prompt.txt +3 -3
- package/workspace/client/public/fluxy.svg +56 -0
- package/dist-fluxy/assets/globals-wnYSfEuA.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{e as o,j as e,R as n,O as r}from"./globals-
|
|
1
|
+
import{e as o,j as e,R as n,O as r}from"./globals-DYbVw4jJ.js";function a(){const t=()=>{window.parent?.postMessage({type:"fluxy:onboard-complete"},"*")};return e.jsx(r,{onComplete:t,isInitialSetup:!0})}o.createRoot(document.getElementById("root")).render(e.jsx(n.StrictMode,{children:e.jsx(a,{})}));
|
package/dist-fluxy/fluxy.html
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content" />
|
|
6
6
|
<title>Fluxy Chat</title>
|
|
7
|
-
<script type="module" crossorigin src="/fluxy/assets/fluxy-
|
|
8
|
-
<link rel="modulepreload" crossorigin href="/fluxy/assets/globals-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/fluxy/assets/globals-
|
|
7
|
+
<script type="module" crossorigin src="/fluxy/assets/fluxy-V26b7AxV.js"></script>
|
|
8
|
+
<link rel="modulepreload" crossorigin href="/fluxy/assets/globals-DYbVw4jJ.js">
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/fluxy/assets/globals-BrtAPuMR.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body class="bg-background text-foreground">
|
|
12
12
|
<div id="root"></div>
|
package/dist-fluxy/onboard.html
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content" />
|
|
6
6
|
<title>Fluxy Setup</title>
|
|
7
|
-
<script type="module" crossorigin src="/fluxy/assets/onboard-
|
|
8
|
-
<link rel="modulepreload" crossorigin href="/fluxy/assets/globals-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/fluxy/assets/globals-
|
|
7
|
+
<script type="module" crossorigin src="/fluxy/assets/onboard-Dwc03fqY.js"></script>
|
|
8
|
+
<link rel="modulepreload" crossorigin href="/fluxy/assets/globals-DYbVw4jJ.js">
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/fluxy/assets/globals-BrtAPuMR.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body class="bg-background text-foreground">
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fluxy-bot",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"releaseNotes": [
|
|
5
5
|
"Fixed some bugs to iOs ",
|
|
6
6
|
"2. ",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"build": "vite build && vite build --config vite.fluxy.config.ts",
|
|
46
46
|
"build:fluxy": "vite build --config vite.fluxy.config.ts",
|
|
47
47
|
"start": "node --import tsx/esm supervisor/index.ts",
|
|
48
|
-
"postinstall": "node scripts/postinstall.js"
|
|
48
|
+
"postinstall": "node scripts/postinstall.js",
|
|
49
|
+
"dev:docs": "cd ./docs && npx fumapress"
|
|
49
50
|
},
|
|
50
51
|
"dependencies": {
|
|
51
52
|
"@anthropic-ai/claude-agent-sdk": "^0.2.50",
|
|
@@ -93,6 +94,7 @@
|
|
|
93
94
|
"@types/react-syntax-highlighter": "^15.5.13",
|
|
94
95
|
"@types/ws": "^8.18.1",
|
|
95
96
|
"concurrently": "^9.2.1",
|
|
97
|
+
"fumapress": "^0.1.1",
|
|
96
98
|
"typescript": "^5.9.3"
|
|
97
99
|
}
|
|
98
100
|
}
|
|
@@ -2096,24 +2096,38 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
2096
2096
|
</div>
|
|
2097
2097
|
)}
|
|
2098
2098
|
|
|
2099
|
-
{/* ── Step 5:
|
|
2099
|
+
{/* ── Step 5: Voice Messages + Complete ── */}
|
|
2100
2100
|
{step === 5 && (
|
|
2101
2101
|
<div>
|
|
2102
|
-
<
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
</h1>
|
|
2106
|
-
<span className="text-[11px] text-white/25 font-medium bg-white/[0.04] border border-white/[0.06] rounded-full px-2.5 py-0.5">
|
|
2107
|
-
Optional
|
|
2108
|
-
</span>
|
|
2109
|
-
</div>
|
|
2102
|
+
<h1 className="text-xl font-bold text-white tracking-tight mb-1">
|
|
2103
|
+
Voice Messages
|
|
2104
|
+
</h1>
|
|
2110
2105
|
<p className="text-white/40 text-[13px] mt-1 leading-relaxed">
|
|
2111
|
-
|
|
2106
|
+
Voice input works out of the box using your browser's built-in speech recognition. For better accuracy, you can optionally enable OpenAI Whisper.
|
|
2112
2107
|
</p>
|
|
2113
2108
|
|
|
2109
|
+
{/* Browser Speech Recognition card */}
|
|
2110
|
+
<div className="w-full mt-5 rounded-xl border border-emerald-500/20 bg-emerald-500/[0.04] p-4">
|
|
2111
|
+
<div className="flex items-center gap-3.5">
|
|
2112
|
+
<div className="w-10 h-10 rounded-xl bg-emerald-500/10 flex items-center justify-center shrink-0">
|
|
2113
|
+
<Mic className="h-5 w-5 text-emerald-400" />
|
|
2114
|
+
</div>
|
|
2115
|
+
<div className="flex-1">
|
|
2116
|
+
<div className="text-[14px] font-medium text-white">Browser Speech Recognition</div>
|
|
2117
|
+
<div className="text-[12px] text-white/35 mt-0.5 leading-relaxed">
|
|
2118
|
+
Built-in voice input — no setup needed. Works in Chrome, Edge, and Safari.
|
|
2119
|
+
</div>
|
|
2120
|
+
</div>
|
|
2121
|
+
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-emerald-500/20 shrink-0">
|
|
2122
|
+
<Check className="h-3.5 w-3.5 text-emerald-400" />
|
|
2123
|
+
</div>
|
|
2124
|
+
</div>
|
|
2125
|
+
</div>
|
|
2126
|
+
|
|
2127
|
+
{/* Whisper upgrade toggle */}
|
|
2114
2128
|
<button
|
|
2115
2129
|
onClick={() => setWhisperEnabled((v) => !v)}
|
|
2116
|
-
className={`w-full mt-
|
|
2130
|
+
className={`w-full mt-3 rounded-xl border transition-all duration-200 p-4 text-left ${
|
|
2117
2131
|
whisperEnabled
|
|
2118
2132
|
? 'bg-white/[0.04] border-[#AF27E3]/40'
|
|
2119
2133
|
: 'bg-transparent border-white/[0.06] hover:border-white/10 hover:bg-white/[0.02]'
|
|
@@ -2124,7 +2138,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
2124
2138
|
<div className="flex-1">
|
|
2125
2139
|
<div className="text-[14px] font-medium text-white">OpenAI Whisper</div>
|
|
2126
2140
|
<div className="text-[12px] text-white/35 mt-0.5 leading-relaxed">
|
|
2127
|
-
|
|
2141
|
+
Upgrade to more accurate transcription that works in all browsers including Firefox.
|
|
2128
2142
|
</div>
|
|
2129
2143
|
</div>
|
|
2130
2144
|
<div className={`w-10 h-[22px] rounded-full transition-colors duration-200 flex items-center px-0.5 shrink-0 ${
|
|
@@ -2157,7 +2171,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
2157
2171
|
<div className="flex items-start gap-2.5 mt-3 bg-white/[0.02] border border-white/[0.06] rounded-xl px-4 py-3">
|
|
2158
2172
|
<Mic className="h-4 w-4 text-[#AF27E3]/60 mt-0.5 shrink-0" />
|
|
2159
2173
|
<p className="text-white/35 text-[12px] leading-relaxed">
|
|
2160
|
-
|
|
2174
|
+
Whisper provides more accurate transcription and works in all browsers including Firefox.
|
|
2161
2175
|
</p>
|
|
2162
2176
|
</div>
|
|
2163
2177
|
</div>
|
|
@@ -2177,7 +2191,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
2177
2191
|
|
|
2178
2192
|
{!whisperEnabled && (
|
|
2179
2193
|
<p className="text-center text-white/20 text-[11px] mt-2.5">
|
|
2180
|
-
|
|
2194
|
+
Voice input is active using your browser's built-in speech recognition.
|
|
2181
2195
|
</p>
|
|
2182
2196
|
)}
|
|
2183
2197
|
</div>
|
|
@@ -2,6 +2,7 @@ import { useState, useRef, useCallback, useEffect, type PointerEvent as RPointer
|
|
|
2
2
|
import { SendHorizontal, Mic, Square, Trash2, Paperclip, Camera, X } from 'lucide-react';
|
|
3
3
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
4
4
|
import type { Attachment } from '../../hooks/useChat';
|
|
5
|
+
import { useSpeechRecognition } from '../../hooks/useSpeechRecognition';
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
7
8
|
onSend: (msg: string, attachments?: Attachment[], audioData?: string) => void;
|
|
@@ -55,6 +56,8 @@ function compressImage(dataUrl: string, maxBytes = 4 * 1024 * 1024): Promise<str
|
|
|
55
56
|
const DRAFT_KEY = 'fluxy_draft';
|
|
56
57
|
|
|
57
58
|
export default function InputBar({ onSend, onStop, streaming, whisperEnabled, onTranscribe }: Props) {
|
|
59
|
+
const { start: startSpeech, stop: stopSpeech, abort: abortSpeech, isSupported: webSpeechSupported } = useSpeechRecognition();
|
|
60
|
+
const voiceEnabled = whisperEnabled || webSpeechSupported;
|
|
58
61
|
const [text, setText] = useState(() => {
|
|
59
62
|
try { return localStorage.getItem(DRAFT_KEY) || ''; } catch { return ''; }
|
|
60
63
|
});
|
|
@@ -112,12 +115,20 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled, on
|
|
|
112
115
|
const recorder = mediaRecorderRef.current;
|
|
113
116
|
const stream = streamRef.current;
|
|
114
117
|
|
|
115
|
-
if (cancelled
|
|
116
|
-
// Clean up mic
|
|
118
|
+
if (cancelled) {
|
|
119
|
+
// Clean up mic + abort speech recognition
|
|
117
120
|
stream?.getTracks().forEach((t) => t.stop());
|
|
118
121
|
mediaRecorderRef.current = null;
|
|
119
122
|
streamRef.current = null;
|
|
120
123
|
audioChunksRef.current = [];
|
|
124
|
+
abortSpeech();
|
|
125
|
+
} else if (!recorder || recorder.state === 'inactive') {
|
|
126
|
+
// No recorder active, clean up
|
|
127
|
+
stream?.getTracks().forEach((t) => t.stop());
|
|
128
|
+
mediaRecorderRef.current = null;
|
|
129
|
+
streamRef.current = null;
|
|
130
|
+
audioChunksRef.current = [];
|
|
131
|
+
abortSpeech();
|
|
121
132
|
} else {
|
|
122
133
|
// Stop recorder — ondataavailable + onstop will fire
|
|
123
134
|
recorder.onstop = async () => {
|
|
@@ -127,7 +138,10 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled, on
|
|
|
127
138
|
mediaRecorderRef.current = null;
|
|
128
139
|
streamRef.current = null;
|
|
129
140
|
|
|
130
|
-
if (blob.size < 1000)
|
|
141
|
+
if (blob.size < 1000) {
|
|
142
|
+
abortSpeech();
|
|
143
|
+
return; // too small, skip
|
|
144
|
+
}
|
|
131
145
|
|
|
132
146
|
// Convert to base64
|
|
133
147
|
const reader = new FileReader();
|
|
@@ -137,23 +151,36 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled, on
|
|
|
137
151
|
if (!base64) return;
|
|
138
152
|
|
|
139
153
|
try {
|
|
140
|
-
|
|
141
|
-
if (onTranscribe) {
|
|
142
|
-
data
|
|
154
|
+
// Whisper path: send audio to Whisper API for transcription
|
|
155
|
+
if (whisperEnabled && (onTranscribe || true)) {
|
|
156
|
+
let data: { transcript?: string };
|
|
157
|
+
if (onTranscribe) {
|
|
158
|
+
data = await onTranscribe(base64);
|
|
159
|
+
} else {
|
|
160
|
+
const res = await fetch('/api/whisper/transcribe', {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
163
|
+
body: JSON.stringify({ audio: base64 }),
|
|
164
|
+
});
|
|
165
|
+
data = await res.json();
|
|
166
|
+
}
|
|
167
|
+
abortSpeech(); // discard Web Speech results when using Whisper
|
|
168
|
+
if (data.transcript?.trim()) {
|
|
169
|
+
const pendingAtts = attachments.length > 0 ? attachments : undefined;
|
|
170
|
+
onSend(data.transcript.trim(), pendingAtts, dataUrl);
|
|
171
|
+
if (pendingAtts) setAttachments([]);
|
|
172
|
+
}
|
|
143
173
|
} else {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (data.transcript?.trim()) {
|
|
152
|
-
const pendingAtts = attachments.length > 0 ? attachments : undefined;
|
|
153
|
-
onSend(data.transcript.trim(), pendingAtts, dataUrl);
|
|
154
|
-
if (pendingAtts) setAttachments([]);
|
|
174
|
+
// Web Speech API path: use accumulated transcript
|
|
175
|
+
const transcript = await stopSpeech();
|
|
176
|
+
if (transcript.trim()) {
|
|
177
|
+
const pendingAtts = attachments.length > 0 ? attachments : undefined;
|
|
178
|
+
onSend(transcript.trim(), pendingAtts, dataUrl);
|
|
179
|
+
if (pendingAtts) setAttachments([]);
|
|
180
|
+
}
|
|
155
181
|
}
|
|
156
182
|
} catch {
|
|
183
|
+
abortSpeech();
|
|
157
184
|
// Transcription failed silently
|
|
158
185
|
}
|
|
159
186
|
};
|
|
@@ -166,7 +193,7 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled, on
|
|
|
166
193
|
setIsRecording(false);
|
|
167
194
|
setRecordingTime(0);
|
|
168
195
|
dragRef.current = 0;
|
|
169
|
-
}, [onSend, onTranscribe, attachments]);
|
|
196
|
+
}, [onSend, onTranscribe, attachments, whisperEnabled, abortSpeech, stopSpeech]);
|
|
170
197
|
|
|
171
198
|
// ── File handling ──
|
|
172
199
|
|
|
@@ -241,7 +268,7 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled, on
|
|
|
241
268
|
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
242
269
|
|
|
243
270
|
holdTimerRef.current = setTimeout(async () => {
|
|
244
|
-
if (!
|
|
271
|
+
if (!voiceEnabled) return;
|
|
245
272
|
try {
|
|
246
273
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
247
274
|
streamRef.current = stream;
|
|
@@ -253,6 +280,12 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled, on
|
|
|
253
280
|
};
|
|
254
281
|
mediaRecorderRef.current = recorder;
|
|
255
282
|
recorder.start();
|
|
283
|
+
|
|
284
|
+
// Start Web Speech API alongside MediaRecorder (used as fallback or primary)
|
|
285
|
+
if (webSpeechSupported) {
|
|
286
|
+
startSpeech();
|
|
287
|
+
}
|
|
288
|
+
|
|
256
289
|
isHolding.current = true;
|
|
257
290
|
setIsRecording(true);
|
|
258
291
|
setRecordingTime(0);
|
|
@@ -260,7 +293,7 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled, on
|
|
|
260
293
|
// Mic permission denied or not available
|
|
261
294
|
}
|
|
262
295
|
}, 200);
|
|
263
|
-
}, [
|
|
296
|
+
}, [voiceEnabled, webSpeechSupported, startSpeech]);
|
|
264
297
|
|
|
265
298
|
const handleMicMove = useCallback((e: RPointerEvent) => {
|
|
266
299
|
if (!isHolding.current) return;
|
|
@@ -391,14 +424,15 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled, on
|
|
|
391
424
|
) : (
|
|
392
425
|
<div
|
|
393
426
|
className={`flex items-center justify-center h-12 w-12 shrink-0 rounded-full transition-colors touch-none select-none ${
|
|
394
|
-
|
|
427
|
+
voiceEnabled
|
|
395
428
|
? 'bg-gradient-brand text-white cursor-pointer'
|
|
396
429
|
: 'bg-gray-200 text-gray-400 cursor-default'
|
|
397
430
|
}`}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
431
|
+
title={!voiceEnabled ? 'Voice input requires Chrome, Edge, or Safari. Enable Whisper in Settings for any browser.' : undefined}
|
|
432
|
+
onPointerDown={voiceEnabled ? handleMicDown : undefined}
|
|
433
|
+
onPointerMove={voiceEnabled ? handleMicMove : undefined}
|
|
434
|
+
onPointerUp={voiceEnabled ? handleMicUp : undefined}
|
|
435
|
+
onPointerCancel={voiceEnabled ? handleMicCancel : undefined}
|
|
402
436
|
>
|
|
403
437
|
<Mic className="h-4 w-4" />
|
|
404
438
|
</div>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { useRef, useCallback, useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
// Extend Window for vendor-prefixed SpeechRecognition
|
|
4
|
+
interface SpeechRecognitionEvent extends Event {
|
|
5
|
+
results: SpeechRecognitionResultList;
|
|
6
|
+
resultIndex: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type SpeechRecognitionConstructor = new () => SpeechRecognitionInstance;
|
|
10
|
+
|
|
11
|
+
interface SpeechRecognitionInstance extends EventTarget {
|
|
12
|
+
continuous: boolean;
|
|
13
|
+
interimResults: boolean;
|
|
14
|
+
lang: string;
|
|
15
|
+
start(): void;
|
|
16
|
+
stop(): void;
|
|
17
|
+
abort(): void;
|
|
18
|
+
onresult: ((event: SpeechRecognitionEvent) => void) | null;
|
|
19
|
+
onend: (() => void) | null;
|
|
20
|
+
onerror: ((event: Event & { error: string }) => void) | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const getSpeechRecognitionCtor = (): SpeechRecognitionConstructor | null => {
|
|
24
|
+
if (typeof window === 'undefined') return null;
|
|
25
|
+
return (
|
|
26
|
+
(window as unknown as Record<string, SpeechRecognitionConstructor>).SpeechRecognition ??
|
|
27
|
+
(window as unknown as Record<string, SpeechRecognitionConstructor>).webkitSpeechRecognition ??
|
|
28
|
+
null
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const isWebSpeechSupported = getSpeechRecognitionCtor() !== null;
|
|
33
|
+
|
|
34
|
+
export function useSpeechRecognition() {
|
|
35
|
+
const instanceRef = useRef<SpeechRecognitionInstance | null>(null);
|
|
36
|
+
const transcriptRef = useRef('');
|
|
37
|
+
const activeRef = useRef(false);
|
|
38
|
+
const resolveRef = useRef<((text: string) => void) | null>(null);
|
|
39
|
+
|
|
40
|
+
const isSupported = useMemo(() => isWebSpeechSupported, []);
|
|
41
|
+
|
|
42
|
+
const start = useCallback(() => {
|
|
43
|
+
const Ctor = getSpeechRecognitionCtor();
|
|
44
|
+
if (!Ctor) return;
|
|
45
|
+
|
|
46
|
+
// Clean up any lingering instance
|
|
47
|
+
if (instanceRef.current) {
|
|
48
|
+
try { instanceRef.current.abort(); } catch {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const recognition = new Ctor();
|
|
52
|
+
recognition.continuous = true;
|
|
53
|
+
recognition.interimResults = true;
|
|
54
|
+
recognition.lang = navigator.language || 'en-US';
|
|
55
|
+
|
|
56
|
+
transcriptRef.current = '';
|
|
57
|
+
activeRef.current = true;
|
|
58
|
+
instanceRef.current = recognition;
|
|
59
|
+
|
|
60
|
+
recognition.onresult = (event: SpeechRecognitionEvent) => {
|
|
61
|
+
let finalTranscript = '';
|
|
62
|
+
for (let i = 0; i < event.results.length; i++) {
|
|
63
|
+
if (event.results[i].isFinal) {
|
|
64
|
+
finalTranscript += event.results[i][0].transcript;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (finalTranscript) {
|
|
68
|
+
transcriptRef.current = finalTranscript;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
recognition.onend = () => {
|
|
73
|
+
// Auto-restart if we're still supposed to be listening (handles silence gaps)
|
|
74
|
+
if (activeRef.current) {
|
|
75
|
+
try { recognition.start(); } catch {}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
recognition.onerror = () => {
|
|
80
|
+
// On error, stop gracefully
|
|
81
|
+
if (activeRef.current) {
|
|
82
|
+
activeRef.current = false;
|
|
83
|
+
resolveRef.current?.(transcriptRef.current);
|
|
84
|
+
resolveRef.current = null;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
try { recognition.start(); } catch {}
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
const stop = useCallback((): Promise<string> => {
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
activeRef.current = false;
|
|
94
|
+
const instance = instanceRef.current;
|
|
95
|
+
if (!instance) {
|
|
96
|
+
resolve(transcriptRef.current);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
resolveRef.current = resolve;
|
|
101
|
+
|
|
102
|
+
// Give a brief moment for any final results, then resolve
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
try { instance.stop(); } catch {}
|
|
105
|
+
instanceRef.current = null;
|
|
106
|
+
|
|
107
|
+
// Resolve with whatever we accumulated
|
|
108
|
+
const text = transcriptRef.current;
|
|
109
|
+
if (resolveRef.current) {
|
|
110
|
+
resolveRef.current(text);
|
|
111
|
+
resolveRef.current = null;
|
|
112
|
+
}
|
|
113
|
+
}, 300);
|
|
114
|
+
});
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
const abort = useCallback(() => {
|
|
118
|
+
activeRef.current = false;
|
|
119
|
+
transcriptRef.current = '';
|
|
120
|
+
const instance = instanceRef.current;
|
|
121
|
+
if (instance) {
|
|
122
|
+
try { instance.abort(); } catch {}
|
|
123
|
+
instanceRef.current = null;
|
|
124
|
+
}
|
|
125
|
+
if (resolveRef.current) {
|
|
126
|
+
resolveRef.current('');
|
|
127
|
+
resolveRef.current = null;
|
|
128
|
+
}
|
|
129
|
+
}, []);
|
|
130
|
+
|
|
131
|
+
return { start, stop, abort, isSupported };
|
|
132
|
+
}
|
package/supervisor/index.ts
CHANGED
|
@@ -17,7 +17,6 @@ import { ensureFileDirs, saveAttachment, type SavedFile } from './file-saver.js'
|
|
|
17
17
|
import { startViteDevServers, stopViteDevServers } from './vite-dev.js';
|
|
18
18
|
import { startScheduler, stopScheduler } from './scheduler.js';
|
|
19
19
|
import { execSync, spawn as cpSpawn } from 'child_process';
|
|
20
|
-
import os from 'os';
|
|
21
20
|
|
|
22
21
|
const DIST_FLUXY = path.join(PKG_DIR, 'dist-fluxy');
|
|
23
22
|
|
|
@@ -785,27 +784,12 @@ export async function startSupervisor() {
|
|
|
785
784
|
const cliPath = path.join(PKG_DIR, 'bin', 'cli.js');
|
|
786
785
|
log.info('Deferred update triggered — running fluxy update...');
|
|
787
786
|
try {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
'--uid=' + user,
|
|
795
|
-
'--setenv=HOME=' + os.homedir(),
|
|
796
|
-
'--setenv=PATH=' + process.env.PATH,
|
|
797
|
-
process.execPath, cliPath, 'update',
|
|
798
|
-
], { detached: true, stdio: 'ignore' });
|
|
799
|
-
child.unref();
|
|
800
|
-
} else {
|
|
801
|
-
// macOS / other: detached child process survives parent exit
|
|
802
|
-
const child = cpSpawn(process.execPath, [cliPath, 'update'], {
|
|
803
|
-
detached: true,
|
|
804
|
-
stdio: 'ignore',
|
|
805
|
-
env: { ...process.env },
|
|
806
|
-
});
|
|
807
|
-
child.unref();
|
|
808
|
-
}
|
|
787
|
+
const child = cpSpawn(process.execPath, [cliPath, 'update'], {
|
|
788
|
+
detached: true,
|
|
789
|
+
stdio: 'ignore',
|
|
790
|
+
env: { ...process.env },
|
|
791
|
+
});
|
|
792
|
+
child.unref();
|
|
809
793
|
} catch (err) {
|
|
810
794
|
log.error(`Deferred update failed: ${err instanceof Error ? err.message : err}`);
|
|
811
795
|
}
|
|
@@ -156,17 +156,17 @@ Notify your human only if importance is 7+ — otherwise log results silently.
|
|
|
156
156
|
|
|
157
157
|
**Check version:** `cat ~/.fluxy/VERSION` (current) vs `npm view fluxy-bot version` (latest).
|
|
158
158
|
|
|
159
|
-
**To update:** Create the trigger file `touch
|
|
159
|
+
**To update:** Create the trigger file `touch .update` — the supervisor runs the update after your turn ends. You will
|
|
160
160
|
NOT die. Finish your turn normally.
|
|
161
161
|
|
|
162
162
|
**On PULSE:** Occasionally check for updates (not every pulse — once every few hours). If a new version exists:
|
|
163
163
|
1. Read release notes: `npm view fluxy-bot releaseNotes --json`
|
|
164
|
-
2. `touch
|
|
164
|
+
2. `touch .update`
|
|
165
165
|
3. Save to daily notes: "Updated from vX to vY" + notable changes to talk with your human later "Btw I updated myself this night"
|
|
166
166
|
|
|
167
167
|
**When user asks to update:**
|
|
168
168
|
1. Check version, if already latest say so
|
|
169
|
-
2. Otherwise read release notes, `touch
|
|
169
|
+
2. Otherwise read release notes, `touch .update`
|
|
170
170
|
3. Tell your human what is new and that the page will be unresponsive for up to 2 minutes while this happens.
|
|
171
171
|
|
|
172
172
|
## Task Files — `tasks/`
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" fill="none">
|
|
2
|
+
<path d="M 240.91,138.91 C 233.17,132.97 219.01,128.82 219.78,105.04 C 222.51,70.32 202.18,34.26 155.19,27.17 C 122.91,22.28 98.81,35.96 80.08,59.11 C 69.99,71.74 59.18,76.12 43.12,75.61 C 39.98,75.51 38.02,75.06 35.42,75.17 C 17.29,75.97 2.11,88.69 2.24,103.78 C 2.35,116.39 13.41,127.29 26.58,127.07 C 35.21,126.92 41.39,123.86 47.89,118.76 C 51.29,116.21 55.71,114.78 58.31,116.93 C 62.68,120.47 59.98,127.97 46.92,136.32 C 32.82,144.78 17.01,156.87 18.19,173.94 C 19.53,191.76 37.19,199.93 54.61,202.21 C 63.18,202.97 70.92,201.38 78.32,201.99 C 88.92,202.91 96.07,207.81 103.91,214.81 C 113.25,223.09 123.72,227.77 136.99,227.34 C 152.17,226.82 163.18,218.71 174.28,207.06 C 184.32,196.76 196.62,191.96 208.76,191.59 C 218.29,191.29 227.23,190.96 235.12,187.22 C 252.26,178.94 259.46,153.81 240.91,138.91 Z" fill="url(#paint0_radial_143_2721)"/>
|
|
3
|
+
<path d="M 142.28,26.41 C 119.01,26.08 98.31,36.68 80.38,58.48 C 70.89,70.46 61.18,76.07 43.66,75.74 C 60.22,76.76 70.92,70.93 79.92,60.09 C 96.71,39.08 117.29,28.48 142.28,26.41 Z" fill="url(#paint1_linear_143_2721)"/>
|
|
4
|
+
<path d="M 200.43,201.63 C 191.8,202.29 184.71,209.18 184.81,214.81 C 184.92,221.44 192.42,226.56 200.01,226.51 C 207.91,226.46 214.46,219.12 214.46,212.93 C 214.46,206.04 207.26,201.15 200.43,201.63 Z" fill="url(#paint2_radial_143_2721)"/>
|
|
5
|
+
<path d="M 241.12,150.21 C 239.69,141.98 232.86,137.88 223.71,139.26 C 210.71,141.38 201.61,157.41 194.78,166.13 C 204.11,159.65 209.17,151.7 222.34,150.1 C 231.13,149.02 237.01,155.06 238.76,156.24 C 240.61,157.46 241.69,153.81 241.12,150.21 Z" fill="url(#paint3_radial_143_2721)"/>
|
|
6
|
+
<path d="M 42.41,80.18 C 33.13,77.01 21.04,78.71 13.84,87.91 C 9.01,94.05 9.49,97.49 11.39,97.65 C 13.73,97.86 16.02,94.16 20.22,90.11 C 26.88,83.64 33.33,82.89 42.41,80.18 Z" fill="url(#paint4_radial_143_2721)"/>
|
|
7
|
+
<path d="M 61.82,142.19 C 55.22,138.19 41.23,143.21 33.74,154.11 C 28.73,161.16 28.16,170.04 33.28,172.1 C 38.69,174.31 46.19,165.86 51.07,160.5 C 56.79,154.21 66.02,144.9 61.82,142.19 Z" fill="url(#paint5_radial_143_2721)"/>
|
|
8
|
+
<path d="M 111.11,86.68 C 105.39,86.68 103.96,91.68 103.96,93.74 C 103.96,99.01 108.01,101.51 111.11,101.51 C 116.98,101.51 119.09,96.87 119.09,93.95 C 119.09,90.06 116.15,86.68 111.11,86.68 Z" fill="url(#paint6_radial_143_2721)"/>
|
|
9
|
+
<path d="M 166.45,86.68 C 160.26,86.68 157.92,91.84 157.92,93.95 C 157.92,99.22 162.02,101.62 165.68,101.62 C 171.09,101.62 173.64,97.13 173.64,93.95 C 173.64,89.85 170.4,86.68 166.45,86.68 Z" fill="url(#paint7_radial_143_2721)"/>
|
|
10
|
+
<path d="M 146.82,99.06 C 143.88,101.83 141.49,102.04 138.79,102.04 C 135.44,102.04 133.16,100.91 130.82,99.06 C 128.81,97.54 126.96,100.76 128.19,102.14 C 130.58,104.96 134.58,106.56 138.79,106.56 C 143.31,106.56 146.82,104.76 148.99,102.14 C 150.32,100.55 148.62,97.33 146.82,99.06 Z" fill="url(#paint8_radial_143_2721)"/>
|
|
11
|
+
<path d="M 199.81,204.07 C 194.82,205.25 191.83,208.94 192.25,211.06 C 192.82,213.88 197.81,213.61 200.41,212.69 C 205.08,211.05 207.15,208.42 206.73,206.41 C 206.31,204.67 203.71,203.29 199.81,204.07 Z" fill="url(#paint9_radial_143_2721)"/>
|
|
12
|
+
<defs>
|
|
13
|
+
<radialGradient id="paint0_radial_143_2721" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(109.896 49.9071) scale(193.071 191.493)">
|
|
14
|
+
<stop stop-color="#00FFFF"/>
|
|
15
|
+
<stop offset="0.354167" stop-color="#9933FF"/>
|
|
16
|
+
<stop offset="0.692708" stop-color="#FE336F"/>
|
|
17
|
+
<stop offset="1" stop-color="#FF7777"/>
|
|
18
|
+
</radialGradient>
|
|
19
|
+
<linearGradient id="paint1_linear_143_2721" x1="113.903" y1="26.3556" x2="113.903" y2="75.8114" gradientUnits="userSpaceOnUse">
|
|
20
|
+
<stop stop-color="#00FFFF"/>
|
|
21
|
+
<stop offset="1" stop-color="#00FFFF" stop-opacity="0.01"/>
|
|
22
|
+
</linearGradient>
|
|
23
|
+
<radialGradient id="paint2_radial_143_2721" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(200.253 209.179) scale(19.3566 17.7296)">
|
|
24
|
+
<stop stop-color="#FFCDBB"/>
|
|
25
|
+
<stop offset="0.994792" stop-color="#FF5555"/>
|
|
26
|
+
</radialGradient>
|
|
27
|
+
<radialGradient id="paint3_radial_143_2721" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(227.953 148.408) rotate(16.5911) scale(18.3468 12.2944)">
|
|
28
|
+
<stop stop-color="#FFD9C5"/>
|
|
29
|
+
<stop offset="1" stop-color="#FFD9C5" stop-opacity="0.01"/>
|
|
30
|
+
</radialGradient>
|
|
31
|
+
<radialGradient id="paint4_radial_143_2721" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(23.7314 86.0908) rotate(29.0072) scale(15.4089 9.09911)">
|
|
32
|
+
<stop stop-color="#99FFFF"/>
|
|
33
|
+
<stop offset="1" stop-color="#00CCFF" stop-opacity="0.01"/>
|
|
34
|
+
</radialGradient>
|
|
35
|
+
<radialGradient id="paint5_radial_143_2721" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(46.4881 153.877) rotate(37.8519) scale(17.2361 10.9818)">
|
|
36
|
+
<stop stop-color="#9999FF"/>
|
|
37
|
+
<stop offset="1" stop-color="#9999FF" stop-opacity="0.01"/>
|
|
38
|
+
</radialGradient>
|
|
39
|
+
<radialGradient id="paint6_radial_143_2721" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(111.098 93.3764) scale(9.10161 8.12939)">
|
|
40
|
+
<stop stop-color="#1E1240"/>
|
|
41
|
+
<stop offset="1" stop-color="#0A003B"/>
|
|
42
|
+
</radialGradient>
|
|
43
|
+
<radialGradient id="paint7_radial_143_2721" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(165.666 93.3764) scale(9.10161 8.12939)">
|
|
44
|
+
<stop stop-color="#1E1240"/>
|
|
45
|
+
<stop offset="1" stop-color="#0A003B"/>
|
|
46
|
+
</radialGradient>
|
|
47
|
+
<radialGradient id="paint8_radial_143_2721" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(138.658 101.292) scale(10.0069 5.26581)">
|
|
48
|
+
<stop stop-color="#1E1240"/>
|
|
49
|
+
<stop offset="1" stop-color="#0A003B"/>
|
|
50
|
+
</radialGradient>
|
|
51
|
+
<radialGradient id="paint9_radial_143_2721" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(199.318 208.168) rotate(16.4322) scale(9.07216 5.68359)">
|
|
52
|
+
<stop stop-color="#FFCDBB"/>
|
|
53
|
+
<stop offset="0.994792" stop-color="#FFCDBB" stop-opacity="0.01"/>
|
|
54
|
+
</radialGradient>
|
|
55
|
+
</defs>
|
|
56
|
+
</svg>
|