fluxy-bot 0.2.0 → 0.2.2
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/client/fluxy-main.tsx +75 -0
- package/client/fluxy.html +12 -0
- package/client/index.html +1 -0
- package/client/src/App.tsx +2 -80
- package/client/src/hooks/useFluxyChat.ts +119 -0
- package/dist/assets/index-BxQ8et35.js +64 -0
- package/dist/assets/index-D2PQx64r.css +1 -0
- package/dist/index.html +3 -2
- package/dist/sw.js +1 -1
- package/dist-fluxy/assets/fluxy-B49yi-07.js +53 -0
- package/dist-fluxy/assets/fluxy-D2PQx64r.css +1 -0
- package/dist-fluxy/fluxy.html +13 -0
- package/dist-fluxy/fluxy.png +0 -0
- package/dist-fluxy/fluxy_frame1.png +0 -0
- package/dist-fluxy/fluxy_say_hi.webm +0 -0
- package/dist-fluxy/fluxy_tilts.webm +0 -0
- package/dist-fluxy/icons/claude.png +0 -0
- package/dist-fluxy/icons/codex.png +0 -0
- package/dist-fluxy/icons/openai.svg +15 -0
- package/package.json +5 -2
- package/shared/paths.ts +2 -1
- package/supervisor/fluxy-agent.ts +169 -0
- package/supervisor/index.ts +91 -21
- package/supervisor/widget.js +75 -0
- package/vite.fluxy.config.ts +19 -0
- package/client/src/components/FluxyFab.tsx +0 -29
- package/client/src/hooks/useWebSocket.ts +0 -22
- package/dist/assets/index-D8wa5QyC.js +0 -100
- package/dist/assets/index-Dpj8titN.css +0 -1
- package/supervisor/fluxy.html +0 -94
|
@@ -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
package/client/src/App.tsx
CHANGED
|
@@ -1,24 +1,8 @@
|
|
|
1
|
-
import { useState, useEffect
|
|
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
5
|
import OnboardWizard from './components/Onboard/OnboardWizard';
|
|
10
|
-
import {
|
|
11
|
-
Sheet,
|
|
12
|
-
SheetContent,
|
|
13
|
-
SheetTitle,
|
|
14
|
-
SheetDescription,
|
|
15
|
-
} from './components/ui/sheet';
|
|
16
|
-
import {
|
|
17
|
-
DropdownMenu,
|
|
18
|
-
DropdownMenuTrigger,
|
|
19
|
-
DropdownMenuContent,
|
|
20
|
-
DropdownMenuItem,
|
|
21
|
-
} from './components/ui/dropdown-menu';
|
|
22
6
|
|
|
23
7
|
function DashboardError() {
|
|
24
8
|
return (
|
|
@@ -34,36 +18,19 @@ function DashboardError() {
|
|
|
34
18
|
}
|
|
35
19
|
|
|
36
20
|
export default function App() {
|
|
37
|
-
const [chatOpen, setChatOpen] = useState(false);
|
|
38
21
|
const [showOnboard, setShowOnboard] = useState(false);
|
|
39
|
-
const [botName, setBotName] = useState('Fluxy');
|
|
40
|
-
const [whisperEnabled, setWhisperEnabled] = useState(false);
|
|
41
|
-
const { ws, connected } = useWebSocket();
|
|
42
|
-
const clearContextRef = useRef<(() => void) | null>(null);
|
|
43
22
|
|
|
44
23
|
useEffect(() => {
|
|
45
24
|
fetch('/api/settings')
|
|
46
25
|
.then((r) => r.json())
|
|
47
26
|
.then((s) => {
|
|
48
27
|
if (s.onboard_complete !== 'true') setShowOnboard(true);
|
|
49
|
-
if (s.agent_name) setBotName(s.agent_name);
|
|
50
|
-
if (s.whisper_enabled === 'true') setWhisperEnabled(true);
|
|
51
28
|
})
|
|
52
|
-
.catch(() => {
|
|
53
|
-
// Network error — bot is unreachable, don't assume not onboarded
|
|
54
|
-
});
|
|
29
|
+
.catch(() => {});
|
|
55
30
|
}, []);
|
|
56
31
|
|
|
57
|
-
// Refresh bot name + whisper after onboarding completes
|
|
58
32
|
const handleOnboardComplete = () => {
|
|
59
33
|
setShowOnboard(false);
|
|
60
|
-
fetch('/api/settings')
|
|
61
|
-
.then((r) => r.json())
|
|
62
|
-
.then((s) => {
|
|
63
|
-
if (s.agent_name) setBotName(s.agent_name);
|
|
64
|
-
setWhisperEnabled(s.whisper_enabled === 'true');
|
|
65
|
-
})
|
|
66
|
-
.catch(() => {});
|
|
67
34
|
};
|
|
68
35
|
|
|
69
36
|
return (
|
|
@@ -74,51 +41,6 @@ export default function App() {
|
|
|
74
41
|
</DashboardLayout>
|
|
75
42
|
</ErrorBoundary>
|
|
76
43
|
|
|
77
|
-
{/* Chat sheet — slides from right */}
|
|
78
|
-
<Sheet open={chatOpen} onOpenChange={setChatOpen}>
|
|
79
|
-
<SheetContent
|
|
80
|
-
side="right"
|
|
81
|
-
className="w-full sm:max-w-md !gap-0 !p-0 !border-l-0 !h-dvh overflow-hidden"
|
|
82
|
-
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
83
|
-
>
|
|
84
|
-
{/* Header — pr-12 to avoid overlap with built-in X button */}
|
|
85
|
-
<div className="flex items-center gap-3 px-4 pr-12 py-3 border-b border-border shrink-0">
|
|
86
|
-
<img src="/fluxy.png" alt={botName} className="h-5 w-auto" />
|
|
87
|
-
<SheetTitle className="text-sm font-semibold">{botName}</SheetTitle>
|
|
88
|
-
<div
|
|
89
|
-
className={`h-2 w-2 rounded-full ${connected ? 'bg-green-500' : 'bg-red-500'}`}
|
|
90
|
-
/>
|
|
91
|
-
<div className="flex-1" />
|
|
92
|
-
<DropdownMenu>
|
|
93
|
-
<DropdownMenuTrigger asChild>
|
|
94
|
-
<button
|
|
95
|
-
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"
|
|
96
|
-
>
|
|
97
|
-
<EllipsisVertical className="h-4 w-4" />
|
|
98
|
-
</button>
|
|
99
|
-
</DropdownMenuTrigger>
|
|
100
|
-
<DropdownMenuContent align="end" sideOffset={8}>
|
|
101
|
-
<DropdownMenuItem onClick={() => clearContextRef.current?.()}>
|
|
102
|
-
<Trash2 className="h-3.5 w-3.5" />
|
|
103
|
-
Clear Context
|
|
104
|
-
</DropdownMenuItem>
|
|
105
|
-
</DropdownMenuContent>
|
|
106
|
-
</DropdownMenu>
|
|
107
|
-
<SheetDescription className="sr-only">
|
|
108
|
-
Chat with your {botName} AI assistant
|
|
109
|
-
</SheetDescription>
|
|
110
|
-
</div>
|
|
111
|
-
|
|
112
|
-
{/* Chat body — fills remaining height */}
|
|
113
|
-
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
|
114
|
-
<ChatView ws={ws} clearContextRef={clearContextRef} whisperEnabled={whisperEnabled} />
|
|
115
|
-
</div>
|
|
116
|
-
</SheetContent>
|
|
117
|
-
</Sheet>
|
|
118
|
-
|
|
119
|
-
<FluxyFab onClick={() => setChatOpen((o) => !o)} />
|
|
120
|
-
|
|
121
|
-
{/* Onboarding wizard overlay */}
|
|
122
44
|
{showOnboard && <OnboardWizard onComplete={handleOnboardComplete} />}
|
|
123
45
|
</>
|
|
124
46
|
);
|
|
@@ -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
|
+
}
|