@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.
@@ -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, useEffect } from 'react';
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 [messages, setMessages] = useState<MessageData[]>([
17
- { author: 'AI Assistant', content: 'Hello! How can I help you build your prototype today?' },
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 handleSendMessage = (e: React.FormEvent) => {
16
+ const handleSubmit = (e: React.FormEvent) => {
26
17
  e.preventDefault();
27
- if (newMessage.trim() === '') return;
28
- addMessage({ author: 'User', content: newMessage, isUser: true });
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="This is the foundational interface for the Ramme AI prototyping assistant."
26
+ description="Full-screen command center for Ramme AI."
60
27
  />
61
- <Card className="flex flex-col h-[600px]">
62
- <Conversation>
63
- {messages.map((msg, index) => (
64
- <Message key={index} {...msg} onSuggestionClick={handleSuggestionClick} />
65
- ))}
66
- </Conversation>
67
- <PromptInput
68
- value={newMessage}
69
- onChange={(e) => setNewMessage(e.target.value)}
70
- onSubmit={handleSendMessage}
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="Dashboard Overview"
37
- description="A summary of key metrics and recent activity."
45
+ title="Command Center"
46
+ description="Real-time device monitoring and business analytics."
38
47
  />
39
48
 
40
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
41
- <StatCard
42
- title="Total Users"
43
- value="1,234"
44
- icon="users"
45
- changeText="+10% from last month"
46
- changeDirection="positive"
47
- />
48
- <StatCard
49
- title="Sales Today"
50
- value="$5,678"
51
- icon="dollar-sign"
52
- changeText="+5% from yesterday"
53
- changeDirection="positive"
54
- />
55
- <StatCard
56
- title="New Orders"
57
- value="89"
58
- icon="shopping-cart"
59
- changeText="-2 since last hour"
60
- changeDirection="negative"
61
- />
62
- <StatCard
63
- title="Active Projects"
64
- value="12"
65
- icon="briefcase"
66
- footerText="3 nearing completion"
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-text-foreground">Monthly Revenue</h3>
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-text-foreground">User Growth</h3>
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-text">Recent Vehicle Data</h3>
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
- // ramme-app-starter/template/src/templates/dashboard/DashboardLayout.tsx
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" // Indent child items
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>;