fluxy-bot 0.1.46 → 0.2.1

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 (45) hide show
  1. package/bin/cli.js +1 -3
  2. package/client/fluxy-main.tsx +75 -0
  3. package/client/fluxy.html +12 -0
  4. package/client/index.html +1 -0
  5. package/client/src/App.tsx +2 -83
  6. package/client/src/components/Layout/DashboardLayout.tsx +1 -1
  7. package/client/src/hooks/useChat.ts +51 -62
  8. package/client/src/hooks/useFluxyChat.ts +119 -0
  9. package/client/src/lib/ws-client.ts +1 -1
  10. package/dist/assets/index-BxQ8et35.js +64 -0
  11. package/dist/assets/index-D2PQx64r.css +1 -0
  12. package/dist/index.html +3 -2
  13. package/dist/sw.js +1 -1
  14. package/dist-fluxy/assets/fluxy-B49yi-07.js +53 -0
  15. package/dist-fluxy/assets/fluxy-D2PQx64r.css +1 -0
  16. package/dist-fluxy/fluxy.html +13 -0
  17. package/dist-fluxy/fluxy.png +0 -0
  18. package/dist-fluxy/fluxy_frame1.png +0 -0
  19. package/dist-fluxy/fluxy_say_hi.webm +0 -0
  20. package/dist-fluxy/fluxy_tilts.webm +0 -0
  21. package/dist-fluxy/icons/claude.png +0 -0
  22. package/dist-fluxy/icons/codex.png +0 -0
  23. package/dist-fluxy/icons/openai.svg +15 -0
  24. package/package.json +14 -9
  25. package/scripts/postinstall.js +10 -26
  26. package/shared/paths.ts +2 -11
  27. package/supervisor/index.ts +130 -176
  28. package/supervisor/widget.js +75 -0
  29. package/supervisor/worker.ts +3 -16
  30. package/tsconfig.json +2 -3
  31. package/vite.config.ts +4 -1
  32. package/vite.fluxy.config.ts +19 -0
  33. package/{supervisor → worker}/claude-agent.ts +43 -50
  34. package/{shared → worker}/db.ts +1 -9
  35. package/{shared → worker}/file-storage.ts +1 -1
  36. package/worker/index.ts +133 -31
  37. package/worker/prompts/fluxy-system-prompt.txt +8 -0
  38. package/client/src/components/BuildOverlay.tsx +0 -75
  39. package/client/src/components/FluxyFab.tsx +0 -29
  40. package/client/src/hooks/useWebSocket.ts +0 -22
  41. package/dist/assets/index-BAUWfBMW.js +0 -100
  42. package/dist/assets/index-CiN0-4-O.css +0 -1
  43. package/shared/workspace.ts +0 -196
  44. package/supervisor/prompts/fluxy-system-prompt.txt +0 -35
  45. package/supervisor/vite-dev.ts +0 -75
package/bin/cli.js CHANGED
@@ -8,10 +8,8 @@ import { fileURLToPath } from 'url';
8
8
 
9
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
10
 
11
+ const ROOT = path.resolve(__dirname, '..');
11
12
  const DATA_DIR = path.join(os.homedir(), '.fluxy');
12
- const APP_DIR = path.join(DATA_DIR, 'app');
13
- // Use ~/.fluxy/app/ if it exists (editable copy), otherwise fall back to npm install location
14
- const ROOT = fs.existsSync(APP_DIR) ? APP_DIR : path.resolve(__dirname, '..');
15
13
  const CONFIG_PATH = path.join(DATA_DIR, 'config.json');
16
14
  const BIN_DIR = path.join(DATA_DIR, 'bin');
17
15
  const CF_PATH = path.join(BIN_DIR, 'cloudflared');
