fluxy-bot 0.1.41 → 0.1.43

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.
@@ -7,6 +7,7 @@ import DashboardPage from './components/Dashboard/DashboardPage';
7
7
  import ChatView from './components/Chat/ChatView';
8
8
  import FluxyFab from './components/FluxyFab';
9
9
  import BuildOverlay from './components/BuildOverlay';
10
+
10
11
  import OnboardWizard from './components/Onboard/OnboardWizard';
11
12
  import {
12
13
  Sheet,
@@ -71,6 +72,7 @@ export default function App() {
71
72
  <>
72
73
  <ErrorBoundary fallback={<DashboardError />}>
73
74
  <DashboardLayout onOpenOnboard={() => setShowOnboard(true)}>
75
+ <BuildOverlay ws={ws} />
74
76
  <DashboardPage />
75
77
  </DashboardLayout>
76
78
  </ErrorBoundary>
@@ -117,7 +119,6 @@ export default function App() {
117
119
  </SheetContent>
118
120
  </Sheet>
119
121
 
120
- <BuildOverlay ws={ws} />
121
122
  <FluxyFab onClick={() => setChatOpen((o) => !o)} />
122
123
 
123
124
  {/* Onboarding wizard overlay */}
@@ -6,7 +6,7 @@ interface Props {
6
6
  }
7
7
 
8
8
  export default function BuildOverlay({ ws }: Props) {
9
- const [state, setState] = useState<'idle' | 'building' | 'error'>('idle');
9
+ const [toast, setToast] = useState(false);
10
10
  const [error, setError] = useState('');
11
11
  const [copied, setCopied] = useState(false);
12
12
 
@@ -14,27 +14,18 @@ export default function BuildOverlay({ ws }: Props) {
14
14
  if (!ws) return;
15
15
 
16
16
  const unsubs = [
17
- ws.on('build:start', () => {
18
- setState('building');
19
- setError('');
20
- setCopied(false);
21
- }),
22
- ws.on('build:complete', () => {
23
- setState('idle');
24
- // Auto-refresh after a brief delay so the user sees the transition
25
- setTimeout(() => window.location.reload(), 500);
17
+ ws.on('changes:applied', () => {
18
+ setToast(true);
19
+ setTimeout(() => setToast(false), 3000);
26
20
  }),
27
21
  ws.on('build:error', (data: { error: string }) => {
28
- setState('error');
29
- setError(data.error || 'Unknown build error');
22
+ setError(data.error || 'Unknown error');
30
23
  }),
31
24
  ];
32
25
 
33
26
  return () => unsubs.forEach((u) => u());
34
27
  }, [ws]);
35
28
 
36
- if (state === 'idle') return null;
37
-
38
29
  const handleCopy = () => {
39
30
  navigator.clipboard.writeText(error).then(() => {
40
31
  setCopied(true);
@@ -43,18 +34,19 @@ export default function BuildOverlay({ ws }: Props) {
43
34
  };
44
35
 
45
36
  return (
46
- <div className="fixed inset-0 z-[100] bg-black/80 backdrop-blur-sm flex items-center justify-center p-6">
47
- {state === 'building' && (
48
- <div className="text-center">
49
- <div className="inline-block h-8 w-8 animate-spin rounded-full border-2 border-white/20 border-t-white mb-4" />
50
- <p className="text-sm text-white/80">Building...</p>
37
+ <>
38
+ {/* Toast notification */}
39
+ {toast && (
40
+ <div className="fixed bottom-4 right-4 z-50 px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-lg shadow-lg text-sm text-zinc-300 animate-in fade-in slide-in-from-bottom-2 duration-200">
41
+ Dashboard updated
51
42
  </div>
52
43
  )}
53
44
 
54
- {state === 'error' && (
55
- <div className="w-full max-w-lg bg-zinc-900 rounded-lg border border-red-500/30 overflow-hidden">
45
+ {/* HMR error non-blocking, dismissible */}
46
+ {error && (
47
+ <div className="fixed bottom-4 right-4 z-50 w-96 max-w-[calc(100vw-2rem)] bg-zinc-900 rounded-lg border border-red-500/30 shadow-lg overflow-hidden">
56
48
  <div className="flex items-center justify-between px-4 py-3 border-b border-red-500/20">
57
- <span className="text-sm font-medium text-red-400">Build Failed</span>
49
+ <span className="text-sm font-medium text-red-400">HMR Error</span>
58
50
  <div className="flex gap-2">
59
51
  <button
60
52
  onClick={handleCopy}
@@ -63,14 +55,14 @@ export default function BuildOverlay({ ws }: Props) {
63
55
  {copied ? 'Copied' : 'Copy'}
64
56
  </button>
65
57
  <button
66
- onClick={() => setState('idle')}
58
+ onClick={() => setError('')}
67
59
  className="px-3 py-1 text-xs rounded bg-white/10 hover:bg-white/20 text-white/80 transition-colors"
68
60
  >
69
61
  Dismiss
70
62
  </button>
71
63
  </div>
72
64
  </div>
73
- <pre className="p-4 text-xs text-red-300/90 overflow-auto max-h-64 whitespace-pre-wrap font-mono">
65
+ <pre className="p-4 text-xs text-red-300/90 overflow-auto max-h-48 whitespace-pre-wrap font-mono">
74
66
  {error}
75
67
  </pre>
76
68
  <p className="px-4 pb-3 text-xs text-white/40">
@@ -78,6 +70,6 @@ export default function BuildOverlay({ ws }: Props) {
78
70
  </p>
79
71
  </div>
80
72
  )}
81
- </div>
73
+ </>
82
74
  );
83
75
  }
@@ -17,7 +17,7 @@ export default function DashboardLayout({ children, onOpenOnboard }: Props) {
17
17
  <Sidebar />
18
18
  </div>
19
19
  {/* Main content */}
20
- <main className="flex-1 overflow-y-auto p-4 md:p-6">{children}</main>
20
+ <main className="relative flex-1 overflow-y-auto p-4 md:p-6">{children}</main>
21
21
  </div>
22
22
  </div>
23
23
  );
@@ -52,56 +52,67 @@ 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 (with retry for worker restarts)
56
56
  useEffect(() => {
57
57
  if (!conversationId) return;
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
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
+ }
78
83
  }
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
- // Network error (worker may be restarting) — don't clear, just ignore
104
- });
84
+
85
+ let attachments: StoredAttachment[] | undefined;
86
+ if (m.attachments) {
87
+ try {
88
+ attachments = JSON.parse(m.attachments);
89
+ } catch { /* ignore malformed */ }
90
+ }
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; };
105
116
  }, [conversationId]);
106
117
 
107
118
  // Persist conversationId to DB when it changes
@@ -121,7 +132,8 @@ export function useChat(ws: WsClient | null) {
121
132
  if (!ws) return;
122
133
 
123
134
  const unsubs = [
124
- ws.on('bot:typing', () => {
135
+ ws.on('bot:typing', (data: { conversationId?: string }) => {
136
+ if (data.conversationId) setConversationId(data.conversationId);
125
137
  setStreaming(true);
126
138
  setTools([]);
127
139
  }),
@@ -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 = import.meta.env.DEV ? 'localhost:3000' : location.host;
23
+ const host = location.host;
24
24
  this.url = url ?? `${proto}//${host}/ws`;
25
25
  }
26
26