@ramme-io/create-app 2.0.0-alpha.4 ā 2.0.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/README.md +2 -2
- package/index.js +3 -3
- package/package.json +1 -1
- package/template/index.html +1 -1
- package/template/package.json +15 -6
- package/template/public/_redirects +1 -0
- package/template/src/App.tsx +3 -2
- package/template/src/components/AppHeader.tsx +5 -0
- package/template/src/components/ScrollToTop.tsx +6 -6
- package/template/src/features/ai/pages/AiChat.tsx +136 -37
- package/template/src/features/auth/AuthContext.tsx +16 -6
- package/template/src/features/config/AppConfigContext.tsx +2 -2
- package/template/src/features/docs/pages/EdgeTelemetryDemo.tsx +149 -0
- package/template/src/features/onboarding/pages/AboutRamme.tsx +12 -12
- package/template/src/features/onboarding/pages/PrototypeGallery.tsx +8 -6
- package/template/src/features/onboarding/pages/RammeFeatures.tsx +18 -17
- package/template/src/features/onboarding/pages/RammeTutorial.tsx +20 -11
- package/template/src/features/onboarding/pages/Welcome.tsx +6 -6
- package/template/src/features/styleguide/sections/tables/TablesSection.tsx +25 -5
- package/template/src/features/theme/pages/ThemeCustomizerPage.tsx +344 -256
- package/template/src/features/theme/utils/ThemeGenerator.logic.ts +587 -0
- package/template/src/hooks/__tests__/useStudioHotkeys.test.ts +100 -0
- package/template/src/hooks/useStudioHotkeys.ts +36 -0
- package/template/src/index.css +91 -1
- package/template/src/main.tsx +44 -2
- package/template/src/templates/dashboard/DashboardLayout.tsx +6 -1
- package/template/src/templates/dashboard/dashboard.sitemap.ts +1 -1
- package/template/src/templates/docs/docs.sitemap.ts +8 -0
- package/template/src/templates/settings/SettingsLayout.tsx +13 -26
- package/template/src/test/setup.ts +1 -0
- package/template/tsconfig.app.json +1 -0
- package/template/vite.config.ts +7 -0
package/README.md
CHANGED
|
@@ -15,9 +15,9 @@ This CLI scaffolds a production-ready React application with:
|
|
|
15
15
|
Get started in seconds. No global installation required.
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
npm create @ramme-io/app
|
|
18
|
+
npm create @ramme-io/app my-app
|
|
19
19
|
# or
|
|
20
|
-
|
|
20
|
+
npm create @ramme-io/app my-app
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
You will be prompted to name your project.
|
package/index.js
CHANGED
|
@@ -19,7 +19,7 @@ const projectName = process.argv[2];
|
|
|
19
19
|
if (!projectName) {
|
|
20
20
|
console.error('ā Error: Please specify the project directory.');
|
|
21
21
|
console.log('\nš Usage:');
|
|
22
|
-
console.log(' npm create @ramme-io/app
|
|
22
|
+
console.log(' npm create @ramme-io/app <project-name>\n');
|
|
23
23
|
process.exit(1);
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -27,7 +27,7 @@ const templatePath = path.resolve(__dirname, 'template');
|
|
|
27
27
|
const destinationPath = path.resolve(process.cwd(), projectName);
|
|
28
28
|
|
|
29
29
|
// THE STABILITY PACT: Hardcoded target versions for the public release
|
|
30
|
-
const RELEASE_VERSION = "^2.0.0
|
|
30
|
+
const RELEASE_VERSION = "^2.0.0";
|
|
31
31
|
|
|
32
32
|
try {
|
|
33
33
|
console.log(`\nš Creating new Ramme app in: ${destinationPath}`);
|
|
@@ -79,7 +79,7 @@ try {
|
|
|
79
79
|
|
|
80
80
|
console.log('šØ Happy Creating!\n');
|
|
81
81
|
console.log('š Docs: https://ramme.io/docs');
|
|
82
|
-
console.log('
|
|
82
|
+
console.log('š¬ Discord: https://ramme.io/discord\n');
|
|
83
83
|
|
|
84
84
|
} catch (err) {
|
|
85
85
|
console.error('\nā Critical Failure:', err.message);
|
package/package.json
CHANGED
package/template/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/orange.png" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
-
<title>Ramme -
|
|
7
|
+
<title>Ramme | The High-Fidelity Creator Framework</title>
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<div id="root"></div>
|
package/template/package.json
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ramme-starter",
|
|
3
3
|
"private": true,
|
|
4
|
-
"version": "2.0.0
|
|
4
|
+
"version": "2.0.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "vite",
|
|
8
8
|
"build": "tsc && vite build",
|
|
9
9
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
|
10
|
-
"preview": "vite preview"
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest"
|
|
11
13
|
},
|
|
12
14
|
"dependencies": {
|
|
13
15
|
"@ai-sdk/google": "^0.0.10",
|
|
14
16
|
"@google/generative-ai": "^0.24.1",
|
|
15
17
|
"@radix-ui/react-slot": "^1.2.4",
|
|
16
|
-
"@ramme-io/kernel": "
|
|
17
|
-
"@ramme-io/
|
|
18
|
+
"@ramme-io/kernel": "workspace:*",
|
|
19
|
+
"@ramme-io/shared": "workspace:*",
|
|
20
|
+
"@ramme-io/ui": "workspace:*",
|
|
21
|
+
"ably": "^2.21.0",
|
|
18
22
|
"ag-charts-community": "^13.0.0",
|
|
19
23
|
"ag-charts-react": "^13.0.0",
|
|
20
24
|
"ag-grid-community": "^31.3.1",
|
|
@@ -30,8 +34,8 @@
|
|
|
30
34
|
"react": "^18.3.1",
|
|
31
35
|
"react-datepicker": "^7.3.0",
|
|
32
36
|
"react-dom": "^18.3.1",
|
|
33
|
-
"react-router-dom": "6.22.3",
|
|
34
37
|
"react-markdown": "^10.1.0",
|
|
38
|
+
"react-router-dom": "6.22.3",
|
|
35
39
|
"react-select": "^5.8.0",
|
|
36
40
|
"react-syntax-highlighter": "^16.1.0",
|
|
37
41
|
"recharts": "^2.12.7",
|
|
@@ -41,14 +45,19 @@
|
|
|
41
45
|
"zustand": "^5.0.0"
|
|
42
46
|
},
|
|
43
47
|
"devDependencies": {
|
|
48
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
49
|
+
"@testing-library/react": "^16.3.2",
|
|
44
50
|
"@types/node": "^20.14.10",
|
|
45
51
|
"@types/react": "^18.3.3",
|
|
46
52
|
"@types/react-dom": "^18.3.0",
|
|
47
53
|
"@vitejs/plugin-react": "^4.3.1",
|
|
54
|
+
"@vitest/ui": "^2",
|
|
48
55
|
"autoprefixer": "^10.4.19",
|
|
56
|
+
"jsdom": "^29.0.0",
|
|
49
57
|
"postcss": "^8.4.39",
|
|
50
58
|
"tailwindcss": "^3.4.4",
|
|
51
59
|
"typescript": "^5.2.2",
|
|
52
|
-
"vite": "^5.3.1"
|
|
60
|
+
"vite": "^5.3.1",
|
|
61
|
+
"vitest": "^2.1.9"
|
|
53
62
|
}
|
|
54
63
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* /index.html 200
|
package/template/src/App.tsx
CHANGED
|
@@ -15,8 +15,6 @@ import DashboardLayout from './templates/dashboard/DashboardLayout';
|
|
|
15
15
|
import DocsLayout from './templates/docs/DocsLayout';
|
|
16
16
|
import SettingsLayout from './templates/settings/SettingsLayout';
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
18
|
// --- SITEMAPS ---
|
|
21
19
|
import { dashboardSitemap } from './templates/dashboard/dashboard.sitemap';
|
|
22
20
|
import { docsSitemap } from './templates/docs/docs.sitemap';
|
|
@@ -24,6 +22,7 @@ import { settingsSitemap } from './templates/settings/settings.sitemap';
|
|
|
24
22
|
|
|
25
23
|
// --- UTILS ---
|
|
26
24
|
import ProtectedRoute from './components/ProtectedRoute';
|
|
25
|
+
import { useStudioHotkeys } from './hooks/useStudioHotkeys';
|
|
27
26
|
import NotFound from './components/NotFound';
|
|
28
27
|
import ScrollToTop from './components/ScrollToTop';
|
|
29
28
|
import HashLinkScroll from './components/HashLinkScroll';
|
|
@@ -82,6 +81,7 @@ const generateRoutes = (items: any[]) => {
|
|
|
82
81
|
};
|
|
83
82
|
|
|
84
83
|
function App() {
|
|
84
|
+
useStudioHotkeys();
|
|
85
85
|
const liveDashboardRoutes = useDynamicSitemap(dashboardSitemap);
|
|
86
86
|
|
|
87
87
|
return (
|
|
@@ -120,6 +120,7 @@ function App() {
|
|
|
120
120
|
|
|
121
121
|
{/* Settings */}
|
|
122
122
|
<Route path="/settings/*" element={<SettingsLayout />}>
|
|
123
|
+
<Route index element={<Navigate to="general" replace />} />
|
|
123
124
|
{generateRoutes(settingsSitemap)}
|
|
124
125
|
</Route>
|
|
125
126
|
|
|
@@ -5,12 +5,12 @@ const ScrollToTop = () => {
|
|
|
5
5
|
const { pathname } = useLocation();
|
|
6
6
|
|
|
7
7
|
useEffect(() => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
top: 0,
|
|
11
|
-
|
|
12
|
-
behavior: 'instant'
|
|
13
|
-
}
|
|
8
|
+
const container = document.getElementById('main-scroll-container');
|
|
9
|
+
if (container) {
|
|
10
|
+
container.scrollTo({ top: 0, left: 0, behavior: 'instant' as ScrollBehavior });
|
|
11
|
+
} else {
|
|
12
|
+
window.scrollTo({ top: 0, left: 0, behavior: 'instant' as ScrollBehavior });
|
|
13
|
+
}
|
|
14
14
|
}, [pathname]);
|
|
15
15
|
|
|
16
16
|
return null;
|
|
@@ -1,61 +1,160 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Message,
|
|
8
|
-
PromptInput,
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import ReactMarkdown from 'react-markdown';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
5
|
+
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
6
|
+
import {
|
|
7
|
+
Card, Conversation, Message, PromptInput, PageHeader, Icon,
|
|
9
8
|
} from '@ramme-io/ui';
|
|
10
|
-
|
|
9
|
+
|
|
10
|
+
const systemInstruction = `
|
|
11
|
+
You are the Ramme OS Assistant, part of the internal dev team.
|
|
12
|
+
Ramme is a high-performance, browser-native modular web-OS (Kernel + UI).
|
|
13
|
+
It is NOT the Instagram wrapper by Niklas Higle.
|
|
14
|
+
Answer as a Senior Engineer helping with a monorepo project.
|
|
15
|
+
Use numbered lists for steps and code blocks for commands.
|
|
16
|
+
`;
|
|
11
17
|
|
|
12
18
|
const AiChat: React.FC = () => {
|
|
13
|
-
const { messages, isLoading, sendMessage } = useMockChat();
|
|
14
19
|
const [input, setInput] = useState('');
|
|
20
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
21
|
+
const [apiHistory, setApiHistory] = useState<any[]>([]);
|
|
22
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
const [messages, setMessages] = useState([
|
|
24
|
+
{ id: 'init', author: 'AI Agent', content: 'Ramme OS Assistant online. How can I help you build today?', isUser: false }
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
29
|
+
}, [messages, isLoading]);
|
|
30
|
+
|
|
31
|
+
const handleSubmit = async (e?: React.FormEvent) => {
|
|
32
|
+
if (e) e.preventDefault();
|
|
33
|
+
if (!input.trim() || isLoading) return;
|
|
15
34
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
sendMessage(input);
|
|
35
|
+
const userText = input;
|
|
36
|
+
setMessages(prev => [...prev, { id: `u-${Date.now()}`, author: 'You', content: userText, isUser: true }]);
|
|
19
37
|
setInput('');
|
|
38
|
+
setIsLoading(true);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const apiKey = localStorage.getItem('ramme_vault_gemini_key');
|
|
42
|
+
if (!apiKey) {
|
|
43
|
+
setMessages(prev => [...prev, {
|
|
44
|
+
id: `err-${Date.now()}`,
|
|
45
|
+
author: 'System',
|
|
46
|
+
content: 'No API key found. Please configure your Gemini API key in Settings ā Theme Engine.',
|
|
47
|
+
isUser: false
|
|
48
|
+
}]);
|
|
49
|
+
setIsLoading(false);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
contents: [
|
|
58
|
+
{ role: "user", parts: [{ text: `INSTRUCTION: ${systemInstruction}` }] },
|
|
59
|
+
{ role: "model", parts: [{ text: "Understood." }] },
|
|
60
|
+
...apiHistory,
|
|
61
|
+
{ role: "user", parts: [{ text: userText }] }
|
|
62
|
+
]
|
|
63
|
+
})
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const data = await response.json();
|
|
67
|
+
const aiText = data.candidates?.[0]?.content?.parts?.[0]?.text || "No response.";
|
|
68
|
+
|
|
69
|
+
setMessages(prev => [...prev, { id: `ai-${Date.now()}`, author: 'AI Agent', content: aiText, isUser: false }]);
|
|
70
|
+
setApiHistory([...apiHistory, { role: "user", parts: [{ text: userText }] }, { role: "model", parts: [{ text: aiText }] }]);
|
|
71
|
+
} catch (err: any) {
|
|
72
|
+
setMessages(prev => [...prev, { id: `err-${Date.now()}`, author: 'System', content: `Error: ${err.message}`, isUser: false }]);
|
|
73
|
+
} finally {
|
|
74
|
+
setIsLoading(false);
|
|
75
|
+
}
|
|
20
76
|
};
|
|
21
77
|
|
|
22
78
|
return (
|
|
23
|
-
<div className="p-4 sm:p-6 lg:p-8 space-y-
|
|
79
|
+
<div className="p-4 sm:p-6 lg:p-8 space-y-6 h-[calc(100vh-64px)] flex flex-col">
|
|
24
80
|
<PageHeader
|
|
25
81
|
title="AI Assistant"
|
|
26
82
|
description="Full-screen command center for Ramme AI."
|
|
27
83
|
/>
|
|
28
84
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
85
|
+
<Card className="flex-1 flex flex-col min-h-0 shadow-sm border-border/50 overflow-hidden">
|
|
86
|
+
<div className="px-4 py-2 border-b flex justify-between items-center bg-muted/20">
|
|
87
|
+
<div className="flex items-center gap-2">
|
|
88
|
+
<div className={`w-2 h-2 rounded-full ${isLoading ? 'bg-amber-500 animate-pulse' : 'bg-emerald-500'}`} />
|
|
89
|
+
<span className="font-bold text-[10px] uppercase tracking-widest opacity-60">Ramme Assistant</span>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div ref={scrollRef} className="flex-1 overflow-y-auto px-4 py-4 custom-scrollbar">
|
|
34
94
|
<Conversation>
|
|
35
95
|
{messages.map((msg) => (
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
96
|
+
<div key={msg.id} className={`flex ${msg.isUser ? 'justify-end' : 'justify-start'} mb-3`}>
|
|
97
|
+
<div className="max-w-[98%] w-full">
|
|
98
|
+
<Message
|
|
99
|
+
author={msg.author}
|
|
100
|
+
isUser={msg.isUser}
|
|
101
|
+
// @ts-ignore
|
|
102
|
+
content={
|
|
103
|
+
<div className="prose prose-sm dark:prose-invert max-w-none text-foreground leading-relaxed">
|
|
104
|
+
<ReactMarkdown
|
|
105
|
+
remarkPlugins={[remarkGfm]}
|
|
106
|
+
components={{
|
|
107
|
+
p: ({children}) => <p className="mb-4 last:mb-0">{children}</p>,
|
|
108
|
+
ol: ({children}) => <ol className="list-decimal pl-8 mb-4 space-y-2">{children}</ol>,
|
|
109
|
+
ul: ({children}) => <ul className="list-disc pl-8 mb-4 space-y-2">{children}</ul>,
|
|
110
|
+
li: ({children}) => <li className="pl-1">{children}</li>,
|
|
111
|
+
code({ node, inline, className, children, ...props }: any) {
|
|
112
|
+
const match = /language-(\w+)/.exec(className || '');
|
|
113
|
+
return !inline && match ? (
|
|
114
|
+
<div className="my-5 rounded-lg overflow-hidden border border-border bg-[#0d0d0d] shadow-xl">
|
|
115
|
+
<div className="px-3 py-1 bg-white/5 border-b border-white/10 flex justify-between">
|
|
116
|
+
<span className="text-[9px] text-muted-foreground uppercase">{match[1]}</span>
|
|
117
|
+
</div>
|
|
118
|
+
<SyntaxHighlighter
|
|
119
|
+
style={oneDark}
|
|
120
|
+
language={match[1]}
|
|
121
|
+
PreTag="div"
|
|
122
|
+
customStyle={{ margin: 0, padding: '1.2rem', fontSize: '0.8rem', background: 'transparent' }}
|
|
123
|
+
{...props}
|
|
124
|
+
>
|
|
125
|
+
{String(children).replace(/\n$/, '')}
|
|
126
|
+
</SyntaxHighlighter>
|
|
127
|
+
</div>
|
|
128
|
+
) : (
|
|
129
|
+
<code className="bg-muted px-1.5 py-0.5 rounded font-mono text-primary text-[0.9em]" {...props}>
|
|
130
|
+
{children}
|
|
131
|
+
</code>
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{msg.content}
|
|
137
|
+
</ReactMarkdown>
|
|
138
|
+
</div>
|
|
139
|
+
}
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
45
143
|
))}
|
|
46
|
-
{isLoading && <Message author="AI Agent" isUser={false} loading />}
|
|
47
144
|
</Conversation>
|
|
48
145
|
</div>
|
|
49
|
-
|
|
50
|
-
<div className="p-4 border-t bg-card">
|
|
51
|
-
<PromptInput
|
|
52
|
-
value={input}
|
|
53
|
-
onChange={(e) => setInput(e.target.value)}
|
|
54
|
-
onSubmit={handleSubmit}
|
|
55
|
-
placeholder="Type a command or ask a question..."
|
|
56
|
-
/>
|
|
146
|
+
|
|
147
|
+
<div className="p-4 border-t bg-card/40 backdrop-blur-sm">
|
|
148
|
+
<PromptInput value={input} onChange={(e) => setInput(e.target.value)} onSubmit={handleSubmit} placeholder="Ask about Ramme..." />
|
|
57
149
|
</div>
|
|
58
150
|
</Card>
|
|
151
|
+
|
|
152
|
+
<style dangerouslySetInnerHTML={{ __html: `
|
|
153
|
+
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
|
|
154
|
+
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
|
|
155
|
+
.custom-scrollbar::-webkit-scrollbar-thumb { background: rgb(var(--app-primary-color), 0.2); border-radius: 10px; }
|
|
156
|
+
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: rgb(var(--app-primary-color), 0.5); }
|
|
157
|
+
`}} />
|
|
59
158
|
</div>
|
|
60
159
|
);
|
|
61
160
|
};
|
|
@@ -7,6 +7,19 @@ const EMERGENCY_ADMIN: User = {
|
|
|
7
7
|
id: 'usr_admin_force',
|
|
8
8
|
name: 'Alex Admin (Recovery)',
|
|
9
9
|
email: 'alex@example.com',
|
|
10
|
+
password: 'admin',
|
|
11
|
+
role: 'admin',
|
|
12
|
+
status: 'active',
|
|
13
|
+
joinedAt: new Date().toISOString(),
|
|
14
|
+
lastActive: new Date().toISOString()
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// š DEFAULT GUEST: Auto-login for frictionless marketing site experience
|
|
18
|
+
const DEFAULT_GUEST: User = {
|
|
19
|
+
id: 'usr_guest',
|
|
20
|
+
name: 'Guest Creator',
|
|
21
|
+
email: 'guest@ramme.io',
|
|
22
|
+
password: 'guest',
|
|
10
23
|
role: 'admin',
|
|
11
24
|
status: 'active',
|
|
12
25
|
joinedAt: new Date().toISOString(),
|
|
@@ -27,10 +40,10 @@ interface AuthContextType {
|
|
|
27
40
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
28
41
|
|
|
29
42
|
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
30
|
-
const [user, setUser] = useState<User | null>(
|
|
31
|
-
const [isLoading, setIsLoading] = useState(
|
|
43
|
+
const [user, setUser] = useState<User | null>(DEFAULT_GUEST);
|
|
44
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
32
45
|
|
|
33
|
-
// 1. Mount: Check for existing session
|
|
46
|
+
// 1. Mount: Check for existing session, fall back to guest
|
|
34
47
|
useEffect(() => {
|
|
35
48
|
const storedUser = localStorage.getItem(SESSION_KEY);
|
|
36
49
|
if (storedUser) {
|
|
@@ -38,15 +51,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|
|
38
51
|
const parsed = JSON.parse(storedUser);
|
|
39
52
|
if (parsed && parsed.id) {
|
|
40
53
|
setUser(parsed);
|
|
41
|
-
} else {
|
|
42
|
-
localStorage.removeItem(SESSION_KEY);
|
|
43
54
|
}
|
|
44
55
|
} catch (e) {
|
|
45
56
|
console.error("Failed to parse session", e);
|
|
46
57
|
localStorage.removeItem(SESSION_KEY);
|
|
47
58
|
}
|
|
48
59
|
}
|
|
49
|
-
setIsLoading(false);
|
|
50
60
|
}, []);
|
|
51
61
|
|
|
52
62
|
// 2. Action: Login
|
|
@@ -14,8 +14,8 @@ export const AppConfigProvider = ({ children }: { children: ReactNode }) => {
|
|
|
14
14
|
|
|
15
15
|
const [enableCRT, setEnableCRT] = useState(() => {
|
|
16
16
|
const stored = localStorage.getItem('app_enable_crt');
|
|
17
|
-
// Default to
|
|
18
|
-
return stored !== null ? JSON.parse(stored) :
|
|
17
|
+
// Default to false for clean marketing site clarity
|
|
18
|
+
return stored !== null ? JSON.parse(stored) : false;
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
useEffect(() => {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useDevice } from '@ramme-io/kernel';
|
|
3
|
+
import { CircularDial, WaterPumpControl, TelemetryLog } from '@ramme-io/ui';
|
|
4
|
+
import type { TelemetryLogRow } from '@ramme-io/ui';
|
|
5
|
+
import type { PumpMode } from '@ramme-io/ui';
|
|
6
|
+
import { StandardPageLayout } from '../../../components/layout/StandardPageLayout';
|
|
7
|
+
|
|
8
|
+
const EdgeTelemetryDemo: React.FC = () => {
|
|
9
|
+
const telemetry = useDevice();
|
|
10
|
+
const [fps, setFps] = useState(0);
|
|
11
|
+
const [pumpMode, setPumpMode] = useState<PumpMode>('AUTO');
|
|
12
|
+
const [targetMoisture, setTargetMoisture] = useState(40);
|
|
13
|
+
const [logData, setLogData] = useState<TelemetryLogRow[]>([]);
|
|
14
|
+
const logBuffer = useRef<TelemetryLogRow[]>([]);
|
|
15
|
+
const frameCount = useRef(0);
|
|
16
|
+
const rafId = useRef(0);
|
|
17
|
+
|
|
18
|
+
// Append incoming telemetry to the historical log buffer (capped at 100)
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!telemetry) return;
|
|
21
|
+
const row: TelemetryLogRow = {
|
|
22
|
+
timestamp: Date.now(),
|
|
23
|
+
soilMoisture: telemetry.soilMoisture,
|
|
24
|
+
ambientLight: telemetry.ambientLight,
|
|
25
|
+
temperature: telemetry.temperature,
|
|
26
|
+
};
|
|
27
|
+
logBuffer.current = [...logBuffer.current.slice(-99), row];
|
|
28
|
+
setLogData(logBuffer.current);
|
|
29
|
+
}, [telemetry]);
|
|
30
|
+
|
|
31
|
+
// Continuous RAF loop tallies frames; 1s interval reads & resets the tally
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const tick = () => {
|
|
34
|
+
frameCount.current += 1;
|
|
35
|
+
rafId.current = requestAnimationFrame(tick);
|
|
36
|
+
};
|
|
37
|
+
rafId.current = requestAnimationFrame(tick);
|
|
38
|
+
|
|
39
|
+
const interval = setInterval(() => {
|
|
40
|
+
setFps(frameCount.current);
|
|
41
|
+
frameCount.current = 0;
|
|
42
|
+
}, 1000);
|
|
43
|
+
|
|
44
|
+
return () => {
|
|
45
|
+
cancelAnimationFrame(rafId.current);
|
|
46
|
+
clearInterval(interval);
|
|
47
|
+
};
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<StandardPageLayout
|
|
52
|
+
title="Edge Telemetry Engine"
|
|
53
|
+
description="Hardware-in-the-loop simulation testing 50Hz MQTT ingestion and Zero Jank UI rendering."
|
|
54
|
+
>
|
|
55
|
+
{/* Status Bar */}
|
|
56
|
+
<div className="flex items-center gap-4 p-4 rounded-2xl bg-card border border-border font-mono text-xs mb-8">
|
|
57
|
+
<div className="flex items-center gap-2">
|
|
58
|
+
<div className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
|
59
|
+
<span className="font-bold text-foreground uppercase tracking-widest">Kernel Link Active</span>
|
|
60
|
+
</div>
|
|
61
|
+
<div className="ml-auto flex items-center gap-4">
|
|
62
|
+
<span className={fps > 20 ? "text-red-500 font-bold" : "text-green-500 font-bold"}>
|
|
63
|
+
{fps} FPS
|
|
64
|
+
</span>
|
|
65
|
+
<span className="text-muted-foreground">
|
|
66
|
+
Target: ~15 FPS | Simulator: 50Hz
|
|
67
|
+
</span>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{/* Telemetry Grid */}
|
|
72
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
73
|
+
{/* Soil Moisture Dial */}
|
|
74
|
+
<div className="flex flex-col items-center gap-4 p-8 rounded-3xl bg-card border border-border">
|
|
75
|
+
{telemetry ? (
|
|
76
|
+
<CircularDial
|
|
77
|
+
value={telemetry.soilMoisture ?? 0}
|
|
78
|
+
label="Soil Moisture"
|
|
79
|
+
unit="%"
|
|
80
|
+
size={180}
|
|
81
|
+
/>
|
|
82
|
+
) : (
|
|
83
|
+
<div className="text-muted-foreground text-sm">Awaiting telemetry...</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Ambient Light Dial */}
|
|
88
|
+
<div className="flex flex-col items-center gap-4 p-8 rounded-3xl bg-card border border-border">
|
|
89
|
+
{telemetry ? (
|
|
90
|
+
<CircularDial
|
|
91
|
+
value={telemetry.ambientLight ?? 0}
|
|
92
|
+
min={0}
|
|
93
|
+
max={100000}
|
|
94
|
+
label="Ambient Light"
|
|
95
|
+
unit=" lx"
|
|
96
|
+
size={180}
|
|
97
|
+
/>
|
|
98
|
+
) : (
|
|
99
|
+
<div className="text-muted-foreground text-sm">Awaiting telemetry...</div>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{/* Temperature Dial */}
|
|
104
|
+
<div className="flex flex-col items-center gap-4 p-8 rounded-3xl bg-card border border-border">
|
|
105
|
+
{telemetry ? (
|
|
106
|
+
<CircularDial
|
|
107
|
+
value={telemetry.temperature ?? 0}
|
|
108
|
+
min={-40}
|
|
109
|
+
max={125}
|
|
110
|
+
label="Temperature"
|
|
111
|
+
unit="°C"
|
|
112
|
+
size={180}
|
|
113
|
+
/>
|
|
114
|
+
) : (
|
|
115
|
+
<div className="text-muted-foreground text-sm">Awaiting telemetry...</div>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Command: Water Pump Actuation */}
|
|
121
|
+
<div className="mt-8">
|
|
122
|
+
<WaterPumpControl
|
|
123
|
+
pumpMode={pumpMode}
|
|
124
|
+
targetMoisture={targetMoisture}
|
|
125
|
+
onCommand={(payload) => {
|
|
126
|
+
setPumpMode(payload.pumpMode);
|
|
127
|
+
setTargetMoisture(payload.targetMoisture);
|
|
128
|
+
console.log('[MQTT PUBLISH MOCK]', payload);
|
|
129
|
+
}}
|
|
130
|
+
className="max-w-md"
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Raw Payload Debug */}
|
|
135
|
+
<div className="mt-8 p-6 rounded-2xl bg-card border border-border">
|
|
136
|
+
<div className="font-mono text-[10px] uppercase tracking-[0.3em] text-muted-foreground mb-3 font-bold">Raw Smoothed Payload</div>
|
|
137
|
+
<pre className="font-mono text-sm text-foreground/80 leading-relaxed tabular-nums">
|
|
138
|
+
{JSON.stringify(telemetry, null, 2)}
|
|
139
|
+
</pre>
|
|
140
|
+
</div>
|
|
141
|
+
{/* Telemetry Log ā AG Grid virtualized scroll */}
|
|
142
|
+
<div className="mt-8">
|
|
143
|
+
<TelemetryLog rowData={logData} maxRows={100} height="320px" />
|
|
144
|
+
</div>
|
|
145
|
+
</StandardPageLayout>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export default EdgeTelemetryDemo;
|