@@ -0,0 +1,75 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import { Trash2 } from 'lucide-react';
4
+ import { WsClient } from './src/lib/ws-client';
5
+ import { useFluxyChat } from './src/hooks/useFluxyChat';
6
+ import MessageList from './src/components/Chat/MessageList';
7
+ import InputBar from './src/components/Chat/InputBar';
8
+ import './src/styles/globals.css';
9
+
10
+ function FluxyApp() {
11
+ const clientRef = useRef<WsClient | null>(null);
12
+ const [connected, setConnected] = useState(false);
13
+ const [botName, setBotName] = useState('Fluxy');
14
+ const [whisperEnabled, setWhisperEnabled] = useState(false);
15
+
16
+ useEffect(() => {
17
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
18
+ const host = location.host;
19
+ const client = new WsClient(`${proto}//${host}/fluxy/ws`);
20
+ clientRef.current = client;
21
+
22
+ const unsub = client.onStatus(setConnected);
23
+ client.connect();
24
+
25
+ return () => {
26
+ unsub();
27
+ client.disconnect();
28
+ };
29
+ }, []);
30
+
31
+ // Try to load settings (will work when worker is up, fail silently when down)
32
+ useEffect(() => {
33
+ fetch('/api/settings')
34
+ .then((r) => r.json())
35
+ .then((s) => {
36
+ if (s.agent_name) setBotName(s.agent_name);
37
+ if (s.whisper_enabled === 'true') setWhisperEnabled(true);
38
+ })
39
+ .catch(() => {});
40
+ }, []);
41
+
42
+ const { messages, streaming, streamBuffer, tools, sendMessage, stopStreaming, clearContext } =
43
+ useFluxyChat(clientRef.current);
44
+
45
+ return (
46
+ <div className="flex flex-col h-dvh overflow-hidden">
47
+ {/* Header */}
48
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-border shrink-0">
49
+ <img src="/fluxy.png" alt={botName} className="h-5 w-auto" />
50
+ <span className="text-sm font-semibold">{botName}</span>
51
+ <div className={`h-2 w-2 rounded-full ${connected ? 'bg-green-500' : 'bg-red-500'}`} />
52
+ <div className="flex-1" />
53
+ <button
54
+ onClick={clearContext}
55
+ 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"
56
+ title="Clear context"
57
+ >
58
+ <Trash2 className="h-4 w-4" />
59
+ </button>
60
+ </div>
61
+
62
+ {/* Chat body */}
63
+ <div className="flex-1 min-h-0 flex flex-col overflow-hidden">
64
+ <MessageList messages={messages} streaming={streaming} streamBuffer={streamBuffer} tools={tools} />
65
+ <InputBar onSend={sendMessage} onStop={stopStreaming} streaming={streaming} whisperEnabled={whisperEnabled} />
66
+ </div>
67
+ </div>
68
+ );
69
+ }
70
+
71
+ ReactDOM.createRoot(document.getElementById('root')!).render(
72
+ <React.StrictMode>
73
+ <FluxyApp />
74
+ </React.StrictMode>,
75
+ );
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content" />
6
+ <title>Fluxy Chat</title>
7
+ </head>
8
+ <body class="bg-background text-foreground">
9
+ <div id="root"></div>
10
+ <script type="module" src="/fluxy-main.tsx"></script>
11
+ </body>
12
+ </html>
package/client/index.html CHANGED
@@ -9,5 +9,6 @@
9
9
  <body class="bg-background text-foreground">
10
10
  <div id="root"></div>
11
11
  <script type="module" src="/src/main.tsx"></script>
12
+ <script src="/fluxy/widget.js"></script>
12
13
  </body>
13
14
  </html>
@@ -1,26 +1,8 @@
1
- import { useState, useEffect, useRef } from 'react';
2
- import { useWebSocket } from './hooks/useWebSocket';
3
- import { EllipsisVertical, Trash2 } from 'lucide-react';
1
+ import { useState, useEffect } from 'react';
4
2
  import ErrorBoundary from './components/ErrorBoundary';
5
3
  import DashboardLayout from './components/Layout/DashboardLayout';
