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.
- package/bin/cli.js +1 -3
- 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 -83
- package/client/src/components/Layout/DashboardLayout.tsx +1 -1
- package/client/src/hooks/useChat.ts +51 -62
- package/client/src/hooks/useFluxyChat.ts +119 -0
- package/client/src/lib/ws-client.ts +1 -1
- 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 +14 -9
- package/scripts/postinstall.js +10 -26
- package/shared/paths.ts +2 -11
- package/supervisor/index.ts +130 -176
- package/supervisor/widget.js +75 -0
- package/supervisor/worker.ts +3 -16
- package/tsconfig.json +2 -3
- package/vite.config.ts +4 -1
- package/vite.fluxy.config.ts +19 -0
- package/{supervisor → worker}/claude-agent.ts +43 -50
- package/{shared → worker}/db.ts +1 -9
- package/{shared → worker}/file-storage.ts +1 -1
- package/worker/index.ts +133 -31
- package/worker/prompts/fluxy-system-prompt.txt +8 -0
- package/client/src/components/BuildOverlay.tsx +0 -75
- package/client/src/components/FluxyFab.tsx +0 -29
- package/client/src/hooks/useWebSocket.ts +0 -22
- package/dist/assets/index-BAUWfBMW.js +0 -100
- package/dist/assets/index-CiN0-4-O.css +0 -1
- package/shared/workspace.ts +0 -196
- package/supervisor/prompts/fluxy-system-prompt.txt +0 -35
- 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
package/client/src/App.tsx
CHANGED
|
@@ -1,26 +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
|
-
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="
|
|
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
|
|
55
|
+
// Load messages when conversationId is set
|
|
56
56
|
useEffect(() => {
|
|
57
57
|
if (!conversationId) return;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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', (
|
|
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
|
|