@ramme-io/create-app 1.0.5 → 1.1.2
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 +2 -2
- package/template/package-lock.json +11 -2838
- package/template/pkg.json +1 -1
- package/template/pnpm-lock.yaml +1790 -0
- package/template/src/adaptors/.gitkeep +0 -0
- package/template/src/components/AIChatWidget.tsx +69 -0
- package/template/src/components/AppHeader.tsx +1 -1
- package/template/src/config/app.manifest.ts +48 -79
- package/template/src/config/navigation.ts +86 -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/templates/settings/SettingsLayout.tsx +1 -1
- package/template/src/types/schema.ts +88 -0
- package/template/src/types/signal.ts +22 -0
|
File without changes
|
|
@@ -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
|
+
};
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
import { useAuth } from '../contexts/AuthContext';
|
|
23
23
|
|
|
24
24
|
// --- STRATEGIC IMPORTS ---
|
|
25
|
-
import { appManifest } from '../config/
|
|
25
|
+
import { appManifest } from '../config/navigation';
|
|
26
26
|
import type { ManifestLink } from '../core/manifest-types';
|
|
27
27
|
import TemplateSwitcher from './TemplateSwitcher';
|
|
28
28
|
import rammeLogo from '../assets/orange.png'; // <-- 1. IMPORT THE LOGO
|
|
@@ -1,86 +1,55 @@
|
|
|
1
|
+
import type { AppSpecification } from '../types/schema';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* This is the
|
|
6
|
-
*
|
|
4
|
+
* ------------------------------------------------------------------
|
|
5
|
+
* THE APP MANIFEST (The "Brain")
|
|
6
|
+
* ------------------------------------------------------------------
|
|
7
|
+
* This file is the Single Source of Truth for the application's
|
|
8
|
+
* domain logic. It defines the signals (data), entities (things),
|
|
9
|
+
* and global configuration.
|
|
10
|
+
* * In the "App Builder" workflow, this file is programmatically
|
|
11
|
+
* generated by the AI Assistant.
|
|
7
12
|
*/
|
|
8
|
-
import type { ManifestLink } from '../core/manifest-types';
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
export const appManifest: AppSpecification = {
|
|
15
|
+
meta: {
|
|
16
|
+
name: "My Ramme App",
|
|
17
|
+
version: "0.1.0",
|
|
18
|
+
description: "A prototype application generated by Ramme.",
|
|
19
|
+
author: "Ramme User",
|
|
20
|
+
createdAt: new Date().toISOString(),
|
|
21
|
+
},
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
{
|
|
31
|
-
id: 'user.settings',
|
|
32
|
-
// --- FIX: Point to the settings layout base ---
|
|
33
|
-
path: '/settings', // Correct path
|
|
34
|
-
title: 'Settings',
|
|
35
|
-
icon: 'settings',
|
|
36
|
-
},
|
|
37
|
-
// --- REMOVED: Logout link is redundant ---
|
|
38
|
-
// {
|
|
39
|
-
// id: 'user.logout',
|
|
40
|
-
// path: '/login', // Was pointing here, but onClick is better
|
|
41
|
-
// title: 'Log Out',
|
|
42
|
-
// icon: 'logOut',
|
|
43
|
-
// },
|
|
44
|
-
],
|
|
23
|
+
// The Data Layer
|
|
24
|
+
domain: {
|
|
25
|
+
// 1. Signals: The raw data points (Sensors, Statuses, Commands)
|
|
26
|
+
signals: [
|
|
27
|
+
// Example placeholder:
|
|
28
|
+
// {
|
|
29
|
+
// id: 'temp_01',
|
|
30
|
+
// label: 'Living Room Temp',
|
|
31
|
+
// kind: 'sensor',
|
|
32
|
+
// source: 'mock',
|
|
33
|
+
// unit: '°C',
|
|
34
|
+
// defaultValue: 22
|
|
35
|
+
// }
|
|
36
|
+
],
|
|
45
37
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
{
|
|
58
|
-
id: 'settings.billing',
|
|
59
|
-
path: '/settings/billing',
|
|
60
|
-
title: 'Billing',
|
|
61
|
-
icon: 'creditCard',
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
id: 'settings.team',
|
|
65
|
-
path: '/settings/team',
|
|
66
|
-
title: 'Team Members',
|
|
67
|
-
icon: 'users',
|
|
68
|
-
},
|
|
69
|
-
],
|
|
38
|
+
// 2. Entities: The logical groupings (Devices, Rooms, Systems)
|
|
39
|
+
entities: [
|
|
40
|
+
// Example placeholder:
|
|
41
|
+
// {
|
|
42
|
+
// id: 'dev_thermostat',
|
|
43
|
+
// name: 'Smart Thermostat',
|
|
44
|
+
// type: 'device',
|
|
45
|
+
// signals: ['temp_01']
|
|
46
|
+
// }
|
|
47
|
+
],
|
|
48
|
+
},
|
|
70
49
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
id: 'footer.help',
|
|
77
|
-
path: '/help',
|
|
78
|
-
title: 'Help Center',
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
id: 'footer.terms',
|
|
82
|
-
path: '/terms',
|
|
83
|
-
title: 'Terms of Service',
|
|
84
|
-
},
|
|
85
|
-
],
|
|
50
|
+
// Global Config
|
|
51
|
+
config: {
|
|
52
|
+
theme: 'system',
|
|
53
|
+
mockMode: true, // Default to "Mock Mode" for instant prototyping
|
|
54
|
+
},
|
|
86
55
|
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file navigation.ts
|
|
3
|
+
* @repository ramme-app-starter
|
|
4
|
+
* @description
|
|
5
|
+
* This is the central manifest for all GLOBAL, shared navigation elements.
|
|
6
|
+
* Corrected paths to point within the /settings layout.
|
|
7
|
+
*/
|
|
8
|
+
import type { ManifestLink } from '../core/manifest-types';
|
|
9
|
+
|
|
10
|
+
interface AppManifest {
|
|
11
|
+
userMenu: ManifestLink[];
|
|
12
|
+
settingsMenu: ManifestLink[];
|
|
13
|
+
footerLinks: ManifestLink[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const appManifest: AppManifest = {
|
|
17
|
+
/**
|
|
18
|
+
* Used for the user dropdown menu in the AppHeader.
|
|
19
|
+
* NOTE: Logout is handled by the onClick={logout} in AppHeader,
|
|
20
|
+
* so it's removed from this manifest.
|
|
21
|
+
*/
|
|
22
|
+
userMenu: [
|
|
23
|
+
{
|
|
24
|
+
id: 'user.profile',
|
|
25
|
+
// --- FIX: Point to the settings layout ---
|
|
26
|
+
path: '/settings/profile', // Correct path
|
|
27
|
+
title: 'Your Profile',
|
|
28
|
+
icon: 'user',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'user.settings',
|
|
32
|
+
// --- FIX: Point to the settings layout base ---
|
|
33
|
+
path: '/settings', // Correct path
|
|
34
|
+
title: 'Settings',
|
|
35
|
+
icon: 'settings',
|
|
36
|
+
},
|
|
37
|
+
// --- REMOVED: Logout link is redundant ---
|
|
38
|
+
// {
|
|
39
|
+
// id: 'user.logout',
|
|
40
|
+
// path: '/login', // Was pointing here, but onClick is better
|
|
41
|
+
// title: 'Log Out',
|
|
42
|
+
// icon: 'logOut',
|
|
43
|
+
// },
|
|
44
|
+
],
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Used for a dedicated settings page (e.g., /settings/...)
|
|
48
|
+
* These paths were already correct.
|
|
49
|
+
*/
|
|
50
|
+
settingsMenu: [
|
|
51
|
+
{
|
|
52
|
+
id: 'settings.account',
|
|
53
|
+
path: '/settings/profile',
|
|
54
|
+
title: 'Account',
|
|
55
|
+
icon: 'user',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'settings.billing',
|
|
59
|
+
path: '/settings/billing',
|
|
60
|
+
title: 'Billing',
|
|
61
|
+
icon: 'creditCard',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'settings.team',
|
|
65
|
+
path: '/settings/team',
|
|
66
|
+
title: 'Team Members',
|
|
67
|
+
icon: 'users',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Used in the AppFooter. (Placeholders)
|
|
73
|
+
*/
|
|
74
|
+
footerLinks: [
|
|
75
|
+
{
|
|
76
|
+
id: 'footer.help',
|
|
77
|
+
path: '/help',
|
|
78
|
+
title: 'Help Center',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'footer.terms',
|
|
82
|
+
path: '/terms',
|
|
83
|
+
title: 'Terms of Service',
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
@@ -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
|
);
|