From c404a57c3baa351af3ade29d3a3e4693f61a22f2 Mon Sep 17 00:00:00 2001 From: Aseem Shrey Date: Tue, 3 Feb 2026 15:49:03 -0500 Subject: [PATCH 1/8] feat(ui): add agentic chat interface as home page - Add AgentPage with ChatGPT-style chat interface - Add AgentLayout with sidebar containing: - ShipSec AI branding with "Powered by Claude Opus" - New Chat button - Search conversations - Collapsible Studio section with nav links - Your Chats conversation history - Settings and theme toggle - Add chatStore for managing conversations with Zustand - Add user/assistant avatars to chat messages - Move existing studio routes to /studio/* path - Support light/dark theme via existing theme store Signed-off-by: Aseem Shrey --- frontend/src/App.tsx | 95 +++--- .../src/components/layout/AgentLayout.tsx | 319 ++++++++++++++++++ frontend/src/pages/AgentPage.tsx | 296 ++++++++++++++++ frontend/src/store/chatStore.ts | 102 ++++++ 4 files changed, 764 insertions(+), 48 deletions(-) create mode 100644 frontend/src/components/layout/AgentLayout.tsx create mode 100644 frontend/src/pages/AgentPage.tsx create mode 100644 frontend/src/store/chatStore.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 26139b94..d1af1eec 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,8 +12,10 @@ import { WebhookEditorPage } from '@/pages/WebhookEditorPage'; import { SchedulesPage } from '@/pages/SchedulesPage'; import { ActionCenterPage } from '@/pages/ActionCenterPage'; import { RunRedirect } from '@/pages/RunRedirect'; +import { AgentPage } from '@/pages/AgentPage'; import { ToastProvider } from '@/components/ui/toast-provider'; import { AppLayout } from '@/components/layout/AppLayout'; +import { AgentLayout } from '@/components/layout/AgentLayout'; import { AuthProvider } from '@/auth/auth-context'; import { useAuthStoreIntegration } from '@/auth/store-integration'; import { ProtectedRoute } from '@/components/auth/ProtectedRoute'; @@ -46,54 +48,51 @@ function App() { {/* Analytics wiring */} - - - - } /> - - - - } - /> - - - - } - /> - - - - } - /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } - /> - } /> - - - + + {/* Agent Page - New home page with its own layout */} + + + + } + /> + + {/* Studio routes with AppLayout */} + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } + /> + + + + } + /> + } /> + diff --git a/frontend/src/components/layout/AgentLayout.tsx b/frontend/src/components/layout/AgentLayout.tsx new file mode 100644 index 00000000..e4ae0f88 --- /dev/null +++ b/frontend/src/components/layout/AgentLayout.tsx @@ -0,0 +1,319 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Link } from 'react-router-dom'; +import { Sidebar, SidebarHeader, SidebarContent, SidebarFooter } from '@/components/ui/sidebar'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + Bot, + Plus, + Search, + MessageSquare, + Trash2, + Sun, + Moon, + Settings, + Menu, + X, + Sparkles, + Workflow, + CalendarClock, + Webhook, + Zap, + KeyRound, + Shield, + Archive, + ChevronRight, +} from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { useThemeStore } from '@/store/themeStore'; +import { useChatStore } from '@/store/chatStore'; +import { ThemeTransition } from '@/components/ui/ThemeTransition'; + +interface AgentLayoutProps { + children: React.ReactNode; +} + +function useIsMobile(breakpoint = 768) { + const [isMobile, setIsMobile] = useState( + typeof window !== 'undefined' ? window.innerWidth < breakpoint : false, + ); + + useEffect(() => { + const handleResize = () => { + setIsMobile(window.innerWidth < breakpoint); + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [breakpoint]); + + return isMobile; +} + +const studioNavItems = [ + { name: 'Workflow Builder', href: '/studio', icon: Workflow }, + { name: 'Schedules', href: '/studio/schedules', icon: CalendarClock }, + { name: 'Webhooks', href: '/studio/webhooks', icon: Webhook }, + { name: 'Action Center', href: '/studio/action-center', icon: Zap }, + { name: 'Secrets', href: '/studio/secrets', icon: KeyRound }, + { name: 'API Keys', href: '/studio/api-keys', icon: Shield }, + { name: 'Artifact Library', href: '/studio/artifacts', icon: Archive }, +]; + +export function AgentLayout({ children }: AgentLayoutProps) { + const isMobile = useIsMobile(); + const [sidebarOpen, setSidebarOpen] = useState(!isMobile); + const [searchQuery, setSearchQuery] = useState(''); + const [studioExpanded, setStudioExpanded] = useState(false); + const { theme, startTransition } = useThemeStore(); + + const { + conversations, + activeConversationId, + setActiveConversation, + createConversation, + deleteConversation, + } = useChatStore(); + + // Update sidebar state based on mobile + useEffect(() => { + setSidebarOpen(!isMobile); + }, [isMobile]); + + const handleNewChat = useCallback(() => { + createConversation(); + if (isMobile) { + setSidebarOpen(false); + } + }, [createConversation, isMobile]); + + const handleSelectConversation = useCallback( + (id: string) => { + setActiveConversation(id); + if (isMobile) { + setSidebarOpen(false); + } + }, + [setActiveConversation, isMobile], + ); + + const handleDeleteConversation = useCallback( + (e: React.MouseEvent, id: string) => { + e.stopPropagation(); + deleteConversation(id); + }, + [deleteConversation], + ); + + const filteredConversations = conversations.filter((conv) => + conv.title.toLowerCase().includes(searchQuery.toLowerCase()), + ); + + return ( + <> + +
+ {/* Mobile backdrop */} + {isMobile && sidebarOpen && ( +
setSidebarOpen(false)} + /> + )} + + {/* Sidebar */} + + {/* Header with logo and agent info */} + +
+ +
+ ShipSec { + e.currentTarget.style.display = 'none'; + }} + /> +
+
+
+ ShipSec AI + + + Powered by Claude Opus + +
+ + {isMobile && ( + + )} +
+ + {/* New chat button */} + + + + + {/* Search bar */} +
+ + setSearchQuery(e.target.value)} + className="pl-9 h-9 text-sm" + /> +
+ + {/* Studio Navigation - Collapsible */} +
+ + {studioExpanded && ( +
+ {studioNavItems.map((item) => { + const Icon = item.icon; + return ( + isMobile && setSidebarOpen(false)} + > + + {item.name} + + ); + })} +
+ )} +
+ + {/* Conversation history */} +
+

