fluxy-bot 0.1.21 → 0.1.23

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.
Binary file
@@ -35,7 +35,7 @@ const MODELS: Record<string, { id: string; label: string }[]> = {
35
35
  const TOTAL_STEPS = 6; // 0..5
36
36
 
37
37
  const HANDLES = [
38
- { tier: 'premium', label: (n: string) => `${n}.fluxy.bot`, badge: '$5', badgeCls: 'bg-primary/15 text-primary border-primary/20', highlight: true },
38
+ { tier: 'premium', label: (n: string) => `${n}.fluxy.bot`, badge: '$5', badgeCls: 'bg-[#AF27E3]/15 text-[#AF27E3] border-[#AF27E3]/20', highlight: true },
39
39
  { tier: 'at', label: (n: string) => `${n}.at.fluxy.bot`, badge: 'Free', badgeCls: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20', highlight: false },
40
40
  ] as const;
41
41
 
@@ -61,7 +61,7 @@ function ModelDropdown({ models, value, onChange }: { models: { id: string; labe
61
61
  <button
62
62
  type="button"
63
63
  onClick={() => setOpen((o) => !o)}
64
- className="w-full flex items-center justify-between bg-white/[0.03] border border-white/[0.08] text-white rounded-xl px-4 py-2.5 text-[13px] outline-none hover:border-white/15 focus:border-primary/30 transition-colors"
64
+ className="w-full flex items-center justify-between bg-white/[0.03] border border-white/[0.08] text-white rounded-xl px-4 py-2.5 text-[13px] outline-none hover:border-white/15 focus:border-[#AF27E3]/30 transition-colors"
65
65
  >
66
66
  <span className={selected ? 'text-white' : 'text-white/20'}>
67
67
  {selected ? selected.label : 'Choose a model...'}
@@ -76,7 +76,7 @@ function ModelDropdown({ models, value, onChange }: { models: { id: string; labe
76
76
  onClick={() => { onChange(m.id); setOpen(false); }}
77
77
  className={`w-full text-left px-4 py-2 text-[13px] transition-colors ${
78
78
  value === m.id
79
- ? 'text-primary bg-primary/10'
79
+ ? 'text-[#AF27E3] bg-[#AF27E3]/10'
80
80
  : 'text-white/70 hover:bg-white/[0.04] hover:text-white'
81
81
  }`}
82
82
  >
@@ -495,9 +495,9 @@ export default function OnboardWizard({ onComplete }: Props) {
495
495
  /* ── Styles ── */
496
496
 
497
497
  const inputCls =
498
- 'w-full bg-white/[0.05] border border-white/[0.08] text-white rounded-xl px-4 py-3 text-base outline-none focus:border-primary/40 placeholder:text-white/20 transition-colors';
498
+ 'w-full bg-white/[0.05] border border-white/[0.08] text-white rounded-xl px-4 py-3 text-base outline-none input-glow placeholder:text-white/20 transition-all';
499
499
  const inputSmCls =
500
- 'w-full bg-white/[0.03] border border-white/[0.08] text-white rounded-xl px-4 py-2.5 text-[13px] outline-none focus:border-primary/30 placeholder:text-white/20 transition-colors';
500
+ 'w-full bg-white/[0.03] border border-white/[0.08] text-white rounded-xl px-4 py-2.5 text-[13px] outline-none input-glow placeholder:text-white/20 transition-all';
501
501
 
502
502
  return (
503
503
  <div className="fixed inset-0 z-[200] flex items-center justify-center p-4">
@@ -516,9 +516,9 @@ export default function OnboardWizard({ onComplete }: Props) {
516
516
  key={i}
517
517
  className={`h-1.5 rounded-full transition-all duration-300 ${
518
518
  i === step
519
- ? 'w-7 bg-primary'
519
+ ? 'w-7 bg-gradient-brand'
520
520
  : i < step
521
- ? 'w-1.5 bg-primary/60'
521
+ ? 'w-1.5 bg-gradient-brand opacity-60'
522
522
  : 'w-1.5 bg-white/10'
523
523
  }`}
524
524
  />
@@ -538,7 +538,7 @@ export default function OnboardWizard({ onComplete }: Props) {
538
538
  {/* ── Step 0: Welcome ── */}
539
539
  {step === 0 && (
540
540
  <div className="flex flex-col items-center text-center">
541
- <img src="/fluxy.png" alt="Fluxy" className="h-16 w-auto mb-4" />
541
+ <video src="/fluxy_say_hi.webm" autoPlay loop muted playsInline className="h-[180px] mb-4" />
542
542
  <h1 className="text-2xl font-bold text-white tracking-tight">
543
543
  Welcome to Fluxy
544
544
  </h1>
@@ -547,7 +547,7 @@ export default function OnboardWizard({ onComplete }: Props) {
547
547
  </p>
548
548
  <button
549
549
  onClick={next}
550
- className="mt-6 px-7 py-3 bg-primary hover:bg-primary/90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center gap-2"
550
+ className="mt-6 px-7 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center gap-2"
551
551
  >
552
552
  Get Started
553
553
  <ArrowRight className="h-4 w-4" />
@@ -577,7 +577,7 @@ export default function OnboardWizard({ onComplete }: Props) {
577
577
  <button
578
578
  onClick={next}
579
579
  disabled={!canNext}
580
- className="shrink-0 h-12 w-12 flex items-center justify-center rounded-full bg-primary hover:bg-primary/90 text-white transition-colors disabled:opacity-30"
580
+ className="shrink-0 h-12 w-12 flex items-center justify-center rounded-full bg-gradient-brand hover:opacity-90 text-white transition-colors disabled:opacity-30"
581
581
  >
582
582
  <ArrowRight className="h-5 w-5" />
583
583
  </button>
@@ -608,7 +608,7 @@ export default function OnboardWizard({ onComplete }: Props) {
608
608
  <div className="flex gap-2 mt-4">
609
609
  <button
610
610
  onClick={next}
611
- className="flex-1 py-3 bg-primary hover:bg-primary/90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2"
611
+ className="flex-1 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2"
612
612
  >
613
613
  Continue
614
614
  <ArrowRight className="h-4 w-4" />
@@ -659,7 +659,7 @@ export default function OnboardWizard({ onComplete }: Props) {
659
659
  {handleStatus && botName.length > 0 && !registered && (
660
660
  <div className="absolute right-4 top-1/2 -translate-y-1/2">
661
661
  {handleStatus === 'checking' && (
662
- <div className="w-5 h-5 border-2 border-white/10 border-t-primary rounded-full animate-spin" />
662
+ <div className="w-5 h-5 border-2 border-white/10 border-t-[#04D1FE] rounded-full animate-spin" />
663
663
  )}
664
664
  {handleStatus === 'invalid' && (
665
665
  <div className="w-6 h-6 rounded-full bg-amber-500/15 flex items-center justify-center">
@@ -693,8 +693,8 @@ export default function OnboardWizard({ onComplete }: Props) {
693
693
  ? 'border-white/[0.04] bg-transparent opacity-50 cursor-not-allowed'
694
694
  : selectedTier === h.tier
695
695
  ? h.highlight
696
- ? 'border-primary/40 bg-primary/[0.06]'
697
- : 'border-primary/30 bg-white/[0.04]'
696
+ ? 'border-[#AF27E3]/40 bg-[#AF27E3]/[0.06]'
697
+ : 'border-[#AF27E3]/30 bg-white/[0.04]'
698
698
  : 'border-white/[0.06] bg-transparent hover:border-white/10 hover:bg-white/[0.02]'
699
699
  }`}
700
700
  >
@@ -732,7 +732,7 @@ export default function OnboardWizard({ onComplete }: Props) {
732
732
  <button
733
733
  onClick={showChangeConfirm ? onChangeHandle : onClaimHandle}
734
734
  disabled={registering || changingHandle}
735
- className="w-full mt-4 py-3 bg-primary hover:bg-primary/90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
735
+ className="w-full mt-4 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
736
736
  >
737
737
  {(registering || changingHandle) ? (
738
738
  <><LoaderCircle className="h-4 w-4 animate-spin" />{showChangeConfirm ? 'Changing...' : 'Claiming...'}</>
@@ -746,7 +746,7 @@ export default function OnboardWizard({ onComplete }: Props) {
746
746
  {registered && (
747
747
  <button
748
748
  onClick={next}
749
- className="w-full mt-4 py-3 bg-primary hover:bg-primary/90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2"
749
+ className="w-full mt-4 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2"
750
750
  >
751
751
  Continue
752
752
  <ArrowRight className="h-4 w-4" />
@@ -930,7 +930,7 @@ export default function OnboardWizard({ onComplete }: Props) {
930
930
  <button
931
931
  onClick={next}
932
932
  disabled={!canNext}
933
- className="w-full mt-5 py-3 bg-primary hover:bg-primary/90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
933
+ className="w-full mt-5 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
934
934
  >
935
935
  Continue
936
936
  <ArrowRight className="h-4 w-4" />
@@ -956,7 +956,7 @@ export default function OnboardWizard({ onComplete }: Props) {
956
956
  onClick={() => handleProviderChange(p.id)}
957
957
  className={`flex-1 relative rounded-xl border transition-all duration-200 p-3 text-left ${
958
958
  provider === p.id
959
- ? 'bg-white/[0.04] border-primary/40'
959
+ ? 'bg-white/[0.04] border-[#AF27E3]/40'
960
960
  : 'bg-transparent border-white/[0.06] hover:border-white/10 hover:bg-white/[0.02]'
961
961
  }`}
962
962
  >
@@ -978,7 +978,7 @@ export default function OnboardWizard({ onComplete }: Props) {
978
978
  <Check className="h-2.5 w-2.5 text-emerald-400" />
979
979
  </div>
980
980
  ) : provider === p.id ? (
981
- <div className="absolute top-2 right-2 w-2 h-2 rounded-full bg-primary" />
981
+ <div className="absolute top-2 right-2 w-2 h-2 rounded-full bg-gradient-brand" />
982
982
  ) : null}
983
983
  </button>
984
984
  ))}
@@ -1018,7 +1018,7 @@ export default function OnboardWizard({ onComplete }: Props) {
1018
1018
 
1019
1019
  <button
1020
1020
  onClick={handleAnthropicAuth}
1021
- className="w-full py-2.5 px-4 bg-primary hover:bg-primary/90 text-white text-[13px] font-medium rounded-xl transition-colors flex items-center justify-center gap-2"
1021
+ className="w-full py-2.5 px-4 bg-gradient-brand hover:opacity-90 text-white text-[13px] font-medium rounded-xl transition-colors flex items-center justify-center gap-2"
1022
1022
  >
1023
1023
  {oauthStarted ? (
1024
1024
  <><ExternalLink className="h-3.5 w-3.5 opacity-60" />Open authentication page again</>
@@ -1047,7 +1047,7 @@ export default function OnboardWizard({ onComplete }: Props) {
1047
1047
  <button
1048
1048
  onClick={handleAnthropicConnect}
1049
1049
  disabled={!anthropicCode.trim() || isExchanging}
1050
- className="w-full py-2.5 px-4 bg-primary hover:bg-primary/90 text-white text-[13px] font-medium rounded-xl transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
1050
+ className="w-full py-2.5 px-4 bg-gradient-brand hover:opacity-90 text-white text-[13px] font-medium rounded-xl transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
1051
1051
  >
1052
1052
  {isExchanging ? (<><LoaderCircle className="h-3.5 w-3.5 animate-spin" />Verifying...</>) : 'Connect'}
1053
1053
  </button>
@@ -1164,7 +1164,7 @@ export default function OnboardWizard({ onComplete }: Props) {
1164
1164
  <button
1165
1165
  onClick={next}
1166
1166
  disabled={!canNext}
1167
- className="w-full mt-4 py-3 bg-primary hover:bg-primary/90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
1167
+ className="w-full mt-4 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
1168
1168
  >
1169
1169
  Continue
1170
1170
  <ArrowRight className="h-4 w-4" />
@@ -1192,7 +1192,7 @@ export default function OnboardWizard({ onComplete }: Props) {
1192
1192
  onClick={() => setWhisperEnabled((v) => !v)}
1193
1193
  className={`w-full mt-5 rounded-xl border transition-all duration-200 p-4 text-left ${
1194
1194
  whisperEnabled
1195
- ? 'bg-white/[0.04] border-primary/40'
1195
+ ? 'bg-white/[0.04] border-[#AF27E3]/40'
1196
1196
  : 'bg-transparent border-white/[0.06] hover:border-white/10 hover:bg-white/[0.02]'
1197
1197
  }`}
1198
1198
  >
@@ -1205,7 +1205,7 @@ export default function OnboardWizard({ onComplete }: Props) {
1205
1205
  </div>
1206
1206
  </div>
1207
1207
  <div className={`w-10 h-[22px] rounded-full transition-colors duration-200 flex items-center px-0.5 shrink-0 ${
1208
- whisperEnabled ? 'bg-primary' : 'bg-white/[0.08]'
1208
+ whisperEnabled ? 'bg-gradient-brand' : 'bg-white/[0.08]'
1209
1209
  }`}>
1210
1210
  <div className={`w-[18px] h-[18px] rounded-full bg-white shadow-sm transition-transform duration-200 ${
1211
1211
  whisperEnabled ? 'translate-x-[18px]' : 'translate-x-0'
@@ -1232,7 +1232,7 @@ export default function OnboardWizard({ onComplete }: Props) {
1232
1232
  <p className="text-amber-400/70 text-[11px] mt-1">Key looks too short</p>
1233
1233
  )}
1234
1234
  <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">
1235
- <Mic className="h-4 w-4 text-primary/60 mt-0.5 shrink-0" />
1235
+ <Mic className="h-4 w-4 text-[#AF27E3]/60 mt-0.5 shrink-0" />
1236
1236
  <p className="text-white/35 text-[12px] leading-relaxed">
1237
1237
  Users will see a microphone button in the chat. Audio is sent to OpenAI's Whisper API for transcription, then processed as a regular text message.
1238
1238
  </p>
@@ -1243,7 +1243,7 @@ export default function OnboardWizard({ onComplete }: Props) {
1243
1243
  <button
1244
1244
  onClick={handleComplete}
1245
1245
  disabled={saving || (whisperEnabled && (!whisperKey.startsWith('sk-') || whisperKey.length < 20))}
1246
- className="w-full mt-5 py-3 bg-primary hover:bg-primary/90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
1246
+ className="w-full mt-5 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
1247
1247
  >
1248
1248
  {saving ? (
1249
1249
  <><LoaderCircle className="h-4 w-4 animate-spin" />Setting up...</>
@@ -8,14 +8,12 @@ export function useWebSocket() {
8
8
  useEffect(() => {
9
9
  const client = new WsClient();
10
10
  clientRef.current = client;
11
- client.connect();
12
11
 
13
- const checkConnection = setInterval(() => {
14
- setConnected(client.connected);
15
- }, 1000);
12
+ const unsub = client.onStatus(setConnected);
13
+ client.connect();
16
14
 
17
15
  return () => {
18
- clearInterval(checkConnection);
16
+ unsub();
19
17
  client.disconnect();
20
18
  };
21
19
  }, []);
@@ -1,10 +1,22 @@
1
1
  type MessageHandler = (msg: any) => void;
2
+ type StatusHandler = (connected: boolean) => void;
3
+
4
+ interface QueuedMessage {
5
+ type: string;
6
+ data: any;
7
+ }
2
8
 
3
9
  export class WsClient {
4
10
  private ws: WebSocket | null = null;
5
11
  private handlers = new Map<string, Set<MessageHandler>>();
12
+ private statusHandlers = new Set<StatusHandler>();
6
13
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
14
+ private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
7
15
  private url: string;
16
+ private queue: QueuedMessage[] = [];
17
+ private intentionalClose = false;
18
+ private reconnectDelay = 1000;
19
+ private static MAX_RECONNECT_DELAY = 8000;
8
20
 
9
21
  constructor(url?: string) {
10
22
  const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
@@ -13,23 +25,42 @@ export class WsClient {
13
25
  }
14
26
 
15
27
  connect(): void {
28
+ this.intentionalClose = false;
16
29
  this.ws = new WebSocket(this.url);
17
30
 
31
+ this.ws.onopen = () => {
32
+ this.reconnectDelay = 1000;
33
+ this.notifyStatus(true);
34
+ this.flushQueue();
35
+ this.startHeartbeat();
36
+ };
37
+
18
38
  this.ws.onmessage = (e) => {
39
+ // Ignore pong frames
40
+ if (e.data === 'pong') return;
19
41
  const msg = JSON.parse(e.data);
20
42
  const handlers = this.handlers.get(msg.type);
21
43
  handlers?.forEach((h) => h(msg.data));
22
44
  };
23
45
 
24
46
  this.ws.onclose = () => {
25
- this.reconnectTimer = setTimeout(() => this.connect(), 2000);
47
+ this.stopHeartbeat();
48
+ this.notifyStatus(false);
49
+ if (!this.intentionalClose) {
50
+ this.reconnectTimer = setTimeout(() => {
51
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, WsClient.MAX_RECONNECT_DELAY);
52
+ this.connect();
53
+ }, this.reconnectDelay);
54
+ }
26
55
  };
27
56
 
28
57
  this.ws.onerror = () => this.ws?.close();
29
58
  }
30
59
 
31
60
  disconnect(): void {
32
- if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
61
+ this.intentionalClose = true;
62
+ if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; }
63
+ this.stopHeartbeat();
33
64
  this.ws?.close();
34
65
  this.ws = null;
35
66
  }
@@ -40,13 +71,46 @@ export class WsClient {
40
71
  return () => this.handlers.get(type)?.delete(handler);
41
72
  }
42
73
 
74
+ onStatus(handler: StatusHandler): () => void {
75
+ this.statusHandlers.add(handler);
76
+ return () => this.statusHandlers.delete(handler);
77
+ }
78
+
43
79
  send(type: string, data: any): void {
80
+ const message = { type, data };
44
81
  if (this.ws?.readyState === WebSocket.OPEN) {
45
- this.ws.send(JSON.stringify({ type, data }));
82
+ this.ws.send(JSON.stringify(message));
83
+ } else {
84
+ // Queue for delivery on reconnect
85
+ this.queue.push(message);
46
86
  }
47
87
  }
48
88
 
49
89
  get connected(): boolean {
50
90
  return this.ws?.readyState === WebSocket.OPEN;
51
91
  }
92
+
93
+ private flushQueue(): void {
94
+ while (this.queue.length > 0 && this.ws?.readyState === WebSocket.OPEN) {
95
+ const msg = this.queue.shift()!;
96
+ this.ws.send(JSON.stringify(msg));
97
+ }
98
+ }
99
+
100
+ private notifyStatus(connected: boolean): void {
101
+ this.statusHandlers.forEach((h) => h(connected));
102
+ }
103
+
104
+ private startHeartbeat(): void {
105
+ this.stopHeartbeat();
106
+ this.heartbeatTimer = setInterval(() => {
107
+ if (this.ws?.readyState === WebSocket.OPEN) {
108
+ this.ws.send('ping');
109
+ }
110
+ }, 25_000);
111
+ }
112
+
113
+ private stopHeartbeat(): void {
114
+ if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }
115
+ }
52
116
  }
@@ -46,10 +46,65 @@ body {
46
46
  }
47
47
 
48
48
  ::selection {
49
- background-color: rgba(60, 143, 255, 0.25);
49
+ background-color: rgba(175, 39, 227, 0.25);
50
50
  }
51
51
 
52
52
  ::-webkit-scrollbar { width: 6px; }
53
53
  ::-webkit-scrollbar-track { background: transparent; }
54
54
  ::-webkit-scrollbar-thumb { background: #3a3a3a; border-radius: 3px; }
55
55
  ::-webkit-scrollbar-thumb:hover { background: #4a4a4a; }
56
+
57
+ .text-gradient {
58
+ background-clip: text;
59
+ -webkit-background-clip: text;
60
+ color: transparent;
61
+ -webkit-text-fill-color: transparent;
62
+ background-image: linear-gradient(135deg, #04D1FE, #AF27E3, #FB4072);
63
+ }
64
+
65
+ .bg-gradient-brand {
66
+ background-image: linear-gradient(135deg, #04D1FE, #AF27E3, #FB4072);
67
+ }
68
+
69
+ .glow-border {
70
+ box-shadow: 0 0 0 1px rgba(175, 39, 227, 0.1),
71
+ 0 0 20px -5px rgba(175, 39, 227, 0.15);
72
+ }
73
+
74
+ .animated-border {
75
+ position: relative;
76
+ overflow: hidden;
77
+ }
78
+ .animated-border::before {
79
+ content: '';
80
+ position: absolute;
81
+ inset: -150%;
82
+ background: conic-gradient(
83
+ from 0deg,
84
+ #04D1FE,
85
+ #AF27E3,
86
+ #FB4072,
87
+ #04D1FE
88
+ );
89
+ animation: border-spin 3s linear infinite;
90
+ }
91
+ .animated-border > * {
92
+ position: relative;
93
+ z-index: 1;
94
+ }
95
+
96
+ .animated-border-slow::before {
97
+ animation-duration: 5s;
98
+ }
99
+
100
+ .input-glow:focus {
101
+ border-color: rgba(175, 39, 227, 0.4);
102
+ box-shadow: 0 0 0 1px rgba(175, 39, 227, 0.15),
103
+ 0 0 20px -5px rgba(175, 39, 227, 0.25),
104
+ 0 0 4px -1px rgba(4, 209, 254, 0.1);
105
+ }
106
+
107
+ @keyframes border-spin {
108
+ 0% { transform: rotate(0deg); }
109
+ 100% { transform: rotate(360deg); }
110
+ }