Initial Phase 1 Alpha build for app.blackroad.io
This commit establishes the foundation for the BlackRoad OS web application, the primary SaaS product for human-AI collaboration. 1. Authentication System - Login and signup pages with email/password - Zustand state management for auth - Workspace-based user model - Protected route handling 2. App Shell - Sidebar navigation (Conversations, Agents, Governance, Account) - App header with workspace and user info - Layout with authentication guards - Responsive design with Tailwind CSS 3. Conversation Interface - Chat UI with message bubbles - User and assistant message rendering - Real-time timestamp display - Loading states and animations - Placeholder for WebSocket integration 4. Core Dependencies - Next.js 16 with App Router - Zustand for state management - TanStack Query (ready for API calls) - Lucide React icons - Tailwind CSS 4 with custom utilities 5. Workspace Management - Multi-tenant workspace structure - Workspace store with Zustand - Plan-based workspace model (Free, Pro, Enterprise) Structure: app/ ├── (auth)/ # Authentication routes │ ├── login/ │ └── signup/ ├── (app)/ # Protected app routes │ ├── conversations/[id]/ │ ├── workspace/ │ └── layout.tsx └── page.tsx # Redirects to login components/ ├── Sidebar.tsx # Main navigation └── AppHeader.tsx # Workspace header stores/ ├── auth-store.ts # Authentication state └── workspace-store.ts lib/ └── cn.ts # Tailwind merge utility Next Steps: - Integrate WebSocket streaming for real-time AI responses - Connect to BlackRoad OS backend API - Add agent management interface - Build governance center UI - Deploy to app.blackroad.io Phase 1 Alpha Target: Jan 25, 2026 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
151
app/(app)/conversations/[id]/page.tsx
Normal file
151
app/(app)/conversations/[id]/page.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Send, Bot, User } from 'lucide-react';
|
||||
import { useParams } from 'next/navigation';
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export default function ConversationPage() {
|
||||
const params = useParams();
|
||||
const [messages, setMessages] = useState<Message[]>([
|
||||
{
|
||||
id: '1',
|
||||
role: 'assistant',
|
||||
content: 'Hello! I\'m Lucidia, your AI companion built on BlackRoad OS. How can I help you today?',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
const [input, setInput] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!input.trim() || isLoading) return;
|
||||
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: 'user',
|
||||
content: input,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
setInput('');
|
||||
setIsLoading(true);
|
||||
|
||||
// TODO: Replace with actual WebSocket/API call
|
||||
setTimeout(() => {
|
||||
const assistantMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: 'I received your message: "' + userMessage.content + '". This is a placeholder response. WebSocket streaming will be integrated next.',
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setMessages((prev) => [...prev, assistantMessage]);
|
||||
setIsLoading(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Messages area */}
|
||||
<div className="flex-1 overflow-y-auto px-4 py-6">
|
||||
<div className="max-w-3xl mx-auto space-y-6">
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`flex gap-4 ${
|
||||
message.role === 'user' ? 'justify-end' : 'justify-start'
|
||||
}`}
|
||||
>
|
||||
{message.role === 'assistant' && (
|
||||
<div className="flex-shrink-0 h-8 w-8 rounded-full bg-gradient-to-br from-blue-800 to-blue-600 flex items-center justify-center text-white">
|
||||
<Bot className="h-5 w-5" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`max-w-[70%] rounded-lg px-4 py-3 ${
|
||||
message.role === 'user'
|
||||
? 'bg-blue-800 text-white'
|
||||
: 'bg-white border border-gray-200 text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<p className="text-sm whitespace-pre-wrap">{message.content}</p>
|
||||
<p
|
||||
className={`text-xs mt-2 ${
|
||||
message.role === 'user' ? 'text-blue-100' : 'text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{message.timestamp.toLocaleTimeString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{message.role === 'user' && (
|
||||
<div className="flex-shrink-0 h-8 w-8 rounded-full bg-gradient-to-br from-gray-700 to-gray-500 flex items-center justify-center text-white">
|
||||
<User className="h-5 w-5" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-shrink-0 h-8 w-8 rounded-full bg-gradient-to-br from-blue-800 to-blue-600 flex items-center justify-center text-white">
|
||||
<Bot className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-lg px-4 py-3">
|
||||
<div className="flex gap-1">
|
||||
<div className="h-2 w-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
||||
<div className="h-2 w-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
||||
<div className="h-2 w-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input area */}
|
||||
<div className="border-t border-gray-200 bg-white px-4 py-4">
|
||||
<form onSubmit={handleSubmit} className="max-w-3xl mx-auto">
|
||||
<div className="flex gap-4">
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="Type your message..."
|
||||
disabled={isLoading}
|
||||
className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-800 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || !input.trim()}
|
||||
className="px-6 py-3 bg-blue-800 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
<Send className="h-5 w-5" />
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
42
app/(app)/layout.tsx
Normal file
42
app/(app)/layout.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { useWorkspaceStore } from '@/stores/workspace-store';
|
||||
import Sidebar from '@/components/Sidebar';
|
||||
import AppHeader from '@/components/AppHeader';
|
||||
|
||||
export default function AppLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||
const fetchWorkspaces = useWorkspaceStore((state) => state.fetchWorkspaces);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) {
|
||||
router.push('/login');
|
||||
} else {
|
||||
fetchWorkspaces();
|
||||
}
|
||||
}, [isAuthenticated, router, fetchWorkspaces]);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden">
|
||||
<Sidebar />
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<AppHeader />
|
||||
<main className="flex-1 overflow-y-auto bg-gray-50">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
88
app/(app)/workspace/page.tsx
Normal file
88
app/(app)/workspace/page.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { MessageSquare, Plus } from 'lucide-react';
|
||||
|
||||
interface Conversation {
|
||||
id: string;
|
||||
title: string;
|
||||
lastMessage: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export default function WorkspacePage() {
|
||||
const [conversations] = useState<Conversation[]>([
|
||||
{
|
||||
id: '1',
|
||||
title: 'Getting Started with BlackRoad OS',
|
||||
lastMessage: 'How can I help you today?',
|
||||
timestamp: '2 hours ago',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Project Planning',
|
||||
lastMessage: 'Let me summarize the key tasks...',
|
||||
timestamp: 'Yesterday',
|
||||
},
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="h-full p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-semibold text-gray-900 mb-2">
|
||||
Conversations
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
Continue your conversations with Lucidia or start a new one
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Empty state or conversation list */}
|
||||
{conversations.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-20">
|
||||
<div className="h-24 w-24 rounded-full bg-gray-100 flex items-center justify-center mb-6">
|
||||
<MessageSquare className="h-12 w-12 text-gray-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
No conversations yet
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-6 text-center max-w-md">
|
||||
Start your first conversation with Lucidia to begin collaborating on your projects
|
||||
</p>
|
||||
<button className="flex items-center gap-2 px-6 py-3 bg-blue-800 hover:bg-blue-700 text-white rounded-md font-medium transition-colors">
|
||||
<Plus className="h-5 w-5" />
|
||||
New Conversation
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{conversations.map((conversation) => (
|
||||
<div
|
||||
key={conversation.id}
|
||||
className="bg-white border border-gray-200 rounded-lg p-6 hover:border-blue-800 hover:shadow-md transition-all cursor-pointer"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-blue-800 to-blue-600 flex items-center justify-center text-white flex-shrink-0">
|
||||
<MessageSquare className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-gray-900 mb-1 truncate">
|
||||
{conversation.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 line-clamp-2 mb-2">
|
||||
{conversation.lastMessage}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{conversation.timestamp}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user