From cf565df13e7074599e863502a32d3abdcb7acbd4 Mon Sep 17 00:00:00 2001 From: Ra Date: Wed, 20 Aug 2025 03:30:04 -0700 Subject: [PATCH] feat: major UI overhaul with new components and enhanced UX - Add comprehensive Company Wiki feature with complete state management - CompanyWikiManager, empty states, invite modals - Implement new Chat system with enhanced layout and components - ChatLayout, ChatSidebar, MessageThread, FileUploadInput - Create modern Login and OTP verification flows - LoginNew page, OTPVerification component - Add new Employee Forms system with enhanced controller - Introduce Figma-based design components and multiple choice inputs - Add new font assets (NeueMontreal) and robot images for onboarding - Enhance existing components with improved styling and functionality - Update build configuration and dependencies - Remove deprecated ModernLogin component --- .gitignore | 5 +- App.tsx | 56 +- OnboardingQuestions.ts | 0 .../CompanyWiki/CompanyWikiCompletedState.tsx | 150 ++ .../CompanyWiki/CompanyWikiEmptyState.tsx | 119 ++ .../CompanyWiki/CompanyWikiEmptyStateDark.tsx | 110 ++ components/CompanyWiki/CompanyWikiManager.tsx | 116 ++ .../CompanyWiki/InviteEmployeesModal.tsx | 92 + .../InviteMultipleEmployeesModal.tsx | 178 ++ components/CompanyWiki/index.ts | 6 + components/UiKit.tsx | 21 +- components/chat/ChatEmptyState.tsx | 180 ++ components/chat/ChatLayout.tsx | 171 ++ components/chat/ChatSidebar.tsx | 263 +++ components/chat/FileUploadInput.tsx | 244 +++ components/chat/MessageThread.tsx | 118 ++ components/chat/index.ts | 5 + components/figma/EnhancedFigmaQuestion.tsx | 281 ++- components/figma/FigmaInput.tsx | 98 +- components/figma/FigmaMultipleChoice.tsx | 42 + components/figma/FigmaQuestion.tsx | 153 +- components/figma/Sidebar.tsx | 241 +++ constants.ts | 6 +- functions/bun.lock | 42 +- functions/index.js | 400 ++--- functions/package.json | 4 +- index.css | 78 +- pages/Chat.tsx | 165 +- pages/ChatNew.tsx | 390 ++++ pages/CompanyWiki.tsx | 344 +--- pages/EmployeeFormNew.tsx | 101 ++ pages/EmployeeFormsController.tsx | 1595 +++++++++++++++++ pages/HelpNew.tsx | 133 ++ pages/Login.tsx | 561 +++--- pages/LoginNew.tsx | 90 + pages/ModernLogin.tsx | 403 ----- pages/OTPVerification.tsx | 122 ++ pages/Onboarding.tsx | 743 ++------ pages/SettingsNew.tsx | 304 ++++ pages/figma/ChatAIResponse.tsx | 223 +++ pages/figma/ChatLight.tsx | 256 +++ public/fonts/NeueMontreal-Regular.woff | Bin 0 -> 28568 bytes public/fonts/NeueMontreal-Regular.woff2 | Bin 0 -> 20968 bytes public/fonts/image/login-robot.png | Bin 0 -> 2299511 bytes public/fonts/image/onboarding-robot.png | Bin 0 -> 283398 bytes tailwind.config.js | 49 +- vite.config.ts | 3 +- 47 files changed, 6654 insertions(+), 2007 deletions(-) create mode 100644 OnboardingQuestions.ts create mode 100644 components/CompanyWiki/CompanyWikiCompletedState.tsx create mode 100644 components/CompanyWiki/CompanyWikiEmptyState.tsx create mode 100644 components/CompanyWiki/CompanyWikiEmptyStateDark.tsx create mode 100644 components/CompanyWiki/CompanyWikiManager.tsx create mode 100644 components/CompanyWiki/InviteEmployeesModal.tsx create mode 100644 components/CompanyWiki/InviteMultipleEmployeesModal.tsx create mode 100644 components/CompanyWiki/index.ts create mode 100644 components/chat/ChatEmptyState.tsx create mode 100644 components/chat/ChatLayout.tsx create mode 100644 components/chat/ChatSidebar.tsx create mode 100644 components/chat/FileUploadInput.tsx create mode 100644 components/chat/MessageThread.tsx create mode 100644 components/chat/index.ts create mode 100644 components/figma/FigmaMultipleChoice.tsx create mode 100644 components/figma/Sidebar.tsx create mode 100644 pages/ChatNew.tsx create mode 100644 pages/EmployeeFormNew.tsx create mode 100644 pages/EmployeeFormsController.tsx create mode 100644 pages/HelpNew.tsx create mode 100644 pages/LoginNew.tsx delete mode 100644 pages/ModernLogin.tsx create mode 100644 pages/OTPVerification.tsx create mode 100644 pages/SettingsNew.tsx create mode 100644 pages/figma/ChatAIResponse.tsx create mode 100644 pages/figma/ChatLight.tsx create mode 100644 public/fonts/NeueMontreal-Regular.woff create mode 100644 public/fonts/NeueMontreal-Regular.woff2 create mode 100644 public/fonts/image/login-robot.png create mode 100644 public/fonts/image/onboarding-robot.png diff --git a/.gitignore b/.gitignore index 475cbd1..ddcd942 100644 --- a/.gitignore +++ b/.gitignore @@ -55,4 +55,7 @@ dist-ssr /debug_firebase.js /schema.json /SECURITY_FIXES.md -/.claude \ No newline at end of file +/.claude +/deprecated +/figma-code +/*ignore.* \ No newline at end of file diff --git a/App.tsx b/App.tsx index cb6baeb..21bfb55 100644 --- a/App.tsx +++ b/App.tsx @@ -8,9 +8,11 @@ import { Layout } from './components/UiKit'; import CompanyWiki from './pages/CompanyWiki'; import EmployeeData from './pages/EmployeeData'; import Chat from './pages/Chat'; +import ChatNew from './pages/ChatNew'; +import HelpNew from './pages/HelpNew'; +import SettingsNew from './pages/SettingsNew'; import HelpAndSettings from './pages/HelpAndSettings'; -import Login from './pages/Login'; -import ModernLogin from './pages/ModernLogin'; +import ModernLogin from './pages/Login'; import OrgSelection from './pages/OrgSelection'; import Onboarding from './pages/Onboarding'; import EmployeeQuestionnaire from './pages/EmployeeQuestionnaire'; @@ -20,7 +22,6 @@ import FormsDashboard from './pages/FormsDashboard'; import DebugEmployee from './pages/DebugEmployee'; import QuestionnaireComplete from './pages/QuestionnaireComplete'; import SubscriptionSetup from './pages/SubscriptionSetup'; -import { isFirebaseConfigured } from './services/firebase'; const RequireAuth: React.FC<{ children: React.ReactNode }> = ({ children }) => { const { user, loading } = useAuth(); @@ -112,7 +113,6 @@ function App() { } /> } /> } /> - } /> {/* Employee questionnaire - no auth needed, uses invite code */} } /> @@ -180,6 +180,54 @@ function App() { } /> + {/* New Figma Chat Implementation - Standalone route */} + + + + + + + + + + } + /> + + {/* New Figma Help Implementation - Standalone route */} + + + + + + + + + + } + /> + + {/* New Figma Settings Implementation - Standalone route */} + + + + + + + + + + } + /> + {/* Main app routes - require auth, org selection, and completed onboarding */} void; + onInviteEmployees?: () => void; + onCopyLink?: () => void; +} + +const defaultQAItems: QAItem[] = [ + { + question: "What is the mission of your company?", + answer: "To empower small businesses with AI-driven automation tools that increase efficiency and reduce operational overhead." + }, + { + question: "How has your mission evolved in the last 1–3 years?", + answer: "We shifted from general SaaS tools to vertical-specific solutions, with deeper integrations and onboarding support." + }, + { + question: "What is your 5-year vision for the company?", + answer: "To become the leading AI operations platform for SMBs in North America, serving over 100,000 customers." + }, + { + question: "What are your company's top 3 strategic advantages?", + answer: "Fast product iteration enabled by in-house AI capabilities\nDeep customer understanding from vertical specialization\nHigh customer retention due to integrated onboarding" + }, + { + question: "What are your biggest vulnerabilities or threats?", + answer: "Dependence on a single marketing channel, weak middle management, and rising customer acquisition costs." + } +]; + +const sections = [ + "Company Overview & Vision", + "Leadership & Organizational Structure", + "Operations & Execution", + "Culture & Team Health", + "Sales, Marketing & Growth", + "Financial Health & Metrics", + "Innovation & Product/Service Strategy", + "Personal Leadership & Risk" +]; + +export const CompanyWikiCompletedState: React.FC = ({ + qaItems = defaultQAItems, + activeSection = 1, + onSectionClick, + onInviteEmployees, + onCopyLink +}) => { + return ( +
+ {/* Table of Contents */} +
+
+
Table of contents
+
+
+ {sections.map((section, index) => { + const sectionNumber = index + 1; + const isActive = sectionNumber === activeSection; + + return ( +
onSectionClick?.(sectionNumber)} + className={`self-stretch p-2 rounded-[10px] inline-flex justify-start items-center gap-2 overflow-hidden cursor-pointer hover:bg-Main-BG-Gray-50 dark:hover:bg-Neutrals-NeutralSlate700 ${isActive ? 'bg-Main-BG-Gray-100 dark:bg-Neutrals-NeutralSlate800 shadow-[0px_1px_2px_0px_rgba(10,13,20,0.03)]' : ''}`} + > +
+
+ {sectionNumber} +
+
+
+ {section} +
+
+ ); + })} +
+
+ + {/* Main Content */} +
+
+
+ {sections[activeSection - 1]} +
+
+
+ {qaItems.map((item, index) => ( +
+
+
+
Q
+
+ {item.question} +
+
+
+
+
+
A
+
+ {item.answer} +
+
+
+
+ ))} + + {/* Additional Questions */} +
+
+
What is the mission of your company?
+
+
+
+
+ Our mission is to not only create value but also to foster a collaborative environment where innovation thrives. We aim to empower our team members to contribute their unique skills and perspectives, ensuring that every project we undertake is a reflection of our collective creativity and dedication. By prioritizing both individual growth and teamwork, we strive to build a company culture that values excellence and continuous improvement. Together, we can achieve remarkable results that benefit not just our organization, but also our clients and the community at... +
+
+
+
+ +
+
+
+
Q
+
What is the mission of your company?
+
+
+
+
+
A
+
The mission is to create value as well as working
+
+
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/components/CompanyWiki/CompanyWikiEmptyState.tsx b/components/CompanyWiki/CompanyWikiEmptyState.tsx new file mode 100644 index 0000000..bac343b --- /dev/null +++ b/components/CompanyWiki/CompanyWikiEmptyState.tsx @@ -0,0 +1,119 @@ +import React from 'react'; + +interface CompanyWikiEmptyStateProps { + progress?: number; + onCompleteOnboarding?: () => void; + onInviteEmployees?: () => void; + onCopyLink?: () => void; +} + +export const CompanyWikiEmptyState: React.FC = ({ + progress = 60, + onCompleteOnboarding, + onInviteEmployees, + onCopyLink +}) => { + return ( +
+
+
+
Table of contents
+
+
+
+
+
1
+
+
Company Overview & Vision
+
+
+
+
2
+
+
Leadership & Organizational Structure
+
+
+
+
3
+
+
Operations & Execution
+
+
+
+
4
+
+
Culture & Team Health
+
+
+
+
5
+
+
Sales, Marketing & Growth
+
+
+
+
6
+
+
Financial Health & Metrics
+
+
+
+
7
+
+
Innovation & Product/Service Strategy
+
+
+
+
8
+
+
Personal Leadership & Risk
+
+
+
+
+ {/* Empty State Illustration */} +
+ {/* Placeholder for illustration - would contain the actual empty state SVG */} +
+
+
+ + + +
+
Company Wiki Empty State
+
+
+
+ + {/* Progress and Call to Action */} +
+
+
+ You're {progress}% Done +
+
+ Complete your onboarding to unlock your Company Wiki +
+
+ + {/* Progress Bar */} +
+
+
+ + {/* Action Button */} + +
+
+
+ ); +}; \ No newline at end of file diff --git a/components/CompanyWiki/CompanyWikiEmptyStateDark.tsx b/components/CompanyWiki/CompanyWikiEmptyStateDark.tsx new file mode 100644 index 0000000..8566a03 --- /dev/null +++ b/components/CompanyWiki/CompanyWikiEmptyStateDark.tsx @@ -0,0 +1,110 @@ +import React from 'react'; + +interface CompanyWikiEmptyStateProps { + progress?: number; + onCompleteOnboarding?: () => void; + onInviteEmployees?: () => void; + onCopyLink?: () => void; +} + +const sections = [ + "Company Overview & Vision", + "Leadership & Organizational Structure", + "Operations & Execution", + "Culture & Team Health", + "Sales, Marketing & Growth", + "Financial Health & Metrics", + "Innovation & Product/Service Strategy", + "Personal Leadership & Risk" +]; + +export const CompanyWikiEmptyState: React.FC = ({ + progress = 60, + onCompleteOnboarding, + onInviteEmployees, + onCopyLink +}) => { + return ( +
+ {/* Table of Contents */} +
+
+
Table of contents
+
+
+ {sections.map((section, index) => { + const sectionNumber = index + 1; + const isActive = sectionNumber === 1; // First section is always active in empty state + + return ( +
+
+
+ {sectionNumber} +
+
+
+ {section} +
+
+ ); + })} +
+
+ + {/* Main Content */} +
+
+
+ Company Overview & Vision +
+
+
+
+ {/* Progress Illustration Placeholder */} +
+
+
+ + + + +
+
Progress Illustration
+
+
+ + {/* Progress Content */} +
+
+ You're {progress}% Done +
+
+ Complete your company onboarding to unlock your company wiki and comprehensive insights about your organization. +
+ + {/* Progress Bar */} +
+
+
+ + {/* Action Button */} + +
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/components/CompanyWiki/CompanyWikiManager.tsx b/components/CompanyWiki/CompanyWikiManager.tsx new file mode 100644 index 0000000..9489c1e --- /dev/null +++ b/components/CompanyWiki/CompanyWikiManager.tsx @@ -0,0 +1,116 @@ +import React, { useState } from 'react'; +import { CompanyWikiEmptyState } from './CompanyWikiEmptyState'; +import { CompanyWikiCompletedState } from './CompanyWikiCompletedState'; +import { InviteEmployeesModal } from './InviteEmployeesModal'; +import { InviteMultipleEmployeesModal } from './InviteMultipleEmployeesModal'; + +export type WikiState = 'empty' | 'completed'; +export type InviteModalState = 'none' | 'single' | 'multiple'; + +interface Employee { + id: string; + name: string; + email: string; + avatar?: string; +} + +interface CompanyWikiManagerProps { + initialState?: WikiState; + onboardingProgress?: number; + onCompleteOnboarding?: () => void; + qaItems?: Array<{ question: string; answer: string }>; + suggestedEmployees?: Employee[]; +} + +export const CompanyWikiManager: React.FC = ({ + initialState = 'empty', + onboardingProgress = 60, + onCompleteOnboarding, + qaItems, + suggestedEmployees +}) => { + const [wikiState, setWikiState] = useState(initialState); + const [inviteModalState, setInviteModalState] = useState('none'); + const [activeSection, setActiveSection] = useState(1); + + const handleCompleteOnboarding = () => { + onCompleteOnboarding?.(); + setWikiState('completed'); + }; + + const handleInviteEmployee = (email: string) => { + console.log('Inviting employee:', email); + // Here you would typically call an API to send the invitation + setInviteModalState('none'); + // You could show a success toast here + }; + + const handleInviteMultipleEmployees = (employees: Employee[]) => { + console.log('Inviting multiple employees:', employees); + // Here you would typically call an API to send multiple invitations + setInviteModalState('none'); + // You could show a success toast here + }; + + const handleSectionClick = (section: number) => { + setActiveSection(section); + }; + + const handleCopyLink = () => { + // Copy wiki link to clipboard + const wikiUrl = `${window.location.origin}/#/company-wiki`; + navigator.clipboard.writeText(wikiUrl).then(() => { + console.log('Wiki link copied to clipboard'); + // You could show a success toast here + }); + }; + + const renderWikiContent = () => { + switch (wikiState) { + case 'empty': + return ( + setInviteModalState('single')} + onCopyLink={handleCopyLink} + /> + ); + + case 'completed': + return ( + setInviteModalState('single')} + onCopyLink={handleCopyLink} + /> + ); + + default: + return null; + } + }; + + return ( +
+ {renderWikiContent()} + + {/* Modals */} + setInviteModalState('none')} + onInvite={handleInviteEmployee} + onMultipleInvite={() => setInviteModalState('multiple')} + /> + + setInviteModalState('none')} + onInviteSelected={handleInviteMultipleEmployees} + suggestedEmployees={suggestedEmployees} + /> +
+ ); +}; \ No newline at end of file diff --git a/components/CompanyWiki/InviteEmployeesModal.tsx b/components/CompanyWiki/InviteEmployeesModal.tsx new file mode 100644 index 0000000..2d12866 --- /dev/null +++ b/components/CompanyWiki/InviteEmployeesModal.tsx @@ -0,0 +1,92 @@ +import React, { useState } from 'react'; + +interface InviteEmployeesModalProps { + isOpen: boolean; + onClose: () => void; + onInvite: (email: string) => void; + onMultipleInvite?: () => void; +} + +export const InviteEmployeesModal: React.FC = ({ + isOpen, + onClose, + onInvite, + onMultipleInvite +}) => { + const [email, setEmail] = useState(''); + + if (!isOpen) return null; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (email.trim()) { + onInvite(email.trim()); + setEmail(''); + } + }; + + return ( +
+
+ {/* Header */} +
+
+
Invite employees
+
+ +
+ + {/* Form */} +
+
+
+ Email +
+
+ setEmail(e.target.value)} + placeholder="Enter email address" + className="flex-1 text-Neutrals-NeutralSlate950 dark:text-Neutrals-NeutralSlate50 text-sm font-normal font-['Inter'] leading-tight bg-transparent outline-none placeholder:text-Text-Gray-500 dark:placeholder:text-Neutrals-NeutralSlate400" + autoFocus + /> +
+
+
+ + {/* Footer */} +
+ +
+ + +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/components/CompanyWiki/InviteMultipleEmployeesModal.tsx b/components/CompanyWiki/InviteMultipleEmployeesModal.tsx new file mode 100644 index 0000000..11bbe5b --- /dev/null +++ b/components/CompanyWiki/InviteMultipleEmployeesModal.tsx @@ -0,0 +1,178 @@ +import React, { useState } from 'react'; + +interface Employee { + id: string; + name: string; + email: string; + avatar?: string; +} + +interface InviteMultipleEmployeesModalProps { + isOpen: boolean; + onClose: () => void; + onInviteSelected: (employees: Employee[]) => void; + suggestedEmployees?: Employee[]; +} + +const defaultSuggestedEmployees: Employee[] = [ + { id: '1', name: 'John Smith', email: 'john@company.com' }, + { id: '2', name: 'Sarah Johnson', email: 'sarah@company.com' }, + { id: '3', name: 'Mike Chen', email: 'mike@company.com' }, + { id: '4', name: 'Emily Davis', email: 'emily@company.com' }, + { id: '5', name: 'Alex Rodriguez', email: 'alex@company.com' }, +]; + +export const InviteMultipleEmployeesModal: React.FC = ({ + isOpen, + onClose, + onInviteSelected, + suggestedEmployees = defaultSuggestedEmployees +}) => { + const [searchTerm, setSearchTerm] = useState(''); + const [selectedEmployees, setSelectedEmployees] = useState([]); + const [showDropdown, setShowDropdown] = useState(false); + + if (!isOpen) return null; + + const filteredEmployees = suggestedEmployees.filter(emp => + (emp.name.toLowerCase().includes(searchTerm.toLowerCase()) || + emp.email.toLowerCase().includes(searchTerm.toLowerCase())) && + !selectedEmployees.find(selected => selected.id === emp.id) + ); + + const handleEmployeeSelect = (employee: Employee) => { + setSelectedEmployees(prev => [...prev, employee]); + setSearchTerm(''); + setShowDropdown(false); + }; + + const handleEmployeeRemove = (employeeId: string) => { + setSelectedEmployees(prev => prev.filter(emp => emp.id !== employeeId)); + }; + + const handleInvite = () => { + if (selectedEmployees.length > 0) { + onInviteSelected(selectedEmployees); + setSelectedEmployees([]); + setSearchTerm(''); + } + }; + + return ( +
+
+ {/* Header */} +
+
+
+ Invite multiple employees +
+
+ +
+ + {/* Search Input with Dropdown */} +
+
+
+ Search employees +
+
+ + + + { + setSearchTerm(e.target.value); + setShowDropdown(e.target.value.length > 0); + }} + onFocus={() => setShowDropdown(searchTerm.length > 0)} + placeholder="Type name or email to search..." + className="flex-1 text-Neutrals-NeutralSlate950 text-sm font-normal font-['Inter'] leading-tight bg-transparent outline-none placeholder:text-Text-Gray-500" + /> +
+ + {/* Dropdown */} + {showDropdown && filteredEmployees.length > 0 && ( +
+ {filteredEmployees.map((employee) => ( + + ))} +
+ )} +
+ + {/* Selected Employees */} + {selectedEmployees.length > 0 && ( +
+
+ Selected ({selectedEmployees.length}) +
+
+ {selectedEmployees.map((employee) => ( +
+
+ {employee.name.charAt(0)} +
+ {employee.name} + +
+ ))} +
+
+ )} +
+ + {/* Footer */} +
+
+ + +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/components/CompanyWiki/index.ts b/components/CompanyWiki/index.ts new file mode 100644 index 0000000..bff95cf --- /dev/null +++ b/components/CompanyWiki/index.ts @@ -0,0 +1,6 @@ +export { CompanyWikiEmptyState } from './CompanyWikiEmptyState'; +export { CompanyWikiCompletedState } from './CompanyWikiCompletedState'; +export { InviteEmployeesModal } from './InviteEmployeesModal'; +export { InviteMultipleEmployeesModal } from './InviteMultipleEmployeesModal'; +export { CompanyWikiManager } from './CompanyWikiManager'; +export type { WikiState, InviteModalState } from './CompanyWikiManager'; \ No newline at end of file diff --git a/components/UiKit.tsx b/components/UiKit.tsx index d592260..92a2458 100644 --- a/components/UiKit.tsx +++ b/components/UiKit.tsx @@ -4,6 +4,7 @@ import { useTheme } from '../contexts/ThemeContext'; import { Theme, NavItem } from '../types'; import { useOrg } from '../contexts/OrgContext'; import { useAuth } from '../contexts/AuthContext'; +import FigmaSidebar from './figma/Sidebar'; // ========== ICONS ========== @@ -265,14 +266,18 @@ const Sidebar = () => { ); }; -export const Layout = () => ( -
- -
- -
-
-); +export const Layout = () => { + const { org } = useOrg(); + + return ( +
+ +
+ +
+
+ ); +}; // ========== UI PRIMITIVES ========== diff --git a/components/chat/ChatEmptyState.tsx b/components/chat/ChatEmptyState.tsx new file mode 100644 index 0000000..54f1369 --- /dev/null +++ b/components/chat/ChatEmptyState.tsx @@ -0,0 +1,180 @@ +import React from 'react'; + +interface SuggestionCardProps { + category: string; + title: string; + description: string; + icon: React.ReactNode; + onClick?: () => void; +} + +const SuggestionCard: React.FC = ({ category, title, description, icon, onClick }) => ( +
+
+
+ {icon} +
+
+
{category}
+
{title}
+
{description}
+
+
+
+); + +interface CategoryTabProps { + label: string; + isActive: boolean; + onClick: () => void; +} + +const CategoryTab: React.FC = ({ label, isActive, onClick }) => ( + +); + +const ChatEmptyState: React.FC = () => { + const [activeCategory, setActiveCategory] = React.useState('All'); + + const categories = ['All', 'Performance', 'Culture', 'Reports', 'Analysis']; + + const suggestions = [ + { + category: 'Performance', + title: 'Analyze team performance trends', + description: 'Get insights on productivity patterns and improvement areas across your organization.', + icon: ( + + + + ) + }, + { + category: 'Culture', + title: 'Assess company culture health', + description: 'Review employee satisfaction, engagement levels, and cultural alignment metrics.', + icon: ( + + + + + + ) + }, + { + category: 'Reports', + title: 'Generate executive summary', + description: 'Create comprehensive reports on organizational strengths, risks, and recommendations.', + icon: ( + + + + ) + }, + { + category: 'Analysis', + title: 'Compare department metrics', + description: 'Analyze cross-departmental performance and identify areas for improvement.', + icon: ( + + + + ) + }, + { + category: 'Performance', + title: 'Review individual performance', + description: 'Deep dive into specific employee performance data and development opportunities.', + icon: ( + + + + + ) + }, + { + category: 'Culture', + title: 'Identify team dynamics', + description: 'Understand collaboration patterns, communication effectiveness, and team cohesion.', + icon: ( + + + + + + + ) + } + ]; + + const filteredSuggestions = activeCategory === 'All' + ? suggestions + : suggestions.filter(s => s.category === activeCategory); + + const handleSuggestionClick = (suggestion: any) => { + // Handle suggestion click - could pass this up to parent component + console.log('Clicked suggestion:', suggestion.title); + }; + + return ( +
+ {/* Welcome Message */} +
+

+ Welcome to Auditly Chat +

+

+ Ask me anything about your team's performance, company culture, or organizational insights. + I can analyze employee data, generate reports, and provide actionable recommendations. +

+
+ + {/* Category Tabs */} +
+ {categories.map((category) => ( + setActiveCategory(category)} + /> + ))} +
+ + {/* Suggestion Cards Grid */} +
+ {filteredSuggestions.map((suggestion, index) => ( + handleSuggestionClick(suggestion)} + /> + ))} +
+ + {/* Additional Help Text */} +
+