6
4
  import DashboardPage from './components/Dashboard/DashboardPage';
7
- import ChatView from './components/Chat/ChatView';
8
- import FluxyFab from './components/FluxyFab';
9
- import BuildOverlay from './components/BuildOverlay';
10
-
11
5
  import OnboardWizard from './components/Onboard/OnboardWizard';
12
- import {
13
- Sheet,
14
- SheetContent,
15
- SheetTitle,
16
- SheetDescription,
17
- } from './components/ui/sheet';
18
- import {
19
- DropdownMenu,
20
- DropdownMenuTrigger,
21
- DropdownMenuContent,
22
- DropdownMenuItem,
23
- } from './components/ui/dropdown-menu';
24
6
 
25
7
  function DashboardError() {
26
8
  return (
@@ -36,92 +18,29 @@ function DashboardError() {
36
18
  }
37
19
 
38
20
  export default function App() {
39
- const [chatOpen, setChatOpen] = useState(false);
40
21
  const [showOnboard, setShowOnboard] = useState(false);
41
- const [botName, setBotName] = useState('Fluxy');
42
- const [whisperEnabled, setWhisperEnabled] = useState(false);
43
- const { ws, connected } = useWebSocket();
44
- const clearContextRef = useRef<(() => void) | null>(null);
45
22
 
46
23
  useEffect(() => {
47
24
  fetch('/api/settings')
48
25
  .then((r) => r.json())
49
26
  .then((s) => {
50
27
  if (s.onboard_complete !== 'true') setShowOnboard(true);
51
- if (s.agent_name) setBotName(s.agent_name);
52
- if (s.whisper_enabled === 'true') setWhisperEnabled(true);
53
28
  })
54
- .catch(() => {
55
- // Network error — bot is unreachable, don't assume not onboarded
56
- });
29
+ .catch(() => {});
57
30
  }, []);
58
31
 
59
- // Refresh bot name + whisper after onboarding completes
60
32
  const handleOnboardComplete = () => {
61
33
  setShowOnboard(false);
62
- fetch('/api/settings')
63
- .then((r) => r.json())
64
- .then((s) => {
65
- if (s.agent_name) setBotName(s.agent_name);
66
- setWhisperEnabled(s.whisper_enabled === 'true');
67
- })
68
- .catch(() => {});
69
34
  };
70
35
 
71
36
  return (
72
37
  <>
73
38
  <ErrorBoundary fallback={<DashboardError />}>
74
39
  <DashboardLayout onOpenOnboard={() => setShowOnboard(true)}>
75
- <BuildOverlay ws={ws} />
76
40
  <DashboardPage />
77
41
  </DashboardLayout>
78
42
  </ErrorBoundary>
79
43
 
80
- {/* Chat sheet — slides from right */}
81
- <Sheet open={chatOpen} onOpenChange={setChatOpen}>
82
- <SheetContent
83
- side="right"
84
- className="w-full sm:max-w-md !gap-0 !p-0 !border-l-0 !h-dvh overflow-hidden"
85
- onOpenAutoFocus={(e) => e.preventDefault()}
86
- >
87
- {/* Header — pr-12 to avoid overlap with built-in X button */}
88
- <div className="flex items-center gap-3 px-4 pr-12 py-3 border-b border-border shrink-0">
89
- <img src="/fluxy.png" alt={botName} className="h-5 w-auto" />
90
- <SheetTitle className="text-sm font-semibold">{botName}</SheetTitle>
91
- <div
92
- className={`h-2 w-2 rounded-full ${connected ? 'bg-green-500' : 'bg-red-500'}`}
93
- />
94
- <div className="flex-1" />
95
- <DropdownMenu>
96
- <DropdownMenuTrigger asChild>
97
- <button
98
- 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"
99
- >
100
- <EllipsisVertical className="h-4 w-4" />
101
- </button>
102
- </DropdownMenuTrigger>
103
- <DropdownMenuContent align="end" sideOffset={8}>
104
- <DropdownMenuItem onClick={() => clearContextRef.current?.()}>
105
- <Trash2 className="h-3.5 w-3.5" />
106
- Clear Context
107
- </DropdownMenuItem>
108
- </DropdownMenuContent>
109
- </DropdownMenu>
110
- <SheetDescription className="sr-only">
111
- Chat with your {botName} AI assistant
112
- </SheetDescription>
113
- </div>
114
-
115
- {/* Chat body — fills remaining height */}
116
- <div className="flex-1 min-h-0 flex flex-col overflow-hidden">
117
- <ChatView ws={ws} clearContextRef={clearContextRef} whisperEnabled={whisperEnabled} />
118
- </div>
119
- </SheetContent>
120
- </Sheet>
121
-
122
- <FluxyFab onClick={() => setChatOpen((o) => !o)} />
123
-
124
- {/* Onboarding wizard overlay */}
125
44
  {showOnboard && <OnboardWizard onComplete={handleOnboardComplete} />}
126
45
  </>
127
46
  );
@@ -17,7 +17,7 @@ export default function DashboardLayout({ children, onOpenOnboard }: Props) {
17
17
  <Sidebar />
18
18
  </div>
19
19
  {/* Main content */}
20
- <main className="relative flex-1 overflow-y-auto p-4 md:p-6">{children}</main>
20
+ <main className="flex-1 overflow-y-auto p-4 md:p-6">{children}</main>
21
21
  </div>
22
22
  </div>
23
23
  );
@@ -52,67 +52,58 @@ export function useChat(ws: WsClient | null) {
52
52
  .catch(() => {});
53
53
  }, []);
54
54
 
55
- // Load messages when conversationId is set (with retry for worker restarts)
55
+ // Load messages when conversationId is set
56
56
  useEffect(() => {
57
57
  if (!conversationId) return;
58
- let cancelled = false;
59
- let retries = 0;
60
-
61
- const loadMessages = () => {
62
- fetch(`/api/conversations/${conversationId}`)
63
- .then((r) => {
64
- if (!r.ok) throw new Error('not found');
65
- return r.json();
66
- })
67
- .then((data) => {
68
- if (cancelled) return;
69
- if (data.messages?.length) {
70
- setMessages(
71
- data.messages
72
- .filter((m: any) => m.role === 'user' || m.role === 'assistant')
73
- .map((m: any) => {
74
- let audioData: string | undefined;
75
- if (m.audio_data) {
76
- if (m.audio_data.startsWith('data:')) {
77
- audioData = m.audio_data;
78
- } else if (m.audio_data.includes('/')) {
79
- audioData = `/api/files/${m.audio_data}`;
80
- } else {
81
- audioData = `data:audio/webm;base64,${m.audio_data}`;
82
- }
83
- }
84
-
85
- let attachments: StoredAttachment[] | undefined;
86
- if (m.attachments) {
87
- try {
88
- attachments = JSON.parse(m.attachments);
89
- } catch { /* ignore malformed */ }
58
+ fetch(`/api/conversations/${conversationId}`)
59
+ .then((r) => {
60
+ if (!r.ok) throw new Error('not found');
61
+ return r.json();
62
+ })
63
+ .then((data) => {
64
+ if (data.messages?.length) {
65
+ setMessages(
66
+ data.messages
67
+ .filter((m: any) => m.role === 'user' || m.role === 'assistant')
68
+ .map((m: any) => {
69
+ // Backward compat for audio_data: file path → URL, data: prefix → legacy, else → prepend data URL
70
+ let audioData: string | undefined;
71
+ if (m.audio_data) {
72
+ if (m.audio_data.startsWith('data:')) {
73
+ audioData = m.audio_data; // legacy data URL
74
+ } else if (m.audio_data.includes('/')) {
75
+ audioData = `/api/files/${m.audio_data}`; // file path → HTTP URL
76
+ } else {
77
+ audioData = `data:audio/webm;base64,${m.audio_data}`; // raw base64
90
78
  }
91
-
92
- return {
93
- id: m.id,
94
- role: m.role,
95
- content: m.content,
96
- timestamp: m.created_at,
97
- audioData,
98
- hasAttachments: !!(attachments && attachments.length > 0),
99
- attachments,
100
- };
101
- }),
102
- );
103
- }
104
- })
105
- .catch(() => {
106
- // Worker may be restarting — retry a few times
107
- if (!cancelled && retries < 5) {
108
- retries++;
109
- setTimeout(loadMessages, 1000);
110
- }
111
- });
112
- };
113
-
114
- loadMessages();
115
- return () => { cancelled = true; };
79
+ }
80
+
81
+ // Parse stored attachments
82
+ let attachments: StoredAttachment[] | undefined;
83
+ if (m.attachments) {
84
+ try {
85
+ attachments = JSON.parse(m.attachments);
86
+ } catch { /* ignore malformed */ }
87
+ }
88
+
89
+ return {
90
+ id: m.id,
91
+ role: m.role,
92
+ content: m.content,
93
+ timestamp: m.created_at,
94
+ audioData,
95
+ hasAttachments: !!(attachments && attachments.length > 0),
96
+ attachments,
97
+ };
98
+ }),
99
+ );
100
+ }
101
+ })
102
+ .catch(() => {
103
+ // Conversation gone clear
104
+ setConversationId(null);
105
+ fetch('/api/context/clear', { method: 'POST' }).catch(() => {});
106
+ });
116
107
  }, [conversationId]);
117
108
 
118
109
  // Persist conversationId to DB when it changes
@@ -132,8 +123,7 @@ export function useChat(ws: WsClient | null) {
132
123
  if (!ws) return;
133
124
 
134
125
  const unsubs = [
135
- ws.on('bot:typing', (data: { conversationId?: string }) => {
136
- if (data.conversationId) setConversationId(data.conversationId);
126
+ ws.on('bot:typing', () => {
137
127
  setStreaming(true);
138
128
  setTools([]);
139
129
  }),
@@ -233,14 +223,13 @@ export function useChat(ws: WsClient | null) {
233
223
  }, [ws, conversationId]);
234
224
 
235
225
  const clearContext = useCallback(() => {
236
- // Clear UI state only — starts a fresh conversation on next message.
237
- // Old messages stay in DB (used as context for the agent).
238
226
  setMessages([]);
239
227
  setConversationId(null);
240
228
  setStreamBuffer('');
241
229
  setStreaming(false);
242
230
  setTools([]);
243
231
  prevConvId.current = null;
232
+ loaded.current = false;
244
233
  fetch('/api/context/clear', { method: 'POST' }).catch(() => {});
245
234
  }, []);
246
235
 
@@ -0,0 +1,119 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import type { WsClient } from '../lib/ws-client';
3
+ import type { ChatMessage, ToolActivity, Attachment } from './useChat';
4
+
5
+ /**
6
+ * Simplified chat hook for the standalone Fluxy chat app.
7
+ * In-memory only — no persistence API calls.
8
+ */
9
+ export function useFluxyChat(ws: WsClient | null) {
10
+ const [messages, setMessages] = useState<ChatMessage[]>([]);
11
+ const [conversationId, setConversationId] = useState<string | null>(null);
12
+ const [streaming, setStreaming] = useState(false);
13
+ const [streamBuffer, setStreamBuffer] = useState('');
14
+ const [tools, setTools] = useState<ToolActivity[]>([]);
15
+
16
+ useEffect(() => {
17
+ if (!ws) return;
18
+
19
+ const unsubs = [
20
+ ws.on('bot:typing', () => {
21
+ setStreaming(true);
22
+ setTools([]);
23
+ }),
24
+ ws.on('bot:token', (data: { token: string }) => {
25
+ setStreamBuffer((buf) => buf + data.token);
26
+ }),
27
+ ws.on('bot:tool', (data: { name: string; status?: string }) => {
28
+ setTools((prev) => {
29
+ const existing = prev.find((t) => t.name === data.name && t.status === 'running');
30
+ if (existing) return prev;
31
+ return [...prev, { name: data.name, status: 'running' }];
32
+ });
33
+ }),
34
+ ws.on('bot:response', (data: { conversationId: string; messageId?: string; content: string }) => {
35
+ setConversationId(data.conversationId);
36
+ setMessages((msgs) => [
37
+ ...msgs,
38
+ {
39
+ id: data.messageId || Date.now().toString(),
40
+ role: 'assistant',
41
+ content: data.content,
42
+ timestamp: new Date().toISOString(),
43
+ },
44
+ ]);
45
+ setStreamBuffer('');
46
+ setStreaming(false);
47
+ setTools([]);
48
+ }),
49
+ ws.on('bot:error', (data: { error: string }) => {
50
+ setStreamBuffer('');
51
+ setStreaming(false);
52
+ setTools([]);
53
+ setMessages((msgs) => [
54
+ ...msgs,
55
+ {
56
+ id: Date.now().toString(),
57
+ role: 'assistant',
58
+ content: `Error: ${data.error}`,
59
+ timestamp: new Date().toISOString(),
60
+ },
61
+ ]);
62
+ }),
63
+ ];
64
+
65
+ return () => unsubs.forEach((u) => u());
66
+ }, [ws]);
67
+
68
+ const sendMessage = useCallback(
69
+ (content: string, attachments?: Attachment[], audioData?: string) => {
70
+ if (!ws || (!content.trim() && (!attachments || attachments.length === 0))) return;
71
+
72
+ const userMsg: ChatMessage = {
73
+ id: Date.now().toString(),
74
+ role: 'user',
75
+ content,
76
+ timestamp: new Date().toISOString(),
77
+ hasAttachments: !!(attachments && attachments.length > 0),
78
+ audioData: audioData ? (audioData.startsWith('data:') ? audioData : `data:audio/webm;base64,${audioData}`) : undefined,
79
+ };
80
+ setMessages((msgs) => [...msgs, userMsg]);
81
+
82
+ const payload: any = { conversationId, content };
83
+ if (audioData) {
84
+ payload.audioData = audioData.includes(',') ? audioData.split(',')[1] : audioData;
85
+ }
86
+ if (attachments?.length) {
87
+ payload.attachments = attachments.map((att) => {
88
+ const match = att.preview.match(/^data:([^;]+);base64,(.+)$/);
89
+ return {
90
+ type: att.type,
91
+ name: att.name,
92
+ mediaType: match?.[1] || 'application/octet-stream',
93
+ data: match?.[2] || '',
94
+ };
95
+ });
96
+ }
97
+ ws.send('user:message', payload);
98
+ },
99
+ [ws, conversationId],
100
+ );
101
+
102
+ const stopStreaming = useCallback(() => {
103
+ if (!ws) return;
104
+ ws.send('user:stop', { conversationId });
105
+ setStreaming(false);
106
+ setStreamBuffer('');
107
+ setTools([]);
108
+ }, [ws, conversationId]);
109
+
110
+ const clearContext = useCallback(() => {
111
+ setMessages([]);
112
+ setConversationId(null);
113
+ setStreamBuffer('');
114
+ setStreaming(false);
115
+ setTools([]);
116
+ }, []);
117
+
118
+ return { messages, streaming, streamBuffer, conversationId, tools, sendMessage, stopStreaming, clearContext };
119
+ }
@@ -20,7 +20,7 @@ export class WsClient {
20
20
 
21
21
  constructor(url?: string) {
22
22
  const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
23
- const host = location.host;
23
+ const host = import.meta.env.DEV ? 'localhost:3000' : location.host;
24
24
  this.url = url ?? `${proto}//${host}/ws`;
25
25
  }
26
26