fluxy-bot 0.12.6 → 0.13.0
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/package.json +7 -5
- package/vite.config.ts +1 -0
- package/worker/prompts/fluxy-system-prompt.txt +6 -1
- package/workspace/.backend.log +1 -0
- package/workspace/client/index.html +7 -5
- package/workspace/client/src/App.tsx +12 -3
- package/workspace/client/src/components/Dashboard/DashboardPage.tsx +120 -113
- package/workspace/client/src/components/Layout/DashboardLayout.tsx +17 -14
- package/workspace/client/src/components/Layout/MobileNav.tsx +2 -2
- package/workspace/client/src/components/Layout/Sidebar.tsx +48 -15
- package/workspace/client/src/components/deleteme_onboarding/WorkspaceTour.tsx +103 -0
- package/workspace/client/src/components/deleteme_onboarding/tour-theme.css +75 -0
- package/workspace/client/src/components/ui/sheet.tsx +3 -2
- package/workspace/client/src/styles/globals.css +4 -1
- package/workspace/node_modules/.vite/deps/_metadata.json +116 -0
- package/workspace/node_modules/.vite/deps/clsx.js +18 -0
- package/workspace/node_modules/.vite/deps/clsx.js.map +1 -0
- package/workspace/node_modules/.vite/deps/driver__js.js +581 -0
- package/workspace/node_modules/.vite/deps/driver__js.js.map +1 -0
- package/workspace/node_modules/.vite/deps/framer-motion.js +13761 -0
- package/workspace/node_modules/.vite/deps/framer-motion.js.map +1 -0
- package/workspace/node_modules/.vite/deps/lucide-react.js +34357 -0
- package/workspace/node_modules/.vite/deps/lucide-react.js.map +1 -0
- package/workspace/node_modules/.vite/deps/package.json +3 -0
- package/workspace/node_modules/.vite/deps/radix-ui.js +13835 -0
- package/workspace/node_modules/.vite/deps/radix-ui.js.map +1 -0
- package/workspace/node_modules/.vite/deps/react-3_O8oni9.js +799 -0
- package/workspace/node_modules/.vite/deps/react-3_O8oni9.js.map +1 -0
- package/workspace/node_modules/.vite/deps/react-dom.js +185 -0
- package/workspace/node_modules/.vite/deps/react-dom.js.map +1 -0
- package/workspace/node_modules/.vite/deps/react-dom_client.js +14384 -0
- package/workspace/node_modules/.vite/deps/react-dom_client.js.map +1 -0
- package/workspace/node_modules/.vite/deps/react-router.js +9785 -0
- package/workspace/node_modules/.vite/deps/react-router.js.map +1 -0
- package/workspace/node_modules/.vite/deps/react.js +2 -0
- package/workspace/node_modules/.vite/deps/react_jsx-dev-runtime.js +204 -0
- package/workspace/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +1 -0
- package/workspace/node_modules/.vite/deps/react_jsx-runtime.js +209 -0
- package/workspace/node_modules/.vite/deps/react_jsx-runtime.js.map +1 -0
- package/workspace/node_modules/.vite/deps/recharts.js +34142 -0
- package/workspace/node_modules/.vite/deps/recharts.js.map +1 -0
- package/workspace/node_modules/.vite/deps/sonner.js +943 -0
- package/workspace/node_modules/.vite/deps/sonner.js.map +1 -0
- package/workspace/node_modules/.vite/deps/tailwind-merge.js +2025 -0
- package/workspace/node_modules/.vite/deps/tailwind-merge.js.map +1 -0
- package/workspace/node_modules/.vite/deps/use-sync-external-store.js +27 -0
- package/workspace/node_modules/.vite/deps/use-sync-external-store.js.map +1 -0
- package/workspace/node_modules/.vite/deps/use-sync-external-store_shim.js +75 -0
- package/workspace/node_modules/.vite/deps/use-sync-external-store_shim.js.map +1 -0
- package/workspace/node_modules/.vite/deps/zustand.js +49 -0
- package/workspace/node_modules/.vite/deps/zustand.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fluxy-bot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"releaseNotes": [
|
|
5
|
-
"
|
|
6
|
-
"2. ",
|
|
7
|
-
"3. ",
|
|
8
|
-
"4. "
|
|
5
|
+
"1. react router implemented",
|
|
6
|
+
"2. new workspace design",
|
|
7
|
+
"3. tour",
|
|
8
|
+
"4. worspace helth checker"
|
|
9
9
|
],
|
|
10
10
|
"description": "Self-hosted, self-evolving AI agent with its own dashboard.",
|
|
11
11
|
"type": "module",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"build:fluxy": "vite build --config vite.fluxy.config.ts",
|
|
48
48
|
"start": "node --import tsx/esm supervisor/index.ts",
|
|
49
49
|
"postinstall": "node scripts/postinstall.js",
|
|
50
|
+
"dev:workspace": "vite",
|
|
50
51
|
"dev:docs": "cd ./docs && npx fumapress"
|
|
51
52
|
},
|
|
52
53
|
"dependencies": {
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
"commander": "^14.0.3",
|
|
61
62
|
"cron-parser": "^5.5.0",
|
|
62
63
|
"date-fns": "^4.1.0",
|
|
64
|
+
"driver.js": "^1.4.0",
|
|
63
65
|
"express": "^5.2.1",
|
|
64
66
|
"framer-motion": "^12.34.3",
|
|
65
67
|
"lucide-react": "^1.7.0",
|
package/vite.config.ts
CHANGED
|
@@ -449,7 +449,12 @@ The workspace is **NOT secured by default**. If your human's Fluxy is accessible
|
|
|
449
449
|
They already have a chat password from onboarding. Don't confuse the two. Don't go looking in the worker database for `portal_pass`. Don't tell them "you already have a password set." Set the workspace password using the route above.
|
|
450
450
|
|
|
451
451
|
## Modular Philosophy
|
|
452
|
-
|
|
452
|
+
|
|
453
|
+
When your human asks for something new, don't redesign the workspace. Build a **mini app**: a new page component, a new React Router route, and a sidebar link. Each feature lives on its own page. Yesterday it was a CRM, today a finance tracker, tomorrow a diet log. They all coexist in the sidebar.
|
|
454
|
+
|
|
455
|
+
On top of the page, add a **small widget on the Dashboard** when it makes sense. The dashboard is a grid of compact cards (see the existing Stripe, Gmail, X, and Research widgets for the pattern). A widget shows a quick summary or key metric from the app. Not every app needs one, but most benefit from a glanceable overview.
|
|
456
|
+
|
|
457
|
+
Only redesign the workspace layout if your human explicitly asks you to. Otherwise, keep adding modules. The sidebar grows, the dashboard fills with widgets, and the workspace becomes more useful over time.
|
|
453
458
|
|
|
454
459
|
---
|
|
455
460
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[backend] Listening on port 3004
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="en" style="background:#
|
|
2
|
+
<html lang="en" style="background-color:#0A0A0A">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content" />
|
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
<meta name="apple-mobile-web-app-title" content="Fluxy" />
|
|
10
10
|
<link rel="apple-touch-icon" href="/fluxy-icon-192.png" />
|
|
11
11
|
<link rel="manifest" href="/manifest.json" />
|
|
12
|
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
|
|
12
13
|
<title>Fluxy</title>
|
|
13
14
|
</head>
|
|
14
|
-
<body class="bg-background text-foreground" style="background:#
|
|
15
|
+
<body class="bg-background text-foreground" style="background-color:#0A0A0A">
|
|
15
16
|
<!-- Dark background fallback — visible instantly while widget.js loads.
|
|
16
17
|
The canvas animation (in widget.js) takes over as the actual splash. -->
|
|
17
|
-
<div id="splash" style="background:#
|
|
18
|
+
<div id="splash" style="background:#0A0A0A;position:fixed;inset:0;z-index:9998;transition:opacity .25s ease-out"></div>
|
|
19
|
+
<script>if(location.port==='5173'){var s=document.getElementById('splash');if(s)s.style.display='none'}</script>
|
|
18
20
|
|
|
19
21
|
<div id="root"></div>
|
|
20
22
|
|
|
@@ -26,7 +28,7 @@
|
|
|
26
28
|
var root = document.getElementById('root');
|
|
27
29
|
if (root && root.children.length === 0) {
|
|
28
30
|
root.innerHTML =
|
|
29
|
-
'<div style="background:#
|
|
31
|
+
'<div style="background:#0A0A0A;color:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100dvh;width:100vw;position:fixed;inset:0;z-index:50;font-family:system-ui,-apple-system,sans-serif;text-align:center;padding:24px">' +
|
|
30
32
|
'<img src="/fluxy_frame1.png" style="height:120px;width:120px;border-radius:50%;object-fit:cover;margin-bottom:32px" alt="Fluxy" />' +
|
|
31
33
|
'<h1 style="font-size:20px;font-weight:600;margin-bottom:8px">Your app crashed</h1>' +
|
|
32
34
|
'<p style="font-size:14px;color:rgba(255,255,255,0.5);max-width:320px;line-height:1.5">Ask the agent to fix it using the chat.</p>' +
|
|
@@ -43,7 +45,7 @@
|
|
|
43
45
|
</script>
|
|
44
46
|
<script type="module" src="/src/main.tsx"></script>
|
|
45
47
|
<script>
|
|
46
|
-
if('serviceWorker' in navigator){
|
|
48
|
+
if('serviceWorker' in navigator && location.port !== '5173'){
|
|
47
49
|
var swRefreshing=false;
|
|
48
50
|
navigator.serviceWorker.addEventListener('controllerchange',function(){
|
|
49
51
|
if(swRefreshing)return;
|
|
@@ -3,6 +3,7 @@ import { Routes, Route } from 'react-router';
|
|
|
3
3
|
import ErrorBoundary from './components/ErrorBoundary';
|
|
4
4
|
import DashboardLayout from './components/Layout/DashboardLayout';
|
|
5
5
|
import DashboardPage from './components/Dashboard/DashboardPage';
|
|
6
|
+
import WorkspaceTour from './components/deleteme_onboarding/WorkspaceTour';
|
|
6
7
|
|
|
7
8
|
function DashboardError() {
|
|
8
9
|
return (
|
|
@@ -25,6 +26,7 @@ function DashboardError() {
|
|
|
25
26
|
|
|
26
27
|
export default function App() {
|
|
27
28
|
const [showOnboard, setShowOnboard] = useState(false);
|
|
29
|
+
const [userName, setUserName] = useState('');
|
|
28
30
|
const [rebuildState, setRebuildState] = useState<'idle' | 'rebuilding' | 'error'>('idle');
|
|
29
31
|
const [buildError, setBuildError] = useState('');
|
|
30
32
|
|
|
@@ -102,10 +104,15 @@ export default function App() {
|
|
|
102
104
|
}, []);
|
|
103
105
|
|
|
104
106
|
useEffect(() => {
|
|
105
|
-
fetch('/api/settings')
|
|
106
|
-
.then((r) =>
|
|
107
|
+
fetch('/api/settings', { signal: AbortSignal.timeout(3000) })
|
|
108
|
+
.then((r) => {
|
|
109
|
+
if (!r.ok) return;
|
|
110
|
+
return r.json();
|
|
111
|
+
})
|
|
107
112
|
.then((s) => {
|
|
113
|
+
if (!s) return;
|
|
108
114
|
if (s.onboard_complete !== 'true') setShowOnboard(true);
|
|
115
|
+
if (s.user_name) setUserName(s.user_name);
|
|
109
116
|
})
|
|
110
117
|
.catch(() => {});
|
|
111
118
|
}, []);
|
|
@@ -148,11 +155,13 @@ export default function App() {
|
|
|
148
155
|
return (
|
|
149
156
|
<>
|
|
150
157
|
<ErrorBoundary fallback={<DashboardError />}>
|
|
151
|
-
<DashboardLayout>
|
|
158
|
+
<DashboardLayout userName={userName}>
|
|
152
159
|
<Routes>
|
|
153
160
|
<Route path="/" element={<DashboardPage />} />
|
|
161
|
+
<Route path="*" element={<DashboardPage />} />
|
|
154
162
|
</Routes>
|
|
155
163
|
</DashboardLayout>
|
|
164
|
+
<WorkspaceTour />
|
|
156
165
|
</ErrorBoundary>
|
|
157
166
|
|
|
158
167
|
{showOnboard && (
|
|
@@ -1,127 +1,134 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Search, Mail, DollarSign, TrendingUp } from 'lucide-react';
|
|
2
|
+
import { AreaChart, Area, BarChart, Bar, ResponsiveContainer } from 'recharts';
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
'Build a task manager',
|
|
8
|
-
];
|
|
4
|
+
const GRADIENT = 'linear-gradient(to right, #4FF2FE 10%, #BC20DE 55%, #FF6B8A 100%)';
|
|
5
|
+
const CARD = 'relative rounded-xl overflow-hidden';
|
|
6
|
+
const BORDER = 'absolute inset-0 rounded-xl bg-gradient-to-b from-white/[0.08] via-white/[0.02] to-transparent pointer-events-none';
|
|
7
|
+
const INNER = 'relative rounded-xl bg-[#141414] m-px p-3.5 h-full';
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
);
|
|
23
|
-
if (input) {
|
|
24
|
-
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
25
|
-
window.HTMLTextAreaElement.prototype, 'value'
|
|
26
|
-
)?.set || Object.getOwnPropertyDescriptor(
|
|
27
|
-
window.HTMLInputElement.prototype, 'value'
|
|
28
|
-
)?.set;
|
|
29
|
-
nativeSetter?.call(input, text);
|
|
30
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
31
|
-
input.focus();
|
|
32
|
-
}
|
|
33
|
-
}, 400);
|
|
34
|
-
};
|
|
9
|
+
const rev = [{ v: 82 }, { v: 89 }, { v: 94 }, { v: 101 }, { v: 108 }, { v: 112 }, { v: 125 }];
|
|
10
|
+
const fol = [{ v: 12 }, { v: 18 }, { v: 9 }, { v: 24 }, { v: 31 }, { v: 19 }, { v: 27 }];
|
|
11
|
+
|
|
12
|
+
function StripeSvg() {
|
|
13
|
+
return <svg className="h-3.5 w-3.5 text-[#635BFF]" viewBox="0 0 24 24" fill="currentColor"><path d="M13.976 9.15c-2.172-.806-3.356-1.426-3.356-2.409 0-.831.683-1.305 1.901-1.305 2.227 0 4.515.858 6.09 1.631l.89-5.494C18.252.975 15.697 0 12.165 0 9.667 0 7.589.654 6.104 1.872 4.56 3.147 3.757 4.992 3.757 7.218c0 4.039 2.467 5.76 6.476 7.219 2.585.92 3.445 1.574 3.445 2.583 0 .98-.84 1.545-2.354 1.545-1.875 0-4.965-.921-6.99-2.109l-.9 5.555C5.175 22.99 8.385 24 11.714 24c2.641 0 4.843-.624 6.328-1.813 1.664-1.305 2.525-3.236 2.525-5.732 0-4.128-2.524-5.851-6.591-7.305z" /></svg>;
|
|
14
|
+
}
|
|
15
|
+
function GmailSvg() {
|
|
16
|
+
return <svg className="h-3.5 w-3.5 text-[#EA4335]" viewBox="0 0 24 24" fill="currentColor"><path d="M24 5.457v13.909c0 .904-.732 1.636-1.636 1.636h-3.819V11.73L12 16.64l-6.545-4.91v9.273H1.636A1.636 1.636 0 0 1 0 19.366V5.457c0-2.023 2.309-3.178 3.927-1.964L5.455 4.64 12 9.548l6.545-4.91 1.528-1.145C21.69 2.28 24 3.434 24 5.457z" /></svg>;
|
|
17
|
+
}
|
|
18
|
+
function XSvg() {
|
|
19
|
+
return <svg className="h-3.5 w-3.5 text-white" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" /></svg>;
|
|
20
|
+
}
|
|
35
21
|
|
|
22
|
+
export default function DashboardPage() {
|
|
36
23
|
return (
|
|
37
|
-
<div className="flex flex-col
|
|
38
|
-
|
|
39
|
-
|
|
24
|
+
<div className="flex flex-col h-full px-4 sm:px-6 pt-8 sm:pt-12 pb-6 max-w-4xl mx-auto w-full overflow-y-auto">
|
|
25
|
+
<h1
|
|
26
|
+
className="text-xl sm:text-2xl font-bold mb-1 tracking-tight w-fit"
|
|
27
|
+
style={{ backgroundImage: GRADIENT, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}
|
|
28
|
+
>
|
|
40
29
|
Let's get started
|
|
41
30
|
</h1>
|
|
42
|
-
<p className="text-muted-foreground text-
|
|
43
|
-
Tell me what to build and I'll create it for you, right here.
|
|
44
|
-
</p>
|
|
31
|
+
<p className="text-muted-foreground text-xs mb-6">Your workspace at a glance.</p>
|
|
45
32
|
|
|
46
|
-
|
|
47
|
-
<div className="flex flex-wrap items-center justify-center gap-2 sm:gap-3 max-w-lg mb-12">
|
|
48
|
-
{starterSuggestions.map((suggestion) => (
|
|
49
|
-
<button
|
|
50
|
-
key={suggestion}
|
|
51
|
-
onClick={() => handleSuggestion(suggestion)}
|
|
52
|
-
className="px-4 py-2 sm:px-5 sm:py-2.5 rounded-full border border-border bg-card text-sm text-muted-foreground hover:text-foreground hover:border-primary/40 hover:bg-card/80 transition-all duration-200 active:scale-[0.97]"
|
|
53
|
-
>
|
|
54
|
-
{suggestion}
|
|
55
|
-
</button>
|
|
56
|
-
))}
|
|
57
|
-
</div>
|
|
33
|
+
<div className="grid grid-cols-3 gap-2.5">
|
|
58
34
|
|
|
59
|
-
|
|
60
|
-
|
|
35
|
+
{/* Stripe — 2 cols */}
|
|
36
|
+
<div className={`${CARD} col-span-2`}>
|
|
37
|
+
<div className={BORDER} />
|
|
38
|
+
<div className={INNER}>
|
|
39
|
+
<div className="flex items-center justify-between">
|
|
40
|
+
<div className="flex items-center gap-2">
|
|
41
|
+
<div className="h-7 w-7 rounded-lg bg-[#635BFF]/10 flex items-center justify-center"><StripeSvg /></div>
|
|
42
|
+
<span className="text-xs font-bold">Stripe</span>
|
|
43
|
+
</div>
|
|
44
|
+
<div className="flex items-center gap-1 text-emerald-500">
|
|
45
|
+
<TrendingUp className="h-3 w-3" />
|
|
46
|
+
<span className="text-[10px] font-bold">+12.5%</span>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<p className="text-2xl font-bold tracking-tight mt-2">$12,480</p>
|
|
50
|
+
<p className="text-[10px] text-muted-foreground/50 mb-1">MRR</p>
|
|
51
|
+
<div className="h-12 overflow-hidden">
|
|
52
|
+
<ResponsiveContainer width="100%" height={48}>
|
|
53
|
+
<AreaChart data={rev}>
|
|
54
|
+
<defs>
|
|
55
|
+
<linearGradient id="sg" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stopColor="#4FF2FE" /><stop offset="50%" stopColor="#BC20DE" /><stop offset="100%" stopColor="#FE546B" /></linearGradient>
|
|
56
|
+
<linearGradient id="sf" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor="#BC20DE" stopOpacity={0.12} /><stop offset="100%" stopColor="#BC20DE" stopOpacity={0} /></linearGradient>
|
|
57
|
+
</defs>
|
|
58
|
+
<Area type="monotone" dataKey="v" stroke="url(#sg)" strokeWidth={1.5} fill="url(#sf)" />
|
|
59
|
+
</AreaChart>
|
|
60
|
+
</ResponsiveContainer>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
65
|
+
{/* X — 1 col */}
|
|
66
|
+
<div className={`${CARD} col-span-1`}>
|
|
67
|
+
<div className={BORDER} />
|
|
68
|
+
<div className={INNER}>
|
|
69
|
+
<div className="flex items-center gap-2 mb-2">
|
|
70
|
+
<div className="h-7 w-7 rounded-lg bg-white/[0.06] flex items-center justify-center"><XSvg /></div>
|
|
71
|
+
<span className="text-xs font-bold">X</span>
|
|
72
|
+
</div>
|
|
73
|
+
<p className="text-2xl font-bold tracking-tight">24.8K</p>
|
|
74
|
+
<div className="flex items-center gap-1 text-emerald-500 mb-1">
|
|
75
|
+
<TrendingUp className="h-2.5 w-2.5" />
|
|
76
|
+
<span className="text-[10px] font-bold">+1.4K</span>
|
|
77
|
+
</div>
|
|
78
|
+
<div className="h-10 overflow-hidden">
|
|
79
|
+
<ResponsiveContainer width="100%" height={40}>
|
|
80
|
+
<BarChart data={fol} barCategoryGap="25%">
|
|
81
|
+
<defs>
|
|
82
|
+
<linearGradient id="xg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor="#BC20DE" stopOpacity={0.3} /><stop offset="100%" stopColor="#4FF2FE" stopOpacity={0.05} /></linearGradient>
|
|
83
|
+
</defs>
|
|
84
|
+
<Bar dataKey="v" fill="url(#xg)" radius={[2, 2, 0, 0]} />
|
|
85
|
+
</BarChart>
|
|
86
|
+
</ResponsiveContainer>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
82
90
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
91
|
+
{/* Gmail — 1 col */}
|
|
92
|
+
<div className={`${CARD} col-span-1`}>
|
|
93
|
+
<div className={BORDER} />
|
|
94
|
+
<div className={INNER}>
|
|
95
|
+
<div className="flex items-center gap-2 mb-2.5">
|
|
96
|
+
<div className="h-7 w-7 rounded-lg bg-[#EA4335]/10 flex items-center justify-center"><GmailSvg /></div>
|
|
97
|
+
<span className="text-xs font-bold">Gmail</span>
|
|
98
|
+
<span className="ml-auto text-[10px] text-muted-foreground/50">3 new</span>
|
|
99
|
+
</div>
|
|
100
|
+
{['Sarah Chen', 'Stripe', 'Alex R.'].map((n) => (
|
|
101
|
+
<div key={n} className="flex items-center gap-2 py-1.5">
|
|
102
|
+
<div className="h-5 w-5 rounded-full bg-white/[0.06] text-[9px] font-bold flex items-center justify-center shrink-0">{n[0]}</div>
|
|
103
|
+
<span className="text-[11px] truncate">{n}</span>
|
|
104
|
+
</div>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
86
108
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
{/* Research — 2 cols */}
|
|
110
|
+
<div className={`${CARD} col-span-2`}>
|
|
111
|
+
<div className={BORDER} />
|
|
112
|
+
<div className={INNER}>
|
|
113
|
+
<div className="flex items-center gap-2 mb-2.5">
|
|
114
|
+
<div className="h-7 w-7 rounded-lg bg-[#9235F9]/10 flex items-center justify-center"><Search className="h-3.5 w-3.5 text-[#9235F9]" /></div>
|
|
115
|
+
<span className="text-xs font-bold">Research</span>
|
|
116
|
+
<span className="ml-auto text-[10px] font-bold text-muted-foreground bg-white/[0.06] px-2 py-0.5 rounded-full">3</span>
|
|
117
|
+
</div>
|
|
118
|
+
{[
|
|
119
|
+
{ t: 'Competitor pricing', s: 'Done', c: 'text-emerald-500 bg-emerald-500/10' },
|
|
120
|
+
{ t: 'Market trends Q1', s: 'Done', c: 'text-emerald-500 bg-emerald-500/10' },
|
|
121
|
+
{ t: 'User feedback', s: 'Review', c: 'text-orange-400 bg-orange-400/10' },
|
|
122
|
+
].map((r) => (
|
|
123
|
+
<div key={r.t} className="flex items-center justify-between py-1.5">
|
|
124
|
+
<span className="text-[11px]">{r.t}</span>
|
|
125
|
+
<span className={`text-[9px] font-bold px-1.5 py-0.5 rounded-full ${r.c}`}>{r.s}</span>
|
|
126
|
+
</div>
|
|
127
|
+
))}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
106
130
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
onClick={runTest}
|
|
110
|
-
disabled={status === 'loading'}
|
|
111
|
-
className="flex items-center gap-2.5 px-5 py-2.5 rounded-full border border-border bg-card text-sm text-muted-foreground hover:text-foreground hover:border-primary/40 hover:bg-card/80 transition-all duration-200 active:scale-[0.97] disabled:opacity-50"
|
|
112
|
-
>
|
|
113
|
-
<span
|
|
114
|
-
className={`h-2 w-2 rounded-full shrink-0 ${
|
|
115
|
-
status === 'ok' ? 'bg-green-500' :
|
|
116
|
-
status === 'error' ? 'bg-red-500' :
|
|
117
|
-
status === 'loading' ? 'bg-yellow-500 animate-pulse' :
|
|
118
|
-
'bg-muted-foreground/30'
|
|
119
|
-
}`}
|
|
120
|
-
/>
|
|
121
|
-
{status === 'idle' && 'Test Backend'}
|
|
122
|
-
{status === 'loading' && 'Testing...'}
|
|
123
|
-
{status === 'ok' && detail}
|
|
124
|
-
{status === 'error' && detail}
|
|
125
|
-
</button>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
126
133
|
);
|
|
127
134
|
}
|
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
import Sidebar from './Sidebar';
|
|
4
|
-
import Footer from './Footer';
|
|
5
4
|
import MobileNav from './MobileNav';
|
|
6
5
|
|
|
7
6
|
interface Props {
|
|
8
7
|
children: ReactNode;
|
|
8
|
+
userName?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export default function DashboardLayout({ children }: Props) {
|
|
11
|
+
export default function DashboardLayout({ children, userName }: Props) {
|
|
12
12
|
const [status, setStatus] = useState<'healthy' | 'restarting'>('healthy');
|
|
13
13
|
|
|
14
14
|
useEffect(() => {
|
|
15
15
|
const check = () => {
|
|
16
|
-
fetch('/app/api/health')
|
|
17
|
-
.then((r) =>
|
|
18
|
-
|
|
19
|
-
})
|
|
20
|
-
.catch(() => setStatus('restarting'));
|
|
16
|
+
fetch('/app/api/health', { signal: AbortSignal.timeout(3000) })
|
|
17
|
+
.then((r) => setStatus(r.ok ? 'healthy' : 'restarting'))
|
|
18
|
+
.catch(() => {});
|
|
21
19
|
};
|
|
22
20
|
check();
|
|
23
21
|
const id = setInterval(check, 10_000);
|
|
@@ -25,10 +23,10 @@ export default function DashboardLayout({ children }: Props) {
|
|
|
25
23
|
}, []);
|
|
26
24
|
|
|
27
25
|
return (
|
|
28
|
-
<div className="flex h-dvh flex-col
|
|
26
|
+
<div className="flex h-dvh flex-col">
|
|
29
27
|
{/* Mobile header */}
|
|
30
28
|
<header className="flex items-center justify-between px-4 py-3 md:hidden">
|
|
31
|
-
<MobileNav />
|
|
29
|
+
<MobileNav userName={userName} backendStatus={status} />
|
|
32
30
|
<div className="flex items-center gap-2">
|
|
33
31
|
<img src="/fluxy.png" alt="Fluxy" className="h-6 w-auto" />
|
|
34
32
|
<span className="font-semibold text-base">Fluxy</span>
|
|
@@ -37,15 +35,20 @@ export default function DashboardLayout({ children }: Props) {
|
|
|
37
35
|
</header>
|
|
38
36
|
|
|
39
37
|
<div className="flex flex-1 overflow-hidden">
|
|
40
|
-
{/* Desktop sidebar */}
|
|
41
|
-
<div className="hidden md:flex shrink-0">
|
|
42
|
-
<
|
|
38
|
+
{/* Desktop sidebar — floating card with shiny border */}
|
|
39
|
+
<div id="tour-sidebar" className="hidden md:flex shrink-0 pl-3 pt-3 pb-3">
|
|
40
|
+
<div className="relative rounded-3xl overflow-hidden h-full">
|
|
41
|
+
{/* Shiny border highlight */}
|
|
42
|
+
<div className="absolute inset-0 rounded-3xl bg-gradient-to-b from-white/[0.12] via-white/[0.04] to-transparent pointer-events-none" />
|
|
43
|
+
<div className="relative rounded-3xl bg-[#1A1A1A] m-px h-full">
|
|
44
|
+
<Sidebar userName={userName} backendStatus={status} />
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
43
47
|
</div>
|
|
44
48
|
|
|
45
49
|
{/* Main content + footer */}
|
|
46
50
|
<div className="flex flex-1 flex-col overflow-hidden">
|
|
47
|
-
<main className="flex-1 overflow-y-auto">{children}</main>
|
|
48
|
-
<Footer status={status} />
|
|
51
|
+
<main id="tour-dashboard" className="flex-1 overflow-y-auto">{children}</main>
|
|
49
52
|
</div>
|
|
50
53
|
</div>
|
|
51
54
|
</div>
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from '@/components/ui/sheet';
|
|
8
8
|
import Sidebar from './Sidebar';
|
|
9
9
|
|
|
10
|
-
export default function MobileNav() {
|
|
10
|
+
export default function MobileNav({ userName, backendStatus }: { userName?: string; backendStatus?: 'healthy' | 'restarting' }) {
|
|
11
11
|
const [open, setOpen] = useState(false);
|
|
12
12
|
|
|
13
13
|
return (
|
|
@@ -22,7 +22,7 @@ export default function MobileNav() {
|
|
|
22
22
|
<Sheet open={open} onOpenChange={setOpen}>
|
|
23
23
|
<SheetContent side="left" className="p-0 w-64" showCloseButton={false}>
|
|
24
24
|
<SheetTitle className="sr-only">Navigation</SheetTitle>
|
|
25
|
-
<Sidebar onNavigate={() => setOpen(false)} />
|
|
25
|
+
<Sidebar userName={userName} backendStatus={backendStatus} onNavigate={() => setOpen(false)} />
|
|
26
26
|
</SheetContent>
|
|
27
27
|
</Sheet>
|
|
28
28
|
</>
|
|
@@ -1,46 +1,74 @@
|
|
|
1
1
|
import { NavLink } from 'react-router';
|
|
2
2
|
import {
|
|
3
3
|
LayoutDashboard,
|
|
4
|
+
AppWindow,
|
|
5
|
+
Search,
|
|
4
6
|
CircleHelp,
|
|
5
7
|
} from 'lucide-react';
|
|
6
8
|
import { cn } from '@/lib/utils';
|
|
7
9
|
|
|
8
10
|
function getGreeting(): string {
|
|
9
11
|
const hour = new Date().getHours();
|
|
10
|
-
if (hour < 12) return 'Good
|
|
11
|
-
if (hour < 18) return 'Good
|
|
12
|
-
return 'Good
|
|
12
|
+
if (hour < 12) return 'Good Morning';
|
|
13
|
+
if (hour < 18) return 'Good Afternoon';
|
|
14
|
+
return 'Good Evening';
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
interface SidebarProps {
|
|
18
|
+
userName?: string;
|
|
19
|
+
backendStatus?: 'healthy' | 'restarting';
|
|
16
20
|
onNavigate?: () => void;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
export default function Sidebar({ onNavigate }: SidebarProps) {
|
|
23
|
+
export default function Sidebar({ userName, backendStatus = 'healthy', onNavigate }: SidebarProps) {
|
|
24
|
+
const firstName = userName?.split(/\s+/)[0] || 'Human';
|
|
20
25
|
return (
|
|
21
|
-
<aside className="flex flex-col h-full w-64
|
|
26
|
+
<aside className="flex flex-col h-full w-64 bg-transparent p-5 pt-8">
|
|
22
27
|
{/* Logo */}
|
|
23
28
|
<div className="flex items-center gap-2.5 mb-8">
|
|
24
29
|
<img src="/fluxy.png" alt="Fluxy" className="h-7 w-auto" />
|
|
25
|
-
<span className="font-
|
|
30
|
+
<span className="font-bold text-lg" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>Fluxy</span>
|
|
26
31
|
</div>
|
|
27
32
|
|
|
28
33
|
{/* Greeting */}
|
|
29
34
|
<div className="mb-10">
|
|
30
|
-
<h1 className="text-4xl font-bold leading-[1.1]
|
|
31
|
-
{getGreeting()}
|
|
35
|
+
<h1 className="text-4xl font-bold leading-[1.1]">
|
|
36
|
+
{getGreeting()}{firstName ? ',' : ''}
|
|
32
37
|
</h1>
|
|
33
|
-
|
|
38
|
+
{firstName && (
|
|
39
|
+
<h2
|
|
40
|
+
className="text-4xl font-bold mt-0.5 tracking-tight leading-[1.08] w-fit"
|
|
41
|
+
style={{
|
|
42
|
+
backgroundImage: 'linear-gradient(to right, #4FF2FE, #BC20DE, #FE546B)',
|
|
43
|
+
WebkitBackgroundClip: 'text',
|
|
44
|
+
WebkitTextFillColor: 'transparent',
|
|
45
|
+
backgroundClip: 'text',
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
{firstName}.
|
|
49
|
+
</h2>
|
|
50
|
+
)}
|
|
34
51
|
</div>
|
|
35
52
|
|
|
36
53
|
{/* Navigation */}
|
|
37
54
|
<nav className="flex-1 space-y-0.5">
|
|
38
55
|
<NavItem to="/" icon={LayoutDashboard} label="Dashboard" onNavigate={onNavigate} />
|
|
39
|
-
<NavItem to="/
|
|
56
|
+
<NavItem to="/app1" icon={AppWindow} label="App 1" sub="Ask Fluxy to create your first app and remove this placeholder" onNavigate={onNavigate} />
|
|
57
|
+
<NavItem to="/research" icon={Search} label="Research" sub="Ask Fluxy to research anything for you, maybe even daily" onNavigate={onNavigate} />
|
|
58
|
+
<NavItem to="/whatelse" icon={CircleHelp} label="What Else?" sub="The sky is the limit, just ask and Fluxy will do it!" onNavigate={onNavigate} />
|
|
40
59
|
</nav>
|
|
41
60
|
|
|
42
|
-
{/*
|
|
43
|
-
<div className="
|
|
61
|
+
{/* Backend status */}
|
|
62
|
+
<div className="rounded-xl bg-[#282828] px-3.5 py-2.5 flex items-center gap-2">
|
|
63
|
+
<span
|
|
64
|
+
className={`h-2 w-2 rounded-full shrink-0 ${
|
|
65
|
+
backendStatus === 'healthy' ? 'bg-emerald-500' : 'bg-orange-400 animate-pulse'
|
|
66
|
+
}`}
|
|
67
|
+
/>
|
|
68
|
+
<span className="text-xs text-muted-foreground">
|
|
69
|
+
Workspace: {backendStatus === 'healthy' ? 'Live' : 'Restarting'}
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
44
72
|
</aside>
|
|
45
73
|
);
|
|
46
74
|
}
|
|
@@ -49,11 +77,13 @@ function NavItem({
|
|
|
49
77
|
to,
|
|
50
78
|
icon: Icon,
|
|
51
79
|
label,
|
|
80
|
+
sub,
|
|
52
81
|
onNavigate,
|
|
53
82
|
}: {
|
|
54
83
|
to: string;
|
|
55
84
|
icon: React.ComponentType<{ className?: string }>;
|
|
56
85
|
label: string;
|
|
86
|
+
sub?: string;
|
|
57
87
|
onNavigate?: () => void;
|
|
58
88
|
}) {
|
|
59
89
|
return (
|
|
@@ -63,15 +93,18 @@ function NavItem({
|
|
|
63
93
|
onClick={onNavigate}
|
|
64
94
|
className={({ isActive }) =>
|
|
65
95
|
cn(
|
|
66
|
-
'flex items-
|
|
96
|
+
'flex items-start gap-3 w-full px-3 py-2.5 rounded-lg text-sm transition-colors',
|
|
67
97
|
isActive
|
|
68
98
|
? 'bg-sidebar-accent text-foreground font-medium'
|
|
69
99
|
: 'text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50',
|
|
70
100
|
)
|
|
71
101
|
}
|
|
72
102
|
>
|
|
73
|
-
<Icon className="h-[18px] w-[18px]" />
|
|
74
|
-
|
|
103
|
+
<Icon className="h-[18px] w-[18px] mt-0.5 shrink-0" />
|
|
104
|
+
<div>
|
|
105
|
+
{label}
|
|
106
|
+
{sub && <p className="text-[10px] text-muted-foreground/50 font-normal leading-tight mt-0.5">{sub}</p>}
|
|
107
|
+
</div>
|
|
75
108
|
</NavLink>
|
|
76
109
|
);
|
|
77
110
|
}
|