@ramme-io/create-app 1.0.4 → 1.1.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 +11 -13
- package/package.json +2 -2
- package/template/DESIGNER_GUIDE.md +52 -32
- package/template/README.md +33 -38
- package/template/pkg.json +1 -1
- package/template/pnpm-lock.yaml +1790 -0
- package/template/src/components/AIChatWidget.tsx +69 -0
- package/template/src/hooks/useMockChat.ts +69 -0
- package/template/src/hooks/useSignal.ts +73 -0
- package/template/src/pages/AiChat.tsx +40 -54
- package/template/src/pages/Dashboard.tsx +79 -33
- package/template/src/templates/dashboard/DashboardLayout.tsx +24 -8
- package/template/src/types/schema.ts +26 -0
- package/template/src/types/signal.ts +22 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Card,
|
|
4
|
+
Conversation,
|
|
5
|
+
Message,
|
|
6
|
+
PromptInput,
|
|
7
|
+
Button,
|
|
8
|
+
Icon,
|
|
9
|
+
} from '@ramme-io/ui';
|
|
10
|
+
import { useMockChat } from '../hooks/useMockChat';
|
|
11
|
+
|
|
12
|
+
interface AIChatWidgetProps {
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const AIChatWidget: React.FC<AIChatWidgetProps> = ({ onClose }) => {
|
|
17
|
+
// 1. Use the shared "Brain"
|
|
18
|
+
const { messages, isLoading, sendMessage } = useMockChat();
|
|
19
|
+
const [input, setInput] = useState('');
|
|
20
|
+
|
|
21
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
sendMessage(input);
|
|
24
|
+
setInput('');
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Card className="fixed bottom-24 right-6 w-96 h-[500px] shadow-2xl flex flex-col z-50 animate-in slide-in-from-bottom-5 border-border">
|
|
29
|
+
|
|
30
|
+
{/* Header */}
|
|
31
|
+
<div className="p-4 border-b flex justify-between items-center bg-muted/50 rounded-t-lg">
|
|
32
|
+
<div className="flex items-center gap-2">
|
|
33
|
+
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
|
34
|
+
<span className="font-semibold text-sm">Bodewell AI</span>
|
|
35
|
+
</div>
|
|
36
|
+
<Button variant="ghost" size="icon" onClick={onClose} className="h-6 w-6">
|
|
37
|
+
<Icon name="x" size={16} />
|
|
38
|
+
</Button>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
{/* Chat History Area */}
|
|
42
|
+
<div className="flex-1 overflow-hidden p-0 bg-background">
|
|
43
|
+
<Conversation>
|
|
44
|
+
{messages.map((msg) => (
|
|
45
|
+
<Message
|
|
46
|
+
key={msg.id}
|
|
47
|
+
author={msg.author}
|
|
48
|
+
content={msg.content}
|
|
49
|
+
isUser={msg.isUser}
|
|
50
|
+
suggestions={msg.suggestions}
|
|
51
|
+
onSuggestionClick={(s) => sendMessage(s)}
|
|
52
|
+
/>
|
|
53
|
+
))}
|
|
54
|
+
{isLoading && <Message author="Bodewell AI" isUser={false} loading />}
|
|
55
|
+
</Conversation>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{/* Input Footer */}
|
|
59
|
+
<div className="p-4 border-t bg-card">
|
|
60
|
+
<PromptInput
|
|
61
|
+
value={input}
|
|
62
|
+
onChange={(e) => setInput(e.target.value)}
|
|
63
|
+
onSubmit={handleSubmit}
|
|
64
|
+
placeholder="Ask to adjust temperature..."
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
</Card>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import { useToast } from '@ramme-io/ui';
|
|
3
|
+
|
|
4
|
+
export interface ChatMessage {
|
|
5
|
+
id: string;
|
|
6
|
+
author: string;
|
|
7
|
+
content: string;
|
|
8
|
+
isUser: boolean;
|
|
9
|
+
suggestions?: string[];
|
|
10
|
+
loading?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const useMockChat = () => {
|
|
14
|
+
// 1. FIX: Destructure 'addToast' (the correct name from your Provider)
|
|
15
|
+
const { addToast } = useToast();
|
|
16
|
+
|
|
17
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
18
|
+
const [messages, setMessages] = useState<ChatMessage[]>([
|
|
19
|
+
{
|
|
20
|
+
id: '1',
|
|
21
|
+
author: 'Bodewell AI',
|
|
22
|
+
content: 'Hello! I am monitoring your 12 active devices. How can I help?',
|
|
23
|
+
isUser: false,
|
|
24
|
+
suggestions: ['Check System Status', 'Turn off Living Room AC']
|
|
25
|
+
}
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const sendMessage = useCallback((content: string) => {
|
|
29
|
+
if (!content.trim()) return;
|
|
30
|
+
|
|
31
|
+
// Add User Message
|
|
32
|
+
const userMsg = { id: Date.now().toString(), author: 'You', content, isUser: true };
|
|
33
|
+
setMessages(prev => [...prev, userMsg]);
|
|
34
|
+
setIsLoading(true);
|
|
35
|
+
|
|
36
|
+
// Simulate AI Delay
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
setIsLoading(false);
|
|
39
|
+
|
|
40
|
+
// Mock Response Logic
|
|
41
|
+
const aiMsg = {
|
|
42
|
+
id: (Date.now() + 1).toString(),
|
|
43
|
+
author: 'Bodewell AI',
|
|
44
|
+
content: `I received your command: "${content}". Executing protocol...`,
|
|
45
|
+
isUser: false,
|
|
46
|
+
suggestions: ['View Logs', 'Undo Action']
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
setMessages(prev => [...prev, aiMsg]);
|
|
50
|
+
|
|
51
|
+
// 2. FIX: Call addToast with separate arguments, not an object
|
|
52
|
+
// Signature: addToast(message, type, duration)
|
|
53
|
+
if (addToast) {
|
|
54
|
+
addToast(
|
|
55
|
+
`Successfully processed: ${content}`, // message
|
|
56
|
+
'success', // type
|
|
57
|
+
3000 // duration
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
}, 1500);
|
|
62
|
+
}, [addToast]);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
messages,
|
|
66
|
+
isLoading,
|
|
67
|
+
sendMessage
|
|
68
|
+
};
|
|
69
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import type { Signal } from '../types/signal';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for the Mock Generator.
|
|
6
|
+
* This allows us to simulate specific hardware constraints.
|
|
7
|
+
*/
|
|
8
|
+
interface SignalConfig<T> {
|
|
9
|
+
initialValue?: T;
|
|
10
|
+
min?: number; // Prevent drifting too low
|
|
11
|
+
max?: number; // Prevent drifting too high
|
|
12
|
+
interval?: number; // Update speed (ms)
|
|
13
|
+
unit?: string; // e.g., "°F", "%", "RPM"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The "Universal Socket."
|
|
18
|
+
* UI Components use this hook to subscribe to data.
|
|
19
|
+
* Currently running in "Synthetic Mode" (Mock) with bounded randomization.
|
|
20
|
+
*/
|
|
21
|
+
export function useSignal<T = any>(signalId: string, config: SignalConfig<T> = {}): Signal<T> {
|
|
22
|
+
// Defaults
|
|
23
|
+
const {
|
|
24
|
+
initialValue,
|
|
25
|
+
min = -Infinity,
|
|
26
|
+
max = Infinity,
|
|
27
|
+
interval = 2000,
|
|
28
|
+
unit
|
|
29
|
+
} = config;
|
|
30
|
+
|
|
31
|
+
// 1. Initialize
|
|
32
|
+
const [signal, setSignal] = useState<Signal<T>>({
|
|
33
|
+
id: signalId,
|
|
34
|
+
value: initialValue as T,
|
|
35
|
+
unit: unit,
|
|
36
|
+
timestamp: Date.now(),
|
|
37
|
+
status: 'fresh',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
// 2. SIMULATION: Generate synthetic data updates
|
|
42
|
+
const timer = setInterval(() => {
|
|
43
|
+
setSignal(prev => {
|
|
44
|
+
let newValue: any = prev.value;
|
|
45
|
+
|
|
46
|
+
// Only apply math if it's a number
|
|
47
|
+
if (typeof prev.value === 'number') {
|
|
48
|
+
// Generate drift
|
|
49
|
+
const variance = (Math.random() - 0.5) * 2; // +/- 1
|
|
50
|
+
let nextNum = prev.value + variance;
|
|
51
|
+
|
|
52
|
+
// 🛡️ CLAMPING: Apply the physical bounds
|
|
53
|
+
if (min !== undefined) nextNum = Math.max(min, nextNum);
|
|
54
|
+
if (max !== undefined) nextNum = Math.min(max, nextNum);
|
|
55
|
+
|
|
56
|
+
newValue = Number(nextNum.toFixed(1));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
...prev,
|
|
61
|
+
value: newValue,
|
|
62
|
+
timestamp: Date.now(),
|
|
63
|
+
status: 'fresh',
|
|
64
|
+
unit: unit // Ensure unit persists
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
}, interval);
|
|
68
|
+
|
|
69
|
+
return () => clearInterval(timer);
|
|
70
|
+
}, [signalId, min, max, interval, unit]);
|
|
71
|
+
|
|
72
|
+
return signal;
|
|
73
|
+
}
|
|
@@ -1,74 +1,60 @@
|
|
|
1
|
-
// src/pages/AiChat.tsx
|
|
2
|
-
import React, { useState
|
|
1
|
+
// ramme-app-starter/template/src/pages/AiChat.tsx
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
Card,
|
|
5
5
|
PageHeader,
|
|
6
6
|
Conversation,
|
|
7
7
|
Message,
|
|
8
8
|
PromptInput,
|
|
9
|
-
type MessageProps,
|
|
10
9
|
} from '@ramme-io/ui';
|
|
11
|
-
|
|
12
|
-
// The MessageData type now directly uses MessageProps from the library
|
|
13
|
-
type MessageData = Omit<MessageProps, 'onSuggestionClick'>;
|
|
10
|
+
import { useMockChat } from '../hooks/useMockChat'; // <--- The new brain
|
|
14
11
|
|
|
15
12
|
const AiChat: React.FC = () => {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
]);
|
|
19
|
-
const [newMessage, setNewMessage] = useState('');
|
|
20
|
-
|
|
21
|
-
const addMessage = (message: MessageData) => {
|
|
22
|
-
setMessages(prev => [...prev, message]);
|
|
23
|
-
}
|
|
13
|
+
const { messages, isLoading, sendMessage } = useMockChat();
|
|
14
|
+
const [input, setInput] = useState('');
|
|
24
15
|
|
|
25
|
-
const
|
|
16
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
26
17
|
e.preventDefault();
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
setNewMessage('');
|
|
18
|
+
sendMessage(input);
|
|
19
|
+
setInput('');
|
|
30
20
|
};
|
|
31
21
|
|
|
32
|
-
const handleSuggestionClick = (suggestion: string) => {
|
|
33
|
-
addMessage({ author: 'User', content: suggestion, isUser: true });
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
const lastMessage = messages[messages.length - 1];
|
|
38
|
-
if (lastMessage?.isUser) {
|
|
39
|
-
addMessage({ author: 'AI Assistant', loading: true });
|
|
40
|
-
|
|
41
|
-
setTimeout(() => {
|
|
42
|
-
setMessages(prev => {
|
|
43
|
-
const newMessages = [...prev];
|
|
44
|
-
newMessages[newMessages.length - 1] = {
|
|
45
|
-
author: 'AI Assistant',
|
|
46
|
-
content: "That's a great idea! I am a mock AI, but I can certainly help you plan that. What components should be on the dashboard?",
|
|
47
|
-
suggestions: ["Stat Cards", "A Bar Chart", "A Data Table"]
|
|
48
|
-
};
|
|
49
|
-
return newMessages;
|
|
50
|
-
});
|
|
51
|
-
}, 1500);
|
|
52
|
-
}
|
|
53
|
-
}, [messages]);
|
|
54
|
-
|
|
55
22
|
return (
|
|
56
|
-
<div className="p-4 sm:p-6 lg:p-8 space-y-8">
|
|
23
|
+
<div className="p-4 sm:p-6 lg:p-8 space-y-8 h-[calc(100vh-64px)] flex flex-col">
|
|
57
24
|
<PageHeader
|
|
58
25
|
title="AI Assistant"
|
|
59
|
-
description="
|
|
26
|
+
description="Full-screen command center for Ramme AI."
|
|
60
27
|
/>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
28
|
+
|
|
29
|
+
{/* We use flex-1 on the Card to make it fill the remaining screen space
|
|
30
|
+
perfectly, creating that immersive "ChatGPT" feel.
|
|
31
|
+
*/}
|
|
32
|
+
<Card className="flex-1 flex flex-col min-h-0 shadow-sm border-border/50">
|
|
33
|
+
<div className="flex-1 overflow-hidden">
|
|
34
|
+
<Conversation>
|
|
35
|
+
{messages.map((msg) => (
|
|
36
|
+
<Message
|
|
37
|
+
key={msg.id}
|
|
38
|
+
author={msg.author}
|
|
39
|
+
content={msg.content}
|
|
40
|
+
isUser={msg.isUser}
|
|
41
|
+
loading={msg.loading}
|
|
42
|
+
suggestions={msg.suggestions}
|
|
43
|
+
onSuggestionClick={(s) => sendMessage(s)}
|
|
44
|
+
/>
|
|
45
|
+
))}
|
|
46
|
+
{isLoading && <Message author="Bodewell AI" isUser={false} loading />}
|
|
47
|
+
</Conversation>
|
|
48
|
+
</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
|
+
/>
|
|
57
|
+
</div>
|
|
72
58
|
</Card>
|
|
73
59
|
</div>
|
|
74
60
|
);
|
|
@@ -7,14 +7,23 @@ import {
|
|
|
7
7
|
StatCard,
|
|
8
8
|
DataTable,
|
|
9
9
|
useDataFetch,
|
|
10
|
+
DeviceCard, // <-- NEW: Import DeviceCard
|
|
10
11
|
type ColDef,
|
|
11
12
|
type ValueFormatterParams,
|
|
12
13
|
} from '@ramme-io/ui';
|
|
14
|
+
import { useSignal } from '../hooks/useSignal'; // <-- NEW: Import the Engine
|
|
13
15
|
import { mockTableData, mockChartData } from '../data/mockData';
|
|
14
16
|
|
|
15
17
|
const Dashboard: React.FC = () => {
|
|
18
|
+
// 1. Existing Logic (SaaS Data)
|
|
16
19
|
const { data: fetchedTableData } = useDataFetch(null, mockTableData);
|
|
17
20
|
|
|
21
|
+
// 2. New Logic (IoT Engine)
|
|
22
|
+
const temp = useSignal('living_room_ac', { initialValue: 72, min: 68, max: 76, unit: '°F' });
|
|
23
|
+
const hum = useSignal('living_room_hum', { initialValue: 45, min: 40, max: 60, unit: '%' });
|
|
24
|
+
const cpu = useSignal('server_01', { initialValue: 42, min: 10, max: 95, unit: '%' });
|
|
25
|
+
|
|
26
|
+
// 3. Table Config
|
|
18
27
|
const columnDefs: ColDef[] = [
|
|
19
28
|
{ field: 'make', headerName: 'Make', sortable: true, filter: true },
|
|
20
29
|
{ field: 'model', headerName: 'Model', sortable: true, filter: true },
|
|
@@ -33,43 +42,79 @@ const Dashboard: React.FC = () => {
|
|
|
33
42
|
return (
|
|
34
43
|
<div className="space-y-8">
|
|
35
44
|
<PageHeader
|
|
36
|
-
title="
|
|
37
|
-
description="
|
|
45
|
+
title="Command Center"
|
|
46
|
+
description="Real-time device monitoring and business analytics."
|
|
38
47
|
/>
|
|
39
48
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
49
|
+
{/* --- SECTION 1: LIVE IOT STATUS (The New Stuff) --- */}
|
|
50
|
+
<div>
|
|
51
|
+
<h3 className="text-lg font-semibold mb-4 text-foreground">Live Device Status</h3>
|
|
52
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
53
|
+
<DeviceCard
|
|
54
|
+
title="Living Room AC"
|
|
55
|
+
description="Zone A • Floor 1"
|
|
56
|
+
icon="thermometer"
|
|
57
|
+
status="online"
|
|
58
|
+
value={`${temp.value}${temp.unit}`}
|
|
59
|
+
trend="Cooling to 70°"
|
|
60
|
+
/>
|
|
61
|
+
<DeviceCard
|
|
62
|
+
title="Air Quality"
|
|
63
|
+
description="Sensor ID: #8842"
|
|
64
|
+
icon="droplets"
|
|
65
|
+
status="active"
|
|
66
|
+
value={`${hum.value}${hum.unit}`}
|
|
67
|
+
trend="Stable"
|
|
68
|
+
/>
|
|
69
|
+
<DeviceCard
|
|
70
|
+
title="Main Server"
|
|
71
|
+
description="192.168.1.42"
|
|
72
|
+
icon="server"
|
|
73
|
+
status={cpu.value > 90 ? 'error' : 'online'}
|
|
74
|
+
value={`${cpu.value}${cpu.unit}`}
|
|
75
|
+
trend="CPU Load"
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* --- SECTION 2: BUSINESS METRICS (The Old Stuff) --- */}
|
|
81
|
+
<div>
|
|
82
|
+
<h3 className="text-lg font-semibold mb-4 text-foreground">Business Overview</h3>
|
|
83
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
84
|
+
<StatCard
|
|
85
|
+
title="Total Users"
|
|
86
|
+
value="1,234"
|
|
87
|
+
icon="users"
|
|
88
|
+
changeText="+10% from last month"
|
|
89
|
+
changeDirection="positive"
|
|
90
|
+
/>
|
|
91
|
+
<StatCard
|
|
92
|
+
title="Sales Today"
|
|
93
|
+
value="$5,678"
|
|
94
|
+
icon="dollar-sign"
|
|
95
|
+
changeText="+5% from yesterday"
|
|
96
|
+
changeDirection="positive"
|
|
97
|
+
/>
|
|
98
|
+
<StatCard
|
|
99
|
+
title="New Orders"
|
|
100
|
+
value="89"
|
|
101
|
+
icon="shopping-cart"
|
|
102
|
+
changeText="-2 since last hour"
|
|
103
|
+
changeDirection="negative"
|
|
104
|
+
/>
|
|
105
|
+
<StatCard
|
|
106
|
+
title="Active Projects"
|
|
107
|
+
value="12"
|
|
108
|
+
icon="briefcase"
|
|
109
|
+
footerText="3 nearing completion"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
68
112
|
</div>
|
|
69
113
|
|
|
114
|
+
{/* --- SECTION 3: ANALYTICS CHARTS --- */}
|
|
70
115
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
71
116
|
<Card className="p-6">
|
|
72
|
-
<h3 className="text-lg font-semibold mb-4 text-
|
|
117
|
+
<h3 className="text-lg font-semibold mb-4 text-foreground">Monthly Revenue</h3>
|
|
73
118
|
<div className="h-[350px] w-full">
|
|
74
119
|
<BarChart
|
|
75
120
|
data={mockChartData}
|
|
@@ -79,7 +124,7 @@ const Dashboard: React.FC = () => {
|
|
|
79
124
|
</div>
|
|
80
125
|
</Card>
|
|
81
126
|
<Card className="p-6">
|
|
82
|
-
<h3 className="text-lg font-semibold mb-4 text-
|
|
127
|
+
<h3 className="text-lg font-semibold mb-4 text-foreground">User Growth</h3>
|
|
83
128
|
<div className="h-[350px] w-full">
|
|
84
129
|
<LineChart
|
|
85
130
|
data={mockChartData}
|
|
@@ -90,8 +135,9 @@ const Dashboard: React.FC = () => {
|
|
|
90
135
|
</Card>
|
|
91
136
|
</div>
|
|
92
137
|
|
|
138
|
+
{/* --- SECTION 4: DATA TABLE --- */}
|
|
93
139
|
<Card className="p-6">
|
|
94
|
-
<h3 className="text-2xl font-semibold mb-4 text-
|
|
140
|
+
<h3 className="text-2xl font-semibold mb-4 text-foreground">Recent Vehicle Data</h3>
|
|
95
141
|
{fetchedTableData && (
|
|
96
142
|
<DataTable
|
|
97
143
|
rowData={fetchedTableData}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
import React from 'react';
|
|
1
|
+
import React, { useState } from 'react'; // <-- Added useState
|
|
3
2
|
import { Outlet, NavLink } from 'react-router-dom';
|
|
4
3
|
import {
|
|
5
4
|
Sidebar,
|
|
@@ -12,11 +11,13 @@ import {
|
|
|
12
11
|
useSidebar,
|
|
13
12
|
Button,
|
|
14
13
|
Icon,
|
|
14
|
+
ChatFAB, // <-- NEW: Import FAB
|
|
15
15
|
} from '@ramme-io/ui';
|
|
16
16
|
import { dashboardSitemap } from './dashboard.sitemap';
|
|
17
17
|
import { SitemapProvider } from '../../contexts/SitemapContext';
|
|
18
18
|
import PageTitleUpdater from '../../components/PageTitleUpdater';
|
|
19
19
|
import AppHeader from '../../components/AppHeader';
|
|
20
|
+
import { AIChatWidget } from '../../components/AIChatWidget'; // <-- NEW: Import Widget
|
|
20
21
|
|
|
21
22
|
// NavLink wrapper - Correct
|
|
22
23
|
const SidebarNavLink = React.forwardRef<HTMLAnchorElement, any>(
|
|
@@ -51,8 +52,6 @@ const AppSidebarContent: React.FC = () => {
|
|
|
51
52
|
<SidebarMenuItem
|
|
52
53
|
as={SidebarNavLink}
|
|
53
54
|
href={item.path ? `/dashboard/${item.path}` : '/dashboard'}
|
|
54
|
-
// --- THIS IS THE FIX ---
|
|
55
|
-
// Force exact match for all parent links
|
|
56
55
|
end
|
|
57
56
|
icon={item.icon ? <Icon name={item.icon} /> : undefined}
|
|
58
57
|
tooltip={item.title}
|
|
@@ -66,12 +65,10 @@ const AppSidebarContent: React.FC = () => {
|
|
|
66
65
|
key={child.id}
|
|
67
66
|
as={SidebarNavLink}
|
|
68
67
|
href={`/dashboard/${item.path}/${child.path}`}
|
|
69
|
-
// --- THIS IS THE FIX ---
|
|
70
|
-
// 'end' was already here, which is correct
|
|
71
68
|
end
|
|
72
69
|
icon={child.icon ? <Icon name={child.icon} /> : undefined}
|
|
73
70
|
tooltip={child.title}
|
|
74
|
-
className="pl-10"
|
|
71
|
+
className="pl-10"
|
|
75
72
|
>
|
|
76
73
|
{child.title}
|
|
77
74
|
</SidebarMenuItem>
|
|
@@ -88,11 +85,14 @@ const AppSidebarContent: React.FC = () => {
|
|
|
88
85
|
|
|
89
86
|
// Main Layout Component
|
|
90
87
|
const DashboardLayout: React.FC = () => {
|
|
88
|
+
// 1. STATE: Track if the chat window is open
|
|
89
|
+
const [isChatOpen, setIsChatOpen] = useState(false);
|
|
90
|
+
|
|
91
91
|
return (
|
|
92
92
|
<SitemapProvider value={dashboardSitemap}>
|
|
93
93
|
<PageTitleUpdater />
|
|
94
94
|
<SidebarProvider>
|
|
95
|
-
<div className="flex h-screen bg-background text-foreground">
|
|
95
|
+
<div className="flex h-screen bg-background text-foreground relative">
|
|
96
96
|
<Sidebar>
|
|
97
97
|
<AppSidebarContent />
|
|
98
98
|
</Sidebar>
|
|
@@ -102,6 +102,22 @@ const DashboardLayout: React.FC = () => {
|
|
|
102
102
|
<Outlet />
|
|
103
103
|
</main>
|
|
104
104
|
</div>
|
|
105
|
+
|
|
106
|
+
{/* --- AI COPILOT SECTION --- */}
|
|
107
|
+
|
|
108
|
+
{/* 2. The Widget: Only renders when open */}
|
|
109
|
+
{isChatOpen && (
|
|
110
|
+
<AIChatWidget onClose={() => setIsChatOpen(false)} />
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{/* 3. The Button: Fixed to bottom-right */}
|
|
114
|
+
<div className="fixed bottom-6 right-6 z-50">
|
|
115
|
+
<ChatFAB
|
|
116
|
+
onClick={() => setIsChatOpen(!isChatOpen)}
|
|
117
|
+
tooltipContent={isChatOpen ? "Close Assistant" : "Open Bodewell AI"}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
105
121
|
</div>
|
|
106
122
|
</SidebarProvider>
|
|
107
123
|
</SitemapProvider>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/types/schema.ts
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
// 1. Define what a "Signal" looks like in the config (Static definition)
|
|
5
|
+
export const SignalSchema = z.object({
|
|
6
|
+
id: z.string(),
|
|
7
|
+
label: z.string(),
|
|
8
|
+
kind: z.enum(['sensor', 'setpoint', 'state']),
|
|
9
|
+
unit: z.string().optional(),
|
|
10
|
+
min: z.number().optional(),
|
|
11
|
+
max: z.number().optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// 2. Define the Application Specification
|
|
15
|
+
export const AppSpecificationSchema = z.object({
|
|
16
|
+
name: z.string(),
|
|
17
|
+
description: z.string().optional(),
|
|
18
|
+
version: z.string().default("1.0.0"),
|
|
19
|
+
theme: z.enum(['light', 'dark', 'system']).default('system'),
|
|
20
|
+
// We will add 'pages' and 'navigation' here later
|
|
21
|
+
signals: z.array(SignalSchema).default([]),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Export the Types
|
|
25
|
+
export type SignalDef = z.infer<typeof SignalSchema>;
|
|
26
|
+
export type AppSpecification = z.infer<typeof AppSpecificationSchema>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// src/types/signal.ts
|
|
2
|
+
/**
|
|
3
|
+
* @file signal.d.ts
|
|
4
|
+
* @description Defines the universal contract for "Live Data" in the Ramme ecosystem.
|
|
5
|
+
*
|
|
6
|
+
* A "Signal" is a single data point that changes over time.
|
|
7
|
+
* unlike a "Record" (which is static database row), a Signal is ephemeral.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type SignalStatus = 'fresh' | 'stale' | 'disconnected' | 'error';
|
|
11
|
+
|
|
12
|
+
export interface Signal<T = any> {
|
|
13
|
+
id: string; // The unique ID (e.g., "temp_01")
|
|
14
|
+
value: T; // The actual data (e.g., 24.5)
|
|
15
|
+
unit?: string; // Optional unit (e.g., "°C")
|
|
16
|
+
timestamp: number; // Unix timestamp of last update
|
|
17
|
+
status: SignalStatus;
|
|
18
|
+
meta?: Record<string, any>; // Extra metadata (e.g., limits)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// A simple map for looking up signals by ID
|
|
22
|
+
export type SignalMap = Record<string, Signal>;
|