Your Chats

+ {filteredConversations.length === 0 ? ( +
+ +

No conversations yet

+

Start a new chat to begin

+
+ ) : ( + filteredConversations.map((conv) => ( + + + )) + )} +
+
+ + +
+ + +
+
+ ShipSec AI v1.0.0 | Claude Opus 4.5 +
+
+ + + {/* Main content */} +
+ {/* Mobile header */} + {isMobile && ( +
+ +
+ ShipSec { + e.currentTarget.style.display = 'none'; + }} + /> + ShipSec AI +
+
+ )} +
{children}
+
+
+ + ); +} diff --git a/frontend/src/pages/AgentPage.tsx b/frontend/src/pages/AgentPage.tsx new file mode 100644 index 00000000..5ef78f84 --- /dev/null +++ b/frontend/src/pages/AgentPage.tsx @@ -0,0 +1,296 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Send, Sparkles, Workflow, Shield, FileSearch, Zap, User } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { cn } from '@/lib/utils'; +import { useChatStore, type ChatMessage } from '@/store/chatStore'; +import { useAuthProvider } from '@/auth/auth-context'; + +const suggestedActions = [ + { + icon: Workflow, + label: 'Run workflow', + description: 'Execute a security workflow', + }, + { + icon: Shield, + label: 'Scan repository', + description: 'Analyze code for vulnerabilities', + }, + { + icon: FileSearch, + label: 'Review findings', + description: 'Check recent security findings', + }, + { + icon: Zap, + label: 'Quick actions', + description: 'Common security tasks', + }, +]; + +interface MessageBubbleProps { + message: ChatMessage; + userImageUrl?: string; + userInitials?: string; +} + +function MessageBubble({ message, userImageUrl, userInitials }: MessageBubbleProps) { + const isUser = message.role === 'user'; + + return ( +
+ {/* Assistant avatar (left side) */} + {!isUser && ( + + + AI + + )} + +
+

{message.content}

+ + {message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + +
+ + {/* User avatar (right side) */} + {isUser && ( + + + + {userInitials || } + + + )} +
+ ); +} + +function WelcomeScreen({ onSuggestedAction }: { onSuggestedAction: (action: string) => void }) { + return ( +
+ {/* Logo and branding */} +
+
+ ShipSec { + e.currentTarget.style.display = 'none'; + }} + /> +
+

ShipSec AI Agent

+

Your intelligent security assistant

+
+
+
+ + Powered by Claude Opus +
+
+ + {/* Suggested actions */} +
+ {suggestedActions.map((action) => { + const Icon = action.icon; + return ( + + ); + })} +
+ + {/* Metadata footer */} +
+

+ ShipSec AI can help you with security workflows, code scanning, and vulnerability + management. +

+

+ Model: claude-opus-4-5-20251101 | Context: 200K tokens +

+
+
+ ); +} + +export function AgentPage() { + const [inputValue, setInputValue] = useState(''); + const messagesEndRef = useRef(null); + const textareaRef = useRef(null); + + const authProvider = useAuthProvider(); + const { user } = authProvider.context; + + // Get user avatar info + const userImageUrl = user?.imageUrl; + const userInitials = + user?.firstName && user?.lastName + ? `${user.firstName[0]}${user.lastName[0]}` + : user?.username + ? user.username.substring(0, 2).toUpperCase() + : user?.email + ? user.email.substring(0, 2).toUpperCase() + : undefined; + + const { activeConversationId, createConversation, addMessage, getActiveConversation } = + useChatStore(); + + const activeConversation = getActiveConversation(); + const messages = activeConversation?.messages || []; + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + // Auto-resize textarea + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 200)}px`; + } + }, [inputValue]); + + const handleSend = () => { + if (!inputValue.trim()) return; + + let conversationId = activeConversationId; + if (!conversationId) { + conversationId = createConversation(); + } + + // Add user message + addMessage(conversationId, { + role: 'user', + content: inputValue.trim(), + }); + + // Simulate agent response (echo "hello" back) + const userMessage = inputValue.trim().toLowerCase(); + setTimeout(() => { + let response = + "Hello! I'm the ShipSec AI Agent. How can I assist you with your security workflows today?"; + + if ( + userMessage.includes('hello') || + userMessage.includes('hi') || + userMessage.includes('hey') + ) { + response = + "Hello! I'm ready to help you with security workflows, vulnerability scanning, and code analysis. What would you like to do?"; + } else if (userMessage.includes('run workflow')) { + response = + 'I can help you run a security workflow. Which workflow would you like to execute? You can choose from:\n\n• Code Security Scan\n• Dependency Audit\n• Infrastructure Review\n• Compliance Check'; + } else if (userMessage.includes('scan')) { + response = + "I'll initiate a security scan for you. Please provide the repository URL or select from your connected repositories."; + } + + addMessage(conversationId!, { + role: 'assistant', + content: response, + }); + }, 500); + + setInputValue(''); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + const handleSuggestedAction = (action: string) => { + setInputValue(action); + // Focus the input + textareaRef.current?.focus(); + }; + + return ( +
+ {/* Messages area */} +
+ {messages.length === 0 ? ( + + ) : ( +
+ {messages.map((message) => ( + + ))} +
+
+ )} +
+ + {/* Input area */} +
+
+
+
+