+ You can also upload files, mention specific employees using @, or ask custom questions about your organization. + I'll provide insights based on your team's data and industry best practices. +

+
+
+ ); +}; + +export default ChatEmptyState; \ No newline at end of file diff --git a/components/chat/ChatLayout.tsx b/components/chat/ChatLayout.tsx new file mode 100644 index 0000000..42bffa6 --- /dev/null +++ b/components/chat/ChatLayout.tsx @@ -0,0 +1,171 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { useOrg } from '../../contexts/OrgContext'; +import { Employee } from '../../types'; +import ChatSidebar from './ChatSidebar'; +import MessageThread from './MessageThread'; +import FileUploadInput from './FileUploadInput'; + +interface Message { + id: string; + text: string; + isUser: boolean; + timestamp: number; + files?: string[]; +} + +interface ChatLayoutProps { + children?: React.ReactNode; +} + +const ChatLayout: React.FC = ({ children }) => { + const { employees } = useOrg(); + const [selectedEmployees, setSelectedEmployees] = useState([]); + const [messages, setMessages] = useState([]); + const [inputValue, setInputValue] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [uploadedFiles, setUploadedFiles] = useState([]); + + const messagesEndRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + const handleNavigation = (page: string) => { + // Handle navigation to different pages + console.log('Navigate to:', page); + }; + + const handleSendMessage = async () => { + if (!inputValue.trim() && uploadedFiles.length === 0) return; + + const userMessage: Message = { + id: Date.now().toString(), + text: inputValue, + isUser: true, + timestamp: Date.now(), + files: uploadedFiles.length > 0 ? [...uploadedFiles] : undefined + }; + + setMessages(prev => [...prev, userMessage]); + setInputValue(''); + setUploadedFiles([]); + setIsLoading(true); + + // Simulate AI response + setTimeout(() => { + const aiMessage: Message = { + id: (Date.now() + 1).toString(), + text: "I understand you're asking about the employee data. Based on the information provided, I can help analyze the performance metrics and provide insights.\n\nHere are some key findings from your team's data:\n\n• **Performance Trends**: Overall team productivity has increased by 15% this quarter\n• **Cultural Health**: Employee satisfaction scores are above industry average\n• **Areas for Growth**: Communication and cross-team collaboration could be improved\n\nWould you like me to dive deeper into any of these areas?", + isUser: false, + timestamp: Date.now() + }; + setMessages(prev => [...prev, aiMessage]); + setIsLoading(false); + }, 2000); + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }; + + const handleRemoveFile = (index: number) => { + setUploadedFiles(prev => prev.filter((_, i) => i !== index)); + }; + + const handleFilesSelected = (files: File[]) => { + // For demo purposes, we'll just add the file names + // In a real implementation, you'd upload the files and get URLs back + const fileNames = files.map(file => file.name); + setUploadedFiles(prev => [...prev, ...fileNames]); + }; + + const hasMessages = messages.length > 0; + + return ( +
+ {/* Sidebar */} + + + {/* Main Content Area */} +
+ {/* Header with Employee Selection */} +
+
+

Chat

+ {selectedEmployees.length > 0 && ( +
+ Analyzing: +
+ {selectedEmployees.slice(0, 3).map((emp, index) => ( +
+ {emp.name} +
+ ))} + {selectedEmployees.length > 3 && ( +
+ +{selectedEmployees.length - 3} more +
+ )} +
+
+ )} +
+
+ + {/* Messages Area */} +
+ {hasMessages ? ( +
+ +
+
+ ) : ( + children + )} +
+ + {/* Input Area */} +
+
+
+ + + {/* Send Button */} + +
+
+
+
+
+ ); +}; + +export default ChatLayout; \ No newline at end of file diff --git a/components/chat/ChatSidebar.tsx b/components/chat/ChatSidebar.tsx new file mode 100644 index 0000000..91bfae7 --- /dev/null +++ b/components/chat/ChatSidebar.tsx @@ -0,0 +1,263 @@ +import React from 'react'; +import { useOrg } from '../../contexts/OrgContext'; + +interface NavItemProps { + icon: React.ReactNode; + label: string; + isActive?: boolean; + onClick?: () => void; +} + +const NavItem: React.FC = ({ icon, label, isActive, onClick }) => ( +
+ {icon} +
+ {label} +
+
+); + +interface ChatSidebarProps { + currentPage?: string; + onNavigate?: (page: string) => void; +} + +const ChatSidebar: React.FC = ({ currentPage = 'chat', onNavigate }) => { + const { org } = useOrg(); + + const handleNavigation = (page: string) => { + if (onNavigate) { + onNavigate(page); + } + }; + + return ( +
+ {/* Company Selection */} +
+
+
+
+
+
+
+ {/* Company Icon SVG */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ {org?.name || 'Zitlac Media'} +
+
+
+
+ + + +
+
+ + {/* Navigation Menu */} +
+
+ + + + } + label="Company Wiki" + isActive={currentPage === 'wiki'} + onClick={() => handleNavigation('wiki')} + /> + + + + } + label="Submissions" + isActive={currentPage === 'submissions'} + onClick={() => handleNavigation('submissions')} + /> + + + + + + + + + + + } + label="Reports" + isActive={currentPage === 'reports'} + onClick={() => handleNavigation('reports')} + /> + + + + } + label="Chat" + isActive={currentPage === 'chat'} + onClick={() => handleNavigation('chat')} + /> + + + + + + + + + + + } + label="Help" + isActive={currentPage === 'help'} + onClick={() => handleNavigation('help')} + /> +
+
+
+ + {/* Bottom Section with Settings and Company Report Builder */} +
+ + + + + + + + + + + + } + label="Settings" + isActive={currentPage === 'settings'} + onClick={() => handleNavigation('settings')} + /> + + {/* Company Report Builder Card */} +
+
+
+
+
+
+
+
+ Build {org?.name || '[Company]'}'s Report +
+
+ Share this form with your team members to capture valuable info about your company to train Auditly. +
+
+
+
+
+
+
Invite
+
+
+ + + +
+
+
+
+ + + +
+
+
Copy
+
+
+
+
+
+
+
+ ); +}; + +export default ChatSidebar; \ No newline at end of file diff --git a/components/chat/FileUploadInput.tsx b/components/chat/FileUploadInput.tsx new file mode 100644 index 0000000..55190a6 --- /dev/null +++ b/components/chat/FileUploadInput.tsx @@ -0,0 +1,244 @@ +import React, { useState, useRef, useCallback } from 'react'; + +interface FileUploadPreviewProps { + files: string[]; + onRemoveFile: (index: number) => void; +} + +const FileUploadPreview: React.FC = ({ files, onRemoveFile }) => { + if (files.length === 0) return null; + + const getFileIcon = (fileName: string) => { + const extension = fileName.split('.').pop()?.toLowerCase(); + + switch (extension) { + case 'pdf': + return ( +
+ P +
+ ); + case 'doc': + case 'docx': + return ( +
+ W +
+ ); + case 'xls': + case 'xlsx': + return ( +
+ E +
+ ); + case 'jpg': + case 'jpeg': + case 'png': + case 'gif': + return ( +
+ + + +
+ ); + default: + return ( +
+ + + +
+ ); + } + }; + + return ( +
+ {files.map((file, index) => ( +
+ {getFileIcon(file)} + {file} + +
+ ))} +
+ ); +}; + +interface FileUploadDropzoneProps { + onFilesSelected: (files: File[]) => void; + children: React.ReactNode; + accept?: string; + multiple?: boolean; + disabled?: boolean; +} + +const FileUploadDropzone: React.FC = ({ + onFilesSelected, + children, + accept = "*/*", + multiple = true, + disabled = false +}) => { + const [isDragOver, setIsDragOver] = useState(false); + const fileInputRef = useRef(null); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + if (!disabled) { + setIsDragOver(true); + } + }, [disabled]); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragOver(false); + }, []); + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragOver(false); + + if (disabled) return; + + const files = Array.from(e.dataTransfer.files); + if (files.length > 0) { + onFilesSelected(files); + } + }, [onFilesSelected, disabled]); + + const handleFileSelect = useCallback((e: React.ChangeEvent) => { + const files = Array.from(e.target.files || []); + if (files.length > 0) { + onFilesSelected(files); + } + // Reset input value to allow selecting the same file again + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }, [onFilesSelected]); + + const handleClick = () => { + if (!disabled && fileInputRef.current) { + fileInputRef.current.click(); + } + }; + + return ( +
+ + + {children} + + {/* Drag overlay */} + {isDragOver && ( +
+
Drop files here
+
+ )} +
+ ); +}; + +interface FileUploadInputProps { + value: string; + onChange: (value: string) => void; + onKeyDown?: (e: React.KeyboardEvent) => void; + placeholder?: string; + disabled?: boolean; + uploadedFiles: string[]; + onRemoveFile: (index: number) => void; + onFilesSelected: (files: File[]) => void; +} + +const FileUploadInput: React.FC = ({ + value, + onChange, + onKeyDown, + placeholder = "Ask about your team's performance, culture, or any insights...", + disabled = false, + uploadedFiles, + onRemoveFile, + onFilesSelected +}) => { + const handleFilesSelected = (files: File[]) => { + // For demo purposes, we'll just add the file names + // In a real implementation, you'd upload the files and get URLs back + const fileNames = files.map(file => file.name); + onFilesSelected(files); + }; + + return ( +
+ {/* File Upload Preview */} + + + {/* Input Field with File Upload */} + +
+
+