-
{currentStep} of {totalSteps}
+
+
{currentStep} of {totalSteps}
)}
@@ -238,9 +238,9 @@ export const FigmaNavigationButtons: React.FC<{
{onSkip && (
)}
@@ -256,10 +256,10 @@ export const FigmaNavigationButtons: React.FC<{
{onBack && (
)}
diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx
index c770e23..c789c19 100644
--- a/src/contexts/AuthContext.tsx
+++ b/src/contexts/AuthContext.tsx
@@ -1,7 +1,6 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import { onAuthStateChanged, signInWithPopup, signOut, User, createUserWithEmailAndPassword, signInWithEmailAndPassword, updateProfile } from 'firebase/auth';
import { auth, googleProvider, isFirebaseConfigured } from '../services/firebase';
-import { demoStorage } from '../services/demoStorage';
import { API_URL } from '../constants';
interface AuthContextType {
@@ -105,30 +104,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const signInWithEmail = async (email: string, password: string) => {
console.log('signInWithEmail called, isFirebaseConfigured:', isFirebaseConfigured);
- if (!isFirebaseConfigured) {
- console.log('Demo mode: authenticating user', email);
- const existingUser = demoStorage.getUserByEmail(email);
-
- if (existingUser) {
- // Verify password
- if (demoStorage.verifyPassword(password, existingUser.passwordHash)) {
- const mockUser = {
- uid: existingUser.uid,
- email: existingUser.email,
- displayName: existingUser.displayName
- } as unknown as User;
-
- setUser(mockUser);
- localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
- console.log('Demo login successful for:', email);
- } else {
- throw new Error('Invalid password');
- }
- } else {
- throw new Error('User not found. Please sign up first.');
- }
- return;
- }
try {
console.log('Attempting Firebase auth');
await signInWithEmailAndPassword(auth, email, password);
@@ -146,36 +121,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
};
const signUpWithEmail = async (email: string, password: string, displayName?: string) => {
- if (!isFirebaseConfigured) {
- console.log('Demo mode: creating new user', email);
- // Check if user already exists
- const existingUser = demoStorage.getUserByEmail(email);
- if (existingUser) {
- throw new Error('User already exists with this email');
- }
-
- // Create new user
- const uid = `demo-${btoa(email).slice(0, 8)}`;
- const newUser = {
- uid,
- email,
- displayName: displayName || email.split('@')[0],
- passwordHash: demoStorage.hashPassword(password)
- };
-
- demoStorage.saveUser(newUser);
-
- const mockUser = {
- uid: newUser.uid,
- email: newUser.email,
- displayName: newUser.displayName
- } as unknown as User;
-
- setUser(mockUser);
- localStorage.setItem('auditly_demo_session', JSON.stringify(mockUser));
- console.log('Demo signup successful for:', email);
- return;
- }
try {
const cred = await createUserWithEmailAndPassword(auth, email, password);
if (displayName) {
diff --git a/src/contexts/OrgContext.tsx b/src/contexts/OrgContext.tsx
index 42c069f..82b8644 100644
--- a/src/contexts/OrgContext.tsx
+++ b/src/contexts/OrgContext.tsx
@@ -1,13 +1,11 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
-import { collection, doc, getDoc, getDocs, onSnapshot, setDoc } from 'firebase/firestore';
-import { db, isFirebaseConfigured } from '../services/firebase';
import { useAuth } from './AuthContext';
import { Employee, Report, Submission, CompanyReport } from '../types';
import { SAMPLE_COMPANY_REPORT, API_URL } from '../constants';
-import { demoStorage } from '../services/demoStorage';
import { apiPost, apiPut } from '../services/api';
import { User } from 'firebase/auth';
import { EmployeeSubmissionAnswers } from '../employeeQuestions';
+import { secureApi } from '../services/secureApi';
interface OrgData {
orgId: string;
@@ -44,12 +42,13 @@ interface OrgContextType {
employees: Employee[];
submissions: Record
;
reports: Record;
+ loading: boolean;
upsertOrg: (data: Partial) => Promise;
saveReport: (employeeId: string, report: Report) => Promise;
inviteEmployee: (args: { name: string; email: string }) => Promise<{ employeeId: string; inviteLink: string }>;
issueInviteViaApi: (args: { name: string; email: string; role?: string; department?: string }) => Promise<{ code: string; inviteLink: string; emailLink: string; employee: any }>;
getInviteStatus: (code: string) => Promise<{ used: boolean; employee: any } | null>;
- consumeInvite: (code: string) => Promise<{ employee: any } | null>;
+ consumeInvite: (code: string) => Promise<{ employee: any; orgId?: string } | null>;
getReportVersions: (employeeId: string) => Promise>;
saveReportVersion: (employeeId: string, report: Report) => Promise;
acceptInvite: (code: string) => Promise;
@@ -78,123 +77,157 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
const [reportVersions, setReportVersions] = useState>>({});
const [companyReports, setCompanyReports] = useState>([]);
const [fullCompanyReports, setFullCompanyReports] = useState([]);
+ const [loading, setLoading] = useState(true);
// Use the provided selectedOrgId instead of deriving from user
const orgId = selectedOrgId;
+ // Load initial data using secure API
useEffect(() => {
- console.log('OrgContext effect running, orgId:', orgId, 'isFirebaseConfigured:', isFirebaseConfigured);
- if (!orgId) return; // Wait for orgId to be available
- console.log('Setting up Firebase org data');
- const orgRef = doc(db, 'orgs', orgId);
- getDoc(orgRef).then(async (snap) => {
- if (snap.exists()) {
- setOrg({ orgId, ...(snap.data() as any) });
- } else {
- const seed = { name: 'Your Company', onboardingCompleted: false };
- await setDoc(orgRef, seed);
- setOrg({ orgId, ...(seed as any) });
+ if (!orgId || !user?.uid) {
+ setLoading(false);
+ return;
+ }
+
+ console.log('OrgContext: Loading data via secure API for orgId:', orgId);
+
+ const loadOrgData = async () => {
+ try {
+ setLoading(true);
+
+ // Load organization data
+ try {
+ const orgData = await secureApi.getOrgData(orgId, user.uid);
+ setOrg({ orgId, ...orgData });
+ } catch (error) {
+ console.warn('Could not load org data, creating default:', error);
+ // Create default org if not found
+ const defaultOrg = { name: 'Your Company', onboardingCompleted: false };
+ await secureApi.updateOrgData(orgId, user.uid, defaultOrg);
+ setOrg({ orgId, ...defaultOrg });
+ }
+
+ // Load employees
+ try {
+ const employeesData = await secureApi.getEmployees(orgId, user.uid);
+ setEmployees(employeesData.map(emp => ({
+ ...emp,
+ initials: emp.name ? emp.name.split(' ').map(n => n[0]).join('').toUpperCase() : emp.email?.substring(0, 2).toUpperCase() || 'U'
+ })));
+ } catch (error) {
+ console.warn('Could not load employees:', error);
+ setEmployees([]);
+ }
+
+ // Load submissions
+ try {
+ const submissionsData = await secureApi.getSubmissions(orgId, user.uid);
+ setSubmissions(submissionsData);
+ } catch (error) {
+ console.warn('Could not load submissions:', error);
+ setSubmissions({});
+ }
+
+ // Load reports
+ try {
+ const reportsData = await secureApi.getReports(orgId, user.uid);
+ setReports(reportsData as Record);
+ } catch (error) {
+ console.warn('Could not load reports:', error);
+ setReports({});
+ }
+
+ // Load company reports
+ try {
+ const companyReportsData = await secureApi.getCompanyReports(orgId, user.uid);
+ setFullCompanyReports(companyReportsData);
+ } catch (error) {
+ console.warn('Could not load company reports:', error);
+ setFullCompanyReports([]);
+ }
+
+ } catch (error) {
+ console.error('Failed to load org data:', error);
+ } finally {
+ setLoading(false);
}
- });
+ };
- const employeesCol = collection(db, 'orgs', orgId, 'employees');
- const unsubEmp = onSnapshot(employeesCol, (snap) => {
- const arr: Employee[] = [];
- snap.forEach((d) => arr.push({ id: d.id, ...(d.data() as any) }));
- setEmployees(arr);
- });
-
- const submissionsCol = collection(db, 'orgs', orgId, 'submissions');
- const unsubSub = onSnapshot(submissionsCol, (snap) => {
- const map: Record = {};
- snap.forEach((d) => (map[d.id] = { employeeId: d.id, ...(d.data() as any) }));
- setSubmissions(map);
- });
-
- const reportsCol = collection(db, 'orgs', orgId, 'reports');
- const unsubRep = onSnapshot(reportsCol, (snap) => {
- const map: Record = {};
- snap.forEach((d) => (map[d.id] = { employeeId: d.id, ...(d.data() as any) } as Report));
- setReports(map);
- });
-
- return () => { unsubEmp(); unsubSub(); unsubRep(); };
- }, [orgId]);
+ loadOrgData();
+ }, [orgId, user?.uid]);
const upsertOrg = async (data: Partial) => {
- const orgRef = doc(db, 'orgs', orgId);
- await setDoc(orgRef, data, { merge: true });
+ if (!user?.uid) {
+ throw new Error('User authentication required');
+ }
- // Update local state
- const updatedOrg = { ...(org || { orgId, name: 'Your Company' }), ...data } as OrgData;
- setOrg(updatedOrg);
+ try {
+ await secureApi.updateOrgData(orgId, user.uid, data);
- // If onboarding was completed, notify other contexts
- if (data.onboardingCompleted) {
- console.log('OrgContext (Firebase): Onboarding completed, dispatching update event', {
- orgId: updatedOrg.orgId,
- onboardingCompleted: true
- });
+ // Update local state
+ const updatedOrg = { ...(org || { orgId, name: 'Your Company' }), ...data } as OrgData;
+ setOrg(updatedOrg);
- window.dispatchEvent(new CustomEvent('organizationUpdated', {
- detail: { orgId: updatedOrg.orgId, onboardingCompleted: true }
- }));
+ // If onboarding was completed, notify other contexts
+ if (data.onboardingCompleted) {
+ console.log('OrgContext: Onboarding completed, dispatching update event', {
+ orgId: updatedOrg.orgId,
+ onboardingCompleted: true
+ });
+
+ window.dispatchEvent(new CustomEvent('organizationUpdated', {
+ detail: { orgId: updatedOrg.orgId, onboardingCompleted: true }
+ }));
+ }
+ } catch (error) {
+ console.error('Failed to update organization:', error);
+ throw error;
}
};
const saveReport = async (employeeId: string, report: Report) => {
- const ref = doc(db, 'orgs', orgId, 'reports', employeeId);
- await setDoc(ref, report, { merge: true });
+ if (!user?.uid) {
+ throw new Error('User authentication required');
+ }
+
+ try {
+ const savedReport = await secureApi.saveReport(orgId, user.uid, employeeId, report);
+
+ // Update local state
+ setReports(prev => ({ ...prev, [employeeId]: savedReport }));
+ } catch (error) {
+ console.error('Failed to save report:', error);
+ throw error;
+ }
};
const inviteEmployee = async ({ name, email }: { name: string; email: string }) => {
console.log('inviteEmployee called:', { name, email, orgId });
try {
- // Always use Cloud Functions for invites to ensure multi-tenant compliance
- const res = await apiPost('/createInvitation', {
- name,
- email
- }, orgId);
+ // Use secure API for invites
+ const data = await secureApi.createInvitation(orgId, name, email);
- if (!res.ok) {
- const errorData = await res.json();
- console.error('Invite creation failed:', errorData);
- throw new Error(errorData.error || `Failed to create invite: ${res.status}`);
- }
-
- const data = await res.json();
- const { code, employee, inviteLink } = data;
-
- console.log('Invite created successfully:', { code, employee: employee.name, inviteLink });
+ console.log('Invite created successfully:', { code: data.code, employee: data.employee.name, inviteLink: data.inviteLink });
// Store employee locally for immediate UI update with proper typing
const newEmployee: Employee = {
- id: employee.id,
- name: employee.name,
- email: employee.email,
- initials: employee.name ? employee.name.split(' ').map(n => n[0]).join('').toUpperCase() : employee.email.substring(0, 2).toUpperCase(),
- department: employee.department,
- role: employee.role,
+ id: data.employee.id,
+ name: data.employee.name,
+ email: data.employee.email,
+ initials: data.employee.name ? data.employee.name.split(' ').map(n => n[0]).join('').toUpperCase() : data.employee.email.substring(0, 2).toUpperCase(),
+ department: data.employee.department,
+ role: data.employee.role,
isOwner: false
};
- if (!isFirebaseConfigured) {
- const employeeWithOrg = { ...newEmployee, orgId };
- setEmployees(prev => {
- if (prev.find(e => e.id === employee.id)) return prev;
- return [...prev, newEmployee];
- });
- demoStorage.saveEmployee(employeeWithOrg);
- } else {
- // For Firebase, add to local state for immediate UI update
- setEmployees(prev => {
- if (prev.find(e => e.id === employee.id)) return prev;
- return [...prev, newEmployee];
- });
- }
+ // Add to local state for immediate UI update
+ setEmployees(prev => {
+ if (prev.find(e => e.id === data.employee.id)) return prev;
+ return [...prev, newEmployee];
+ });
- return { employeeId: employee.id, inviteLink };
+ return { employeeId: data.employee.id, inviteLink: data.inviteLink };
} catch (error) {
console.error('inviteEmployee error:', error);
throw error;
@@ -202,84 +235,76 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
};
const getReportVersions = async (employeeId: string) => {
- if (!isFirebaseConfigured) {
- return reportVersions[employeeId] || [];
- }
- const col = collection(db, 'orgs', orgId, 'reports', employeeId, 'versions');
- const snap = await getDocs(col);
- const arr: Array<{ id: string; createdAt: number; report: Report }> = [];
- snap.forEach(d => {
- const data = d.data() as any;
- arr.push({ id: d.id, createdAt: data.createdAt ?? 0, report: data.report as Report });
- });
- return arr.sort((a, b) => b.createdAt - a.createdAt);
+ // This feature is not yet implemented in secure API
+ // Return empty array for now until we add version support to cloud functions
+ console.warn('Report versions not yet supported in secure API');
+ return [];
};
const saveReportVersion = async (employeeId: string, report: Report) => {
+ // This feature is not yet implemented in secure API
+ console.warn('Report versions not yet supported in secure API');
const version = { id: Date.now().toString(), createdAt: Date.now(), report };
- if (!isFirebaseConfigured) {
- setReportVersions(prev => ({ ...prev, [employeeId]: [version, ...(prev[employeeId] || [])] }));
- return;
- }
- const ref = doc(db, 'orgs', orgId, 'reports', employeeId, 'versions', version.id);
- await setDoc(ref, { createdAt: version.createdAt, report });
+ setReportVersions(prev => ({ ...prev, [employeeId]: [version, ...(prev[employeeId] || [])] }));
};
const acceptInvite = async (code: string) => {
- if (!code) return;
+ if (!code || !user?.uid) return;
- if (!isFirebaseConfigured) {
- // Demo mode: mark invite as used
- demoStorage.markInviteUsed(code);
- return;
+ try {
+ await secureApi.consumeInvitation(code, user.uid);
+ } catch (error) {
+ console.error('Failed to accept invite:', error);
+ throw error;
}
-
- const inviteRef = doc(db, 'orgs', orgId, 'invites', code);
- const snap = await getDoc(inviteRef);
- if (!snap.exists()) return;
- const data = snap.data() as any;
- // Minimal: mark accepted
- await setDoc(inviteRef, { ...data, acceptedAt: Date.now() }, { merge: true });
};
const saveCompanyReport = async (summary: string) => {
- const id = Date.now().toString();
- const createdAt = Date.now();
- if (!isFirebaseConfigured) {
- const reportData = { id, createdAt, summary };
- setCompanyReports(prev => [reportData, ...prev]);
- // Persist to localStorage (note: this method stores simple reports)
- return;
+ if (!user?.uid) {
+ throw new Error('User authentication required');
+ }
+
+ try {
+ const report = {
+ id: Date.now().toString(),
+ createdAt: Date.now(),
+ summary
+ };
+
+ await secureApi.saveCompanyReport(orgId, report);
+
+ // Update local state
+ setCompanyReports(prev => [{ id: report.id, createdAt: report.createdAt, summary }, ...prev]);
+ } catch (error) {
+ console.error('Failed to save company report:', error);
+ throw error;
}
- const ref = doc(db, 'orgs', orgId, 'companyReports', id);
- await setDoc(ref, { createdAt, summary });
};
const getCompanyReportHistory = async () => {
- if (!isFirebaseConfigured) {
- return companyReports;
+ if (!user?.uid) {
+ throw new Error('User authentication required');
+ }
+
+ try {
+ const reports = await secureApi.getCompanyReports(orgId, user.uid);
+ return reports.map(report => ({
+ id: report.id,
+ createdAt: report.createdAt || 0,
+ summary: report.summary || ''
+ })).sort((a, b) => b.createdAt - a.createdAt);
+ } catch (error) {
+ console.error('Failed to get company report history:', error);
+ return [];
}
- const col = collection(db, 'orgs', orgId, 'companyReports');
- const snap = await getDocs(col);
- const arr: Array<{ id: string; createdAt: number; summary: string }> = [];
- snap.forEach(d => {
- const data = d.data() as any;
- arr.push({ id: d.id, createdAt: data.createdAt ?? 0, summary: data.summary ?? '' });
- });
- return arr.sort((a, b) => b.createdAt - a.createdAt);
};
const seedInitialData = async () => {
- if (!isFirebaseConfigured) {
- // Start with completely clean slate - no sample data
- setEmployees([]);
- setSubmissions({});
- setReports({});
- setFullCompanyReports([]);
- return;
- }
-
// Start with clean slate - let users invite their own employees and generate real data
+ setEmployees([]);
+ setSubmissions({});
+ setReports({});
+ setFullCompanyReports([]);
};
const saveFullCompanyReport = async (report: CompanyReport) => {
@@ -288,37 +313,42 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
throw new Error('Organization ID is required to save company report');
}
- if (!isFirebaseConfigured || !db) {
- // Fallback to local storage in demo mode
- setFullCompanyReports(prev => [report, ...prev]);
- demoStorage.saveCompanyReport(orgId, report);
- return;
+ if (!user?.uid) {
+ throw new Error('User authentication required');
}
- // Use direct Firestore operations - much more efficient
- const ref = doc(db, 'orgs', orgId, 'fullCompanyReports', report.id);
- await setDoc(ref, report);
+ try {
+ await secureApi.saveCompanyReport(orgId, report);
- // Update local state after successful save
- setFullCompanyReports(prev => [report, ...prev]);
+ // Update local state after successful save
+ setFullCompanyReports(prev => [report, ...prev]);
+ } catch (error) {
+ console.error('Failed to save full company report:', error);
+ throw error;
+ }
};
const getFullCompanyReportHistory = async (): Promise => {
- if (!isFirebaseConfigured) {
- return fullCompanyReports;
+ if (!user?.uid) {
+ throw new Error('User authentication required');
+ }
+
+ try {
+ const reports = await secureApi.getCompanyReports(orgId, user.uid);
+ return reports.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
+ } catch (error) {
+ console.error('Failed to get full company report history:', error);
+ return [];
}
- const col = collection(db, 'orgs', orgId, 'fullCompanyReports');
- const snap = await getDocs(col);
- const arr: CompanyReport[] = [];
- snap.forEach(d => {
- arr.push({ id: d.id, ...d.data() } as CompanyReport);
- });
- return arr.sort((a, b) => b.createdAt - a.createdAt);
};
const generateCompanyReport = async (): Promise => {
console.log('generateCompanyReport called for org:', orgId);
+ if (!user?.uid) {
+ throw new Error('User authentication required');
+ }
+
// Calculate concrete metrics from actual data (no AI needed)
// Exclude owners from employee counts - they are company wiki contributors, not employees
const actualEmployees = employees.filter(emp => !emp.isOwner);
@@ -343,41 +373,32 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
const departmentBreakdown = Array.from(deptMap.entries()).map(([department, count]) => ({ department, count }));
try {
- // Use AI only for analysis and insights that require reasoning
- const res = await apiPost('/generateCompanyWiki', {
- org: org,
- submissions: employeeSubmissions, // Only employee submissions, not owner data
+ // Use secure API for AI generation
+ const data = await secureApi.generateCompanyWiki({
+ ...org,
metrics: {
totalEmployees,
submissionRate,
departmentBreakdown
}
- }, orgId);
+ }, Object.values(employeeSubmissions));
- if (!res.ok) {
- const errorData = await res.json();
- console.error('Company report generation failed:', errorData);
- throw new Error(errorData.error || 'Failed to generate company report');
- }
-
- const data = await res.json();
console.log('Company insights generated via AI successfully');
- console.log('AI response data:', data);
// Combine concrete metrics with AI insights
const report: CompanyReport = {
id: Date.now().toString(),
createdAt: Date.now(),
// Use AI-generated insights for subjective analysis
- ...data.report,
+ ...(data as any),
// Override with our concrete metrics
overview: {
totalEmployees,
departmentBreakdown,
submissionRate,
lastUpdated: Date.now(),
- averagePerformanceScore: data.report?.overview?.averagePerformanceScore || 0,
- riskLevel: data.report?.overview?.riskLevel || 'Unknown'
+ averagePerformanceScore: (data as any)?.overview?.averagePerformanceScore || 0,
+ riskLevel: (data as any)?.overview?.riskLevel || 'Unknown'
}
};
@@ -392,29 +413,21 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
const generateCompanyWiki = async (orgOverride?: OrgData): Promise => {
const orgData = orgOverride || org;
- console.log('generateCompanyWiki called with:', { orgData, orgId, submissionsCount: Object.keys(submissions || {}).length, isFirebaseConfigured });
+ console.log('generateCompanyWiki called with:', { orgData, orgId, submissionsCount: Object.keys(submissions || {}).length });
if (!orgId) {
throw new Error('Organization ID is required to generate company wiki');
}
- // ALWAYS use API call for wiki generation, with local fallback
+ if (!user?.uid) {
+ throw new Error('User authentication required');
+ }
+
+ // Use secure API for wiki generation
try {
console.log('Making API call to generateCompanyWiki...');
- const res = await apiPost('/generateCompanyWiki', {
- org: orgData,
- submissions: submissions || []
- }, orgId);
+ const payload = await secureApi.generateCompanyWiki(orgData, Object.values(submissions || {}));
- console.log('API response status:', res.status);
-
- if (!res.ok) {
- const errorData = await res.json();
- console.error('API error response:', errorData);
- throw new Error(errorData.error || 'Failed to generate company wiki');
- }
-
- const payload = await res.json();
console.log('API success response:', payload);
// Ensure the report has all required fields to prevent undefined errors
@@ -440,7 +453,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
gradingOverview: {},
executiveSummary: 'Company report generated successfully.',
// Override with API data if available
- ...(payload.report || payload)
+ ...(payload as any || {})
};
await saveFullCompanyReport(data);
@@ -459,41 +472,37 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
const getEmployeeReport = async (employeeId: string) => {
try {
- if (isFirebaseConfigured && user) {
- // Firebase implementation
- const reportDoc = await getDoc(doc(db, 'organizations', orgId, 'employeeReports', employeeId));
- if (reportDoc.exists()) {
- return { success: true, report: reportDoc.data() };
- }
- return { success: false, error: 'Report not found' };
- } else {
- // Demo mode - call API
- const response = await fetch(`${API_URL}/api/employee-report/${employeeId}`);
- const result = await response.json();
- return result;
+ if (!user?.uid) {
+ throw new Error('User authentication required');
}
+
+ // Use secure API for all employee report operations
+ const report = await secureApi.getReports(orgId, user.uid);
+ const employeeReport = report[employeeId];
+
+ if (employeeReport) {
+ return { success: true, report: employeeReport };
+ }
+ return { success: false, error: 'Report not found' };
} catch (error) {
console.error('Error fetching employee report:', error);
- return { success: false, error: error.message };
+ return { success: false, error: (error as Error).message };
}
};
const getEmployeeReports = async () => {
try {
- if (isFirebaseConfigured && user) {
- // Firebase implementation
- const reportsSnapshot = await getDocs(collection(db, 'organizations', orgId, 'employeeReports'));
- const reports = reportsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
- return { success: true, reports };
- } else {
- // Demo mode - call API
- const response = await fetch(`${API_URL}/api/employee-reports`);
- const result = await response.json();
- return result;
+ if (!user?.uid) {
+ throw new Error('User authentication required');
}
+
+ // Use secure API for all employee report operations
+ const reportsData = await secureApi.getReports(orgId, user.uid);
+ const reports = Object.values(reportsData);
+ return { success: true, reports };
} catch (error) {
console.error('Error fetching employee reports:', error);
- return { success: false, error: error.message };
+ return { success: false, error: (error as Error).message };
}
};
@@ -503,6 +512,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
employees,
submissions,
reports,
+ loading,
upsertOrg,
saveReport,
inviteEmployee,
@@ -519,125 +529,68 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
isOwner,
issueInviteViaApi: async ({ name, email, role, department }) => {
try {
- const res = await apiPost('/createInvitation', {
- name,
- email,
- role,
- department
- }, orgId);
-
- if (!res.ok) {
- const errorData = await res.json();
- throw new Error(errorData.error || 'Invite creation failed');
+ if (!user?.uid) {
+ throw new Error('User authentication required');
}
- const json = await res.json();
+ const data = await secureApi.createInvitation(orgId, name, email, role, department);
+
// Optimistically add employee shell (not yet active until consume)
- setEmployees(prev => prev.find(e => e.id === json.employee.id) ? prev : [...prev, { ...json.employee }]);
- return json;
+ setEmployees(prev => prev.find(e => e.id === data.employee.id) ? prev : [...prev, {
+ ...data.employee,
+ initials: data.employee.name ? data.employee.name.split(' ').map((n: string) => n[0]).join('').toUpperCase() : data.employee.email.substring(0, 2).toUpperCase()
+ } as Employee]);
+ return data;
} catch (e) {
console.error('issueInviteViaApi error', e);
throw e;
}
},
getInviteStatus: async (code: string) => {
- if (!isFirebaseConfigured) {
- // Demo mode: check localStorage first, then server
- const invite = demoStorage.getInvite(code);
- if (invite) {
- return { used: invite.used, employee: invite.employee };
- }
- }
-
try {
- const res = await fetch(`${API_URL}/getInvitationStatus?code=${code}`);
- if (!res.ok) return null;
- return await res.json();
+ return await secureApi.getInvitationStatus(code);
} catch (e) {
console.error('getInviteStatus error', e);
return null;
}
},
consumeInvite: async (code: string) => {
- if (!isFirebaseConfigured) {
- // Demo mode: mark invite as used in localStorage and update state
- const invite = demoStorage.getInvite(code);
- if (invite && !invite.used) {
- demoStorage.markInviteUsed(code);
- // Ensure employee is in the list with proper typing
- const convertedEmployee: Employee = {
- id: invite.employee.id,
- name: invite.employee.name,
- email: invite.employee.email,
- initials: invite.employee.name ? invite.employee.name.split(' ').map(n => n[0]).join('').toUpperCase() : invite.employee.email.substring(0, 2).toUpperCase(),
- department: invite.employee.department,
- role: invite.employee.role
- };
- setEmployees(prev => prev.find(e => e.id === invite.employee.id) ? prev : [...prev, convertedEmployee]);
- return { employee: convertedEmployee };
+ try {
+ if (!user?.uid) {
+ throw new Error('User authentication required');
+ }
+
+ const result = await secureApi.consumeInvitation(code, user.uid);
+
+ // Mark employee as active
+ if (result && (result as any).employee) {
+ setEmployees(prev => prev.find(e => e.id === (result as any).employee.id) ? prev : [...prev, (result as any).employee]);
+ return { ...(result as any), orgId: org?.orgId };
}
return null;
- }
-
- try {
- const res = await fetch(`${API_URL}/consumeInvitation?code=${code}`, { method: 'POST' });
- if (!res.ok) return null;
- const json = await res.json();
- // Mark employee as active (could set a flag later)
- setEmployees(prev => prev.find(e => e.id === json.employee.id) ? prev : [...prev, json.employee]);
- return json;
} catch (e) {
console.error('consumeInvite error', e);
return null;
}
},
submitEmployeeAnswers: async (employeeId: string, answers: Record) => {
- if (!isFirebaseConfigured) {
- // Demo mode: save to localStorage and call server endpoint
- try {
- const submission = {
- employeeId,
- orgId,
- answers,
- createdAt: Date.now()
- };
-
- // Save to localStorage for persistence
- demoStorage.saveSubmission(submission);
-
- // Also call Cloud Function for processing with authentication and orgId
- const employee = employees.find(e => e.id === employeeId);
- const res = await apiPost('/submitEmployeeAnswers', {
- employeeId,
- answers,
- employee
- }, orgId);
-
- if (!res.ok) {
- const errorData = await res.json();
- throw new Error(errorData.error || 'Failed to submit to server');
- }
-
- // Update local state for UI with proper typing
- const convertedSubmission: Submission = {
- employeeId,
- answers: Object.entries(answers).map(([question, answer]) => ({
- question,
- answer
- }))
- };
- setSubmissions(prev => ({ ...prev, [employeeId]: convertedSubmission }));
- return true;
- } catch (e) {
- console.error('submitEmployeeAnswers error', e);
- return false;
- }
- }
-
- // Firebase mode: save to Firestore
try {
- const ref = doc(db, 'orgs', orgId, 'submissions', employeeId);
- await setDoc(ref, { ...answers, createdAt: Date.now() }, { merge: true });
+ if (!user?.uid) {
+ throw new Error('User authentication required');
+ }
+
+ // Use secure API for submission
+ await secureApi.submitEmployeeAnswers(orgId, employeeId, answers);
+
+ // Update local state for immediate UI feedback
+ const convertedSubmission: Submission = {
+ employeeId,
+ answers: Object.entries(answers).map(([question, answer]) => ({
+ question,
+ answer
+ }))
+ };
+ setSubmissions(prev => ({ ...prev, [employeeId]: convertedSubmission }));
return true;
} catch (e) {
console.error('submitEmployeeAnswers error', e);
@@ -648,6 +601,10 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
try {
console.log('generateEmployeeReport called for:', employee.name, 'in org:', orgId);
+ if (!user?.uid) {
+ throw new Error('User authentication required');
+ }
+
// Get submission data for this employee
const submission = submissions[employee.id];
if (!submission) {
@@ -659,7 +616,7 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
if (submission.answers) {
if (Array.isArray(submission.answers)) {
// If answers is an array of {question, answer} objects
- submissionAnswers = submission.answers.reduce((acc, item) => {
+ submissionAnswers = submission.answers.reduce((acc, item: any) => {
acc[item.question] = item.answer;
return acc;
}, {} as Record);
@@ -686,29 +643,13 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
console.warn('Could not fetch company report for context:', error);
}
- const res = await apiPost('/generateEmployeeReport', {
- employee,
- submission: submissionAnswers,
- companyWiki
- }, orgId);
+ const data = await secureApi.generateEmployeeReport(employee, submissionAnswers, companyWiki);
- if (!res.ok) {
- const errorData = await res.json();
- console.error('API error response:', errorData);
- throw new Error(errorData.error || 'Failed to generate employee report');
- }
-
- const json = await res.json();
- if (json.report) {
+ if ((data as any).report) {
console.log('Employee report generated successfully');
- setReports(prev => ({ ...prev, [employee.id]: json.report }));
-
- // Also save to persistent storage in demo mode
- if (!isFirebaseConfigured) {
- demoStorage.saveEmployeeReport(orgId, employee.id, json.report);
- }
-
- return json.report as Report;
+ const report = (data as any).report as Report;
+ setReports(prev => ({ ...prev, [employee.id]: report }));
+ return report;
} else {
throw new Error('No report data received from API');
}
@@ -717,45 +658,8 @@ export const OrgProvider: React.FC<{ children: React.ReactNode; selectedOrgId: s
throw e; // Re-throw to allow caller to handle
}
},
- getEmployeeReport: async (employeeId: string) => {
- try {
- if (isFirebaseConfigured && user) {
- // Firebase implementation
- const reportDoc = await getDoc(doc(db, 'organizations', orgId, 'employeeReports', employeeId));
- if (reportDoc.exists()) {
- return { success: true, report: reportDoc.data() };
- }
- return { success: false, error: 'Report not found' };
- } else {
- // Demo mode - call Cloud Function
- const response = await fetch(`${API_URL}/generateEmployeeReport?employeeId=${employeeId}`);
- const result = await response.json();
- return result;
- }
- } catch (error) {
- console.error('Error fetching employee report:', error);
- return { success: false, error: error.message };
- }
- },
-
- getEmployeeReports: async () => {
- try {
- if (isFirebaseConfigured && user) {
- // Firebase implementation
- const reportsSnapshot = await getDocs(collection(db, 'organizations', orgId, 'employeeReports'));
- const reports = reportsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
- return { success: true, reports };
- } else {
- // Demo mode - call Cloud Function
- const response = await fetch(`${API_URL}/generateEmployeeReport`);
- const result = await response.json();
- return result;
- }
- } catch (error) {
- console.error('Error fetching employee reports:', error);
- return { success: false, error: error.message };
- }
- },
+ getEmployeeReport,
+ getEmployeeReports,
};
return (
diff --git a/src/pages/Chat.tsx b/src/pages/Chat.tsx
index f574331..156c44b 100644
--- a/src/pages/Chat.tsx
+++ b/src/pages/Chat.tsx
@@ -1,13 +1,677 @@
-import React from 'react';
-import ChatLayout from '../components/chat/ChatLayout';
-import ChatEmptyState from '../components/chat/ChatEmptyState';
+import React, { useState, useRef, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useAuth } from '../contexts/AuthContext';
+import { useOrg } from '../contexts/OrgContext';
+import { apiPost } from '../services/api';
+import Sidebar from '../components/figma/Sidebar';
+
+interface Message {
+ id: string;
+ role: 'user' | 'assistant';
+ content: string;
+ timestamp: Date;
+ mentions?: Array<{ id: string; name: string }>;
+ attachments?: Array<{
+ name: string;
+ type: string;
+ size: number;
+ data?: string; // Base64 encoded file data
+ }>;
+}
+
+interface ChatState {
+ messages: Message[];
+ isLoading: boolean;
+ showEmployeeMenu: boolean;
+ mentionQuery: string;
+ mentionStartIndex: number;
+ selectedEmployeeIndex: number;
+ hasUploadedFiles: boolean;
+ uploadedFiles: Array<{
+ name: string;
+ type: string;
+ size: number;
+ data?: string; // Base64 encoded file data
+ }>;
+}
const Chat: React.FC = () => {
+ const { user } = useAuth();
+ const { employees, orgId, org } = useOrg();
+ const navigate = useNavigate();
+ const inputRef = useRef(null);
+ const fileInputRef = useRef(null);
+ const messagesEndRef = useRef(null);
+
+ const [state, setState] = useState({
+ messages: [],
+ isLoading: false,
+ showEmployeeMenu: false,
+ mentionQuery: '',
+ mentionStartIndex: -1,
+ selectedEmployeeIndex: 0,
+ hasUploadedFiles: false,
+ uploadedFiles: []
+ });
+
+ const [currentInput, setCurrentInput] = useState('');
+ const [selectedCategory, setSelectedCategory] = useState('Accountability');
+ const [isInputFocused, setIsInputFocused] = useState(false);
+
+ // Auto-resize textarea function
+ const adjustTextareaHeight = () => {
+ if (inputRef.current) {
+ inputRef.current.style.height = 'auto';
+ const scrollHeight = inputRef.current.scrollHeight;
+ const maxHeight = 150; // Maximum height in pixels
+ inputRef.current.style.height = `${Math.min(scrollHeight, maxHeight)}px`;
+ }
+ };
+
+ useEffect(() => {
+ if (!user) {
+ navigate('/login');
+ }
+ }, [user, navigate]);
+
+ // Auto-scroll to bottom when new messages arrive
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }, [state.messages]);
+
+ const questionStarters = [
+ "How can the company serve them better?",
+ "What are our team's main strengths?",
+ "Which areas need improvement?",
+ "How is employee satisfaction?"
+ ];
+
+ const categories = ['Accountability', 'Employee Growth', 'Customer Focus', 'Teamwork'];
+
+ // Enhanced filtering for Google-style autocomplete
+ const filteredEmployees = state.mentionQuery
+ ? employees.filter(emp => {
+ const query = state.mentionQuery.toLowerCase();
+ const nameWords = emp.name.toLowerCase().split(' ');
+ const email = emp.email.toLowerCase();
+
+ // Match if query starts any word in name, or is contained in email
+ return nameWords.some(word => word.startsWith(query)) ||
+ email.includes(query) ||
+ emp.name.toLowerCase().includes(query);
+ }).sort((a, b) => {
+ // Prioritize exact matches at start of name
+ const aStartsWithQuery = a.name.toLowerCase().startsWith(state.mentionQuery.toLowerCase());
+ const bStartsWithQuery = b.name.toLowerCase().startsWith(state.mentionQuery.toLowerCase());
+
+ if (aStartsWithQuery && !bStartsWithQuery) return -1;
+ if (!aStartsWithQuery && bStartsWithQuery) return 1;
+
+ // Then alphabetical
+ return a.name.localeCompare(b.name);
+ })
+ : employees.slice(0, 10); // Show max 10 when no query
+
+ const handleSendMessage = async () => {
+ if (!currentInput.trim() && state.uploadedFiles.length === 0) return;
+
+ const messageText = currentInput.trim();
+ const mentions: Array<{ id: string; name: string }> = [];
+
+ // Extract mentions from the message
+ const mentionRegex = /@(\w+(?:\s+\w+)*)/g;
+ let match;
+ while ((match = mentionRegex.exec(messageText)) !== null) {
+ const mentionedName = match[1];
+ const employee = employees.find(emp => emp.name === mentionedName);
+ if (employee) {
+ mentions.push({ id: employee.id, name: employee.name });
+ }
+ }
+
+ const newMessage: Message = {
+ id: Date.now().toString(),
+ role: 'user',
+ content: messageText,
+ timestamp: new Date(),
+ mentions,
+ attachments: state.uploadedFiles.length > 0 ? [...state.uploadedFiles] : undefined
+ };
+
+ setState(prev => ({
+ ...prev,
+ messages: [...prev.messages, newMessage],
+ isLoading: true,
+ // Clear uploaded files after sending
+ uploadedFiles: [],
+ hasUploadedFiles: false
+ }));
+
+ setCurrentInput('');
+
+ try {
+ // Get mentioned employees' data for context
+ const mentionedEmployees = mentions.map(mention =>
+ employees.find(emp => emp.id === mention.id)
+ ).filter(Boolean);
+
+ // Call actual AI API with full context
+ const res = await apiPost('/chat', {
+ message: messageText,
+ mentions: mentionedEmployees,
+ attachments: state.uploadedFiles.length > 0 ? state.uploadedFiles : undefined,
+ context: {
+ org: org,
+ employees: employees,
+ messageHistory: state.messages.slice(-5) // Last 5 messages for context
+ }
+ }, orgId);
+
+ if (!res.ok) {
+ const errorData = await res.json();
+ throw new Error(errorData.error || 'Failed to get AI response');
+ }
+
+ const data = await res.json();
+
+ const aiResponse: Message = {
+ id: (Date.now() + 1).toString(),
+ role: 'assistant',
+ content: data.response || 'I apologize, but I encountered an issue processing your request.',
+ timestamp: new Date()
+ };
+
+ setState(prev => ({
+ ...prev,
+ messages: [...prev.messages, aiResponse],
+ isLoading: false
+ }));
+ } catch (error) {
+ console.error('Chat API error:', error);
+
+ // Fallback response with context awareness
+ const fallbackMessage: Message = {
+ id: (Date.now() + 1).toString(),
+ role: 'assistant',
+ content: `I understand you're asking about ${mentions.length > 0 ? mentions.map(m => m.name).join(', ') : 'your team'}. I'm currently experiencing some connection issues, but I'd be happy to help you analyze employee data, company metrics, or provide insights about your organization once the connection is restored.`,
+ timestamp: new Date()
+ };
+
+ setState(prev => ({
+ ...prev,
+ messages: [...prev.messages, fallbackMessage],
+ isLoading: false
+ }));
+ }
+ };
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const value = e.target.value;
+ const cursorPosition = e.target.selectionStart;
+
+ setCurrentInput(value);
+
+ // Auto-resize textarea
+ setTimeout(adjustTextareaHeight, 0);
+
+ // Enhanced @ mention detection for real-time search
+ const beforeCursor = value.substring(0, cursorPosition);
+ const lastAtIndex = beforeCursor.lastIndexOf('@');
+
+ if (lastAtIndex !== -1) {
+ // Check if we're still within a mention context
+ const afterAt = beforeCursor.substring(lastAtIndex + 1);
+ const hasSpaceOrNewline = /[\s\n]/.test(afterAt);
+
+ if (!hasSpaceOrNewline) {
+ // We're in a mention - show menu and filter
+ setState(prev => ({
+ ...prev,
+ showEmployeeMenu: true,
+ mentionQuery: afterAt,
+ mentionStartIndex: lastAtIndex,
+ selectedEmployeeIndex: 0
+ }));
+ } else {
+ setState(prev => ({
+ ...prev,
+ showEmployeeMenu: false
+ }));
+ }
+ } else {
+ setState(prev => ({
+ ...prev,
+ showEmployeeMenu: false
+ }));
+ }
+ };
+
+ const handleEmployeeSelect = (employee: { id: string; name: string }) => {
+ if (state.mentionStartIndex === -1) return;
+
+ const beforeMention = currentInput.substring(0, state.mentionStartIndex);
+ const afterCursor = currentInput.substring(inputRef.current?.selectionStart || currentInput.length);
+ const newValue = `${beforeMention}@${employee.name} ${afterCursor}`;
+
+ setCurrentInput(newValue);
+ setState(prev => ({
+ ...prev,
+ showEmployeeMenu: false,
+ mentionQuery: '',
+ mentionStartIndex: -1
+ }));
+
+ // Focus back to input and position cursor after the mention
+ setTimeout(() => {
+ if (inputRef.current) {
+ const newCursorPosition = beforeMention.length + employee.name.length + 2;
+ inputRef.current.focus();
+ inputRef.current.setSelectionRange(newCursorPosition, newCursorPosition);
+ }
+ }, 0);
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (state.showEmployeeMenu && filteredEmployees.length > 0) {
+ switch (e.key) {
+ case 'ArrowDown':
+ e.preventDefault();
+ setState(prev => ({
+ ...prev,
+ selectedEmployeeIndex: prev.selectedEmployeeIndex < filteredEmployees.length - 1
+ ? prev.selectedEmployeeIndex + 1
+ : 0
+ }));
+ break;
+ case 'ArrowUp':
+ e.preventDefault();
+ setState(prev => ({
+ ...prev,
+ selectedEmployeeIndex: prev.selectedEmployeeIndex > 0
+ ? prev.selectedEmployeeIndex - 1
+ : filteredEmployees.length - 1
+ }));
+ break;
+ case 'Enter':
+ case 'Tab':
+ e.preventDefault();
+ if (filteredEmployees[state.selectedEmployeeIndex]) {
+ handleEmployeeSelect(filteredEmployees[state.selectedEmployeeIndex]);
+ }
+ break;
+ case 'Escape':
+ e.preventDefault();
+ setState(prev => ({
+ ...prev,
+ showEmployeeMenu: false
+ }));
+ break;
+ }
+ } else if (e.key === 'Enter' && !e.shiftKey && !e.altKey) {
+ e.preventDefault();
+ handleSendMessage();
+ }
+ // Allow Shift+Enter and Alt+Enter for line breaks (default behavior)
+ };
+
+ const handleFileUpload = async (e: React.ChangeEvent) => {
+ const files = Array.from(e.target.files || []);
+ if (files.length > 0) {
+ const uploadedFiles = await Promise.all(files.map(async (file) => {
+ // Convert file to base64 for API transmission
+ const base64 = await new Promise((resolve) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(reader.result as string);
+ reader.readAsDataURL(file);
+ });
+
+ return {
+ name: file.name,
+ type: file.type,
+ size: file.size,
+ data: base64 // Add the actual file data
+ };
+ }));
+
+ setState(prev => ({
+ ...prev,
+ hasUploadedFiles: true,
+ uploadedFiles: [...prev.uploadedFiles, ...uploadedFiles]
+ }));
+ }
+ };
+
+ const removeFile = (index: number) => {
+ setState(prev => ({
+ ...prev,
+ uploadedFiles: prev.uploadedFiles.filter((_, i) => i !== index),
+ hasUploadedFiles: prev.uploadedFiles.length > 1
+ }));
+ };
+
+ const handleQuestionClick = (question: string) => {
+ setCurrentInput(question);
+ };
+
+ const renderEmployeeMenu = () => {
+ if (!state.showEmployeeMenu || filteredEmployees.length === 0) return null;
+
+ return (
+
+ {state.mentionQuery && (
+
+ {filteredEmployees.length} employee{filteredEmployees.length !== 1 ? 's' : ''} found
+
+ )}
+ {filteredEmployees.map((employee, index) => (
+
handleEmployeeSelect({ id: employee.id, name: employee.name })}
+ onMouseEnter={() => setState(prev => ({ ...prev, selectedEmployeeIndex: index }))}
+ className={`px-3 py-2 rounded-xl flex items-center space-x-3 cursor-pointer transition-colors ${index === state.selectedEmployeeIndex
+ ? 'bg-[--Neutrals-NeutralSlate100] text-[--Neutrals-NeutralSlate950]'
+ : 'hover:bg-[--Neutrals-NeutralSlate50]'
+ }`}
+ >
+
+ {employee.initials || employee.name.split(' ').map(n => n[0]).join('').toUpperCase()}
+
+
+
+ {employee.name}
+
+
+ {employee.role || employee.email}
+
+
+
+ ))}
+
+ );
+ };
+
+ const renderUploadedFiles = () => {
+ if (state.uploadedFiles.length === 0) return null;
+
+ return (
+
+ {state.uploadedFiles.map((file, index) => (
+
+
+
+
removeFile(index)} className="cursor-pointer">
+
+
+
+
+ ))}
+
+ );
+ };
+
+ const renderChatInterface = () => {
+ if (state.messages.length === 0) {
+ return (
+
+
+
What would you like to understand?
+
+ {categories.map((category) => (
+
setSelectedCategory(category)}
+ className={`px-3 py-1.5 rounded-lg shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)] shadow-[inset_0px_-2px_0px_0px_rgba(10,13,18,0.05)] shadow-[inset_0px_0px_0px_1px_rgba(10,13,18,0.18)] flex justify-center items-center gap-1 overflow-hidden cursor-pointer ${selectedCategory === category ? 'bg-white' : ''
+ }`}
+ >
+
+
+ ))}
+
+
+
+ {questionStarters.map((question, index) => (
+
handleQuestionClick(question)}
+ className="flex-1 h-48 px-3 py-4 bg-[--Neutrals-NeutralSlate50] rounded-2xl inline-flex flex-col justify-between items-start overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate100]"
+ >
+
+
{question}
+
+ ))}
+
+
+
+ {/* Enhanced instructions for @ mentions */}
+
+
Ask about your team, company data, or get insights.
+
Use @ to mention team members.
+
+ {/* Sample questions */}
+
+
Try asking:
+
+
+ "How is the team performing overall?"
+
+
+ "What are the main strengths of our organization?"
+
+
+ "Tell me about @[employee name]'s recent feedback"
+
+
+
+
+
+ {renderChatInput()}
+
+ );
+ }
+
+ return (
+
+
+ {state.messages.map((message) => (
+
+
+
{message.content}
+ {message.attachments && message.attachments.length > 0 && (
+
+ {message.attachments.map((file, index) => (
+
+
+ {file.type.startsWith('image/') ? (
+
+ ) : (
+
+ )}
+
+
{file.name}
+
+ ))}
+
+ )}
+
+ {message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
+
+ {message.mentions && message.mentions.length > 0 && (
+
+ Mentioned: {message.mentions.map(m => m.name).join(', ')}
+
+ )}
+
+
+ ))}
+ {state.isLoading && (
+
+ )}
+
+
+ {renderChatInput()}
+
+ );
+ };
+
+ const renderChatInput = () => {
+ return (
+
+ {renderUploadedFiles()}
+
+
+ {currentInput || "Ask anything, use @ to tag staff and ask questions."}
+
+ {/* Custom blinking cursor when focused and has text */}
+ {currentInput && isInputFocused && (
+
+ )}
+ {/* Custom blinking cursor when focused and no text */}
+ {!currentInput && isInputFocused && (
+
+ )}
+
+
+
+
+
fileInputRef.current?.click()} className="cursor-pointer">
+
+
+
fileInputRef.current?.click()} className="cursor-pointer">
+
+
+
+
+
0
+ ? 'bg-[--Neutrals-NeutralSlate700]'
+ : 'bg-[--Neutrals-NeutralSlate400]'
+ }`}
+ >
+
+
+
+
+ {/* Enhanced help text for keyboard navigation */}
+
+ {state.showEmployeeMenu ? '↑↓ Navigate • Enter/Tab Select • Esc Cancel' : 'Enter to send • Shift+Enter new line'}
+
+
+ {renderEmployeeMenu()}
+
+ );
+ };
+
return (
-
-
-
+
+
+
+
+ {renderChatInterface()}
+
+
+
);
};
-export default Chat;
+export default Chat;
\ No newline at end of file
diff --git a/src/pages/ChatNew.tsx b/src/pages/ChatNew.tsx
deleted file mode 100644
index 22af019..0000000
--- a/src/pages/ChatNew.tsx
+++ /dev/null
@@ -1,677 +0,0 @@
-import React, { useState, useRef, useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
-import { useAuth } from '../contexts/AuthContext';
-import { useOrg } from '../contexts/OrgContext';
-import { apiPost } from '../services/api';
-import Sidebar from '../components/figma/Sidebar';
-
-interface Message {
- id: string;
- role: 'user' | 'assistant';
- content: string;
- timestamp: Date;
- mentions?: Array<{ id: string; name: string }>;
- attachments?: Array<{
- name: string;
- type: string;
- size: number;
- data?: string; // Base64 encoded file data
- }>;
-}
-
-interface ChatState {
- messages: Message[];
- isLoading: boolean;
- showEmployeeMenu: boolean;
- mentionQuery: string;
- mentionStartIndex: number;
- selectedEmployeeIndex: number;
- hasUploadedFiles: boolean;
- uploadedFiles: Array<{
- name: string;
- type: string;
- size: number;
- data?: string; // Base64 encoded file data
- }>;
-}
-
-const ChatNew: React.FC = () => {
- const { user } = useAuth();
- const { employees, orgId, org } = useOrg();
- const navigate = useNavigate();
- const inputRef = useRef(null);
- const fileInputRef = useRef(null);
- const messagesEndRef = useRef(null);
-
- const [state, setState] = useState({
- messages: [],
- isLoading: false,
- showEmployeeMenu: false,
- mentionQuery: '',
- mentionStartIndex: -1,
- selectedEmployeeIndex: 0,
- hasUploadedFiles: false,
- uploadedFiles: []
- });
-
- const [currentInput, setCurrentInput] = useState('');
- const [selectedCategory, setSelectedCategory] = useState('Accountability');
- const [isInputFocused, setIsInputFocused] = useState(false);
-
- // Auto-resize textarea function
- const adjustTextareaHeight = () => {
- if (inputRef.current) {
- inputRef.current.style.height = 'auto';
- const scrollHeight = inputRef.current.scrollHeight;
- const maxHeight = 150; // Maximum height in pixels
- inputRef.current.style.height = `${Math.min(scrollHeight, maxHeight)}px`;
- }
- };
-
- useEffect(() => {
- if (!user) {
- navigate('/login');
- }
- }, [user, navigate]);
-
- // Auto-scroll to bottom when new messages arrive
- useEffect(() => {
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
- }, [state.messages]);
-
- const questionStarters = [
- "How can the company serve them better?",
- "What are our team's main strengths?",
- "Which areas need improvement?",
- "How is employee satisfaction?"
- ];
-
- const categories = ['Accountability', 'Employee Growth', 'Customer Focus', 'Teamwork'];
-
- // Enhanced filtering for Google-style autocomplete
- const filteredEmployees = state.mentionQuery
- ? employees.filter(emp => {
- const query = state.mentionQuery.toLowerCase();
- const nameWords = emp.name.toLowerCase().split(' ');
- const email = emp.email.toLowerCase();
-
- // Match if query starts any word in name, or is contained in email
- return nameWords.some(word => word.startsWith(query)) ||
- email.includes(query) ||
- emp.name.toLowerCase().includes(query);
- }).sort((a, b) => {
- // Prioritize exact matches at start of name
- const aStartsWithQuery = a.name.toLowerCase().startsWith(state.mentionQuery.toLowerCase());
- const bStartsWithQuery = b.name.toLowerCase().startsWith(state.mentionQuery.toLowerCase());
-
- if (aStartsWithQuery && !bStartsWithQuery) return -1;
- if (!aStartsWithQuery && bStartsWithQuery) return 1;
-
- // Then alphabetical
- return a.name.localeCompare(b.name);
- })
- : employees.slice(0, 10); // Show max 10 when no query
-
- const handleSendMessage = async () => {
- if (!currentInput.trim() && state.uploadedFiles.length === 0) return;
-
- const messageText = currentInput.trim();
- const mentions: Array<{ id: string; name: string }> = [];
-
- // Extract mentions from the message
- const mentionRegex = /@(\w+(?:\s+\w+)*)/g;
- let match;
- while ((match = mentionRegex.exec(messageText)) !== null) {
- const mentionedName = match[1];
- const employee = employees.find(emp => emp.name === mentionedName);
- if (employee) {
- mentions.push({ id: employee.id, name: employee.name });
- }
- }
-
- const newMessage: Message = {
- id: Date.now().toString(),
- role: 'user',
- content: messageText,
- timestamp: new Date(),
- mentions,
- attachments: state.uploadedFiles.length > 0 ? [...state.uploadedFiles] : undefined
- };
-
- setState(prev => ({
- ...prev,
- messages: [...prev.messages, newMessage],
- isLoading: true,
- // Clear uploaded files after sending
- uploadedFiles: [],
- hasUploadedFiles: false
- }));
-
- setCurrentInput('');
-
- try {
- // Get mentioned employees' data for context
- const mentionedEmployees = mentions.map(mention =>
- employees.find(emp => emp.id === mention.id)
- ).filter(Boolean);
-
- // Call actual AI API with full context
- const res = await apiPost('/chat', {
- message: messageText,
- mentions: mentionedEmployees,
- attachments: state.uploadedFiles.length > 0 ? state.uploadedFiles : undefined,
- context: {
- org: org,
- employees: employees,
- messageHistory: state.messages.slice(-5) // Last 5 messages for context
- }
- }, orgId);
-
- if (!res.ok) {
- const errorData = await res.json();
- throw new Error(errorData.error || 'Failed to get AI response');
- }
-
- const data = await res.json();
-
- const aiResponse: Message = {
- id: (Date.now() + 1).toString(),
- role: 'assistant',
- content: data.response || 'I apologize, but I encountered an issue processing your request.',
- timestamp: new Date()
- };
-
- setState(prev => ({
- ...prev,
- messages: [...prev.messages, aiResponse],
- isLoading: false
- }));
- } catch (error) {
- console.error('Chat API error:', error);
-
- // Fallback response with context awareness
- const fallbackMessage: Message = {
- id: (Date.now() + 1).toString(),
- role: 'assistant',
- content: `I understand you're asking about ${mentions.length > 0 ? mentions.map(m => m.name).join(', ') : 'your team'}. I'm currently experiencing some connection issues, but I'd be happy to help you analyze employee data, company metrics, or provide insights about your organization once the connection is restored.`,
- timestamp: new Date()
- };
-
- setState(prev => ({
- ...prev,
- messages: [...prev.messages, fallbackMessage],
- isLoading: false
- }));
- }
- };
-
- const handleInputChange = (e: React.ChangeEvent) => {
- const value = e.target.value;
- const cursorPosition = e.target.selectionStart;
-
- setCurrentInput(value);
-
- // Auto-resize textarea
- setTimeout(adjustTextareaHeight, 0);
-
- // Enhanced @ mention detection for real-time search
- const beforeCursor = value.substring(0, cursorPosition);
- const lastAtIndex = beforeCursor.lastIndexOf('@');
-
- if (lastAtIndex !== -1) {
- // Check if we're still within a mention context
- const afterAt = beforeCursor.substring(lastAtIndex + 1);
- const hasSpaceOrNewline = /[\s\n]/.test(afterAt);
-
- if (!hasSpaceOrNewline) {
- // We're in a mention - show menu and filter
- setState(prev => ({
- ...prev,
- showEmployeeMenu: true,
- mentionQuery: afterAt,
- mentionStartIndex: lastAtIndex,
- selectedEmployeeIndex: 0
- }));
- } else {
- setState(prev => ({
- ...prev,
- showEmployeeMenu: false
- }));
- }
- } else {
- setState(prev => ({
- ...prev,
- showEmployeeMenu: false
- }));
- }
- };
-
- const handleEmployeeSelect = (employee: { id: string; name: string }) => {
- if (state.mentionStartIndex === -1) return;
-
- const beforeMention = currentInput.substring(0, state.mentionStartIndex);
- const afterCursor = currentInput.substring(inputRef.current?.selectionStart || currentInput.length);
- const newValue = `${beforeMention}@${employee.name} ${afterCursor}`;
-
- setCurrentInput(newValue);
- setState(prev => ({
- ...prev,
- showEmployeeMenu: false,
- mentionQuery: '',
- mentionStartIndex: -1
- }));
-
- // Focus back to input and position cursor after the mention
- setTimeout(() => {
- if (inputRef.current) {
- const newCursorPosition = beforeMention.length + employee.name.length + 2;
- inputRef.current.focus();
- inputRef.current.setSelectionRange(newCursorPosition, newCursorPosition);
- }
- }, 0);
- };
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (state.showEmployeeMenu && filteredEmployees.length > 0) {
- switch (e.key) {
- case 'ArrowDown':
- e.preventDefault();
- setState(prev => ({
- ...prev,
- selectedEmployeeIndex: prev.selectedEmployeeIndex < filteredEmployees.length - 1
- ? prev.selectedEmployeeIndex + 1
- : 0
- }));
- break;
- case 'ArrowUp':
- e.preventDefault();
- setState(prev => ({
- ...prev,
- selectedEmployeeIndex: prev.selectedEmployeeIndex > 0
- ? prev.selectedEmployeeIndex - 1
- : filteredEmployees.length - 1
- }));
- break;
- case 'Enter':
- case 'Tab':
- e.preventDefault();
- if (filteredEmployees[state.selectedEmployeeIndex]) {
- handleEmployeeSelect(filteredEmployees[state.selectedEmployeeIndex]);
- }
- break;
- case 'Escape':
- e.preventDefault();
- setState(prev => ({
- ...prev,
- showEmployeeMenu: false
- }));
- break;
- }
- } else if (e.key === 'Enter' && !e.shiftKey && !e.altKey) {
- e.preventDefault();
- handleSendMessage();
- }
- // Allow Shift+Enter and Alt+Enter for line breaks (default behavior)
- };
-
- const handleFileUpload = async (e: React.ChangeEvent) => {
- const files = Array.from(e.target.files || []);
- if (files.length > 0) {
- const uploadedFiles = await Promise.all(files.map(async (file) => {
- // Convert file to base64 for API transmission
- const base64 = await new Promise((resolve) => {
- const reader = new FileReader();
- reader.onload = () => resolve(reader.result as string);
- reader.readAsDataURL(file);
- });
-
- return {
- name: file.name,
- type: file.type,
- size: file.size,
- data: base64 // Add the actual file data
- };
- }));
-
- setState(prev => ({
- ...prev,
- hasUploadedFiles: true,
- uploadedFiles: [...prev.uploadedFiles, ...uploadedFiles]
- }));
- }
- };
-
- const removeFile = (index: number) => {
- setState(prev => ({
- ...prev,
- uploadedFiles: prev.uploadedFiles.filter((_, i) => i !== index),
- hasUploadedFiles: prev.uploadedFiles.length > 1
- }));
- };
-
- const handleQuestionClick = (question: string) => {
- setCurrentInput(question);
- };
-
- const renderEmployeeMenu = () => {
- if (!state.showEmployeeMenu || filteredEmployees.length === 0) return null;
-
- return (
-
- {state.mentionQuery && (
-
- {filteredEmployees.length} employee{filteredEmployees.length !== 1 ? 's' : ''} found
-
- )}
- {filteredEmployees.map((employee, index) => (
-
handleEmployeeSelect({ id: employee.id, name: employee.name })}
- onMouseEnter={() => setState(prev => ({ ...prev, selectedEmployeeIndex: index }))}
- className={`px-3 py-2 rounded-xl flex items-center space-x-3 cursor-pointer transition-colors ${index === state.selectedEmployeeIndex
- ? 'bg-[--Neutrals-NeutralSlate100] text-[--Neutrals-NeutralSlate950]'
- : 'hover:bg-[--Neutrals-NeutralSlate50]'
- }`}
- >
-
- {employee.initials || employee.name.split(' ').map(n => n[0]).join('').toUpperCase()}
-
-
-
- {employee.name}
-
-
- {employee.role || employee.email}
-
-
-
- ))}
-
- );
- };
-
- const renderUploadedFiles = () => {
- if (state.uploadedFiles.length === 0) return null;
-
- return (
-
- {state.uploadedFiles.map((file, index) => (
-
-
-
-
removeFile(index)} className="cursor-pointer">
-
-
-
-
- ))}
-
- );
- };
-
- const renderChatInterface = () => {
- if (state.messages.length === 0) {
- return (
-
-
-
What would you like to understand?
-
- {categories.map((category) => (
-
setSelectedCategory(category)}
- className={`px-3 py-1.5 rounded-lg shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)] shadow-[inset_0px_-2px_0px_0px_rgba(10,13,18,0.05)] shadow-[inset_0px_0px_0px_1px_rgba(10,13,18,0.18)] flex justify-center items-center gap-1 overflow-hidden cursor-pointer ${selectedCategory === category ? 'bg-white' : ''
- }`}
- >
-
-
- ))}
-
-
-
- {questionStarters.map((question, index) => (
-
handleQuestionClick(question)}
- className="flex-1 h-48 px-3 py-4 bg-[--Neutrals-NeutralSlate50] rounded-2xl inline-flex flex-col justify-between items-start overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate100]"
- >
-
-
{question}
-
- ))}
-
-
-
- {/* Enhanced instructions for @ mentions */}
-
-
Ask about your team, company data, or get insights.
-
Use @ to mention team members.
-
- {/* Sample questions */}
-
-
Try asking:
-
-
- "How is the team performing overall?"
-
-
- "What are the main strengths of our organization?"
-
-
- "Tell me about @[employee name]'s recent feedback"
-
-
-
-
-
- {renderChatInput()}
-
- );
- }
-
- return (
-
-
- {state.messages.map((message) => (
-
-
-
{message.content}
- {message.attachments && message.attachments.length > 0 && (
-
- {message.attachments.map((file, index) => (
-
-
- {file.type.startsWith('image/') ? (
-
- ) : (
-
- )}
-
-
{file.name}
-
- ))}
-
- )}
-
- {message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
-
- {message.mentions && message.mentions.length > 0 && (
-
- Mentioned: {message.mentions.map(m => m.name).join(', ')}
-
- )}
-
-
- ))}
- {state.isLoading && (
-
- )}
-
-
- {renderChatInput()}
-
- );
- };
-
- const renderChatInput = () => {
- return (
-
- {renderUploadedFiles()}
-
-
- {currentInput || "Ask anything, use @ to tag staff and ask questions."}
-
- {/* Custom blinking cursor when focused and has text */}
- {currentInput && isInputFocused && (
-
- )}
- {/* Custom blinking cursor when focused and no text */}
- {!currentInput && isInputFocused && (
-
- )}
-
-
-
-
-
fileInputRef.current?.click()} className="cursor-pointer">
-
-
-
fileInputRef.current?.click()} className="cursor-pointer">
-
-
-
-
-
0
- ? 'bg-[--Neutrals-NeutralSlate700]'
- : 'bg-[--Neutrals-NeutralSlate400]'
- }`}
- >
-
-
-
-
- {/* Enhanced help text for keyboard navigation */}
-
- {state.showEmployeeMenu ? '↑↓ Navigate • Enter/Tab Select • Esc Cancel' : 'Enter to send • Shift+Enter new line'}
-
-
- {renderEmployeeMenu()}
-
- );
- };
-
- return (
-
-
-
-
- {renderChatInterface()}
-
-
-
- );
-};
-
-export default ChatNew;
\ No newline at end of file
diff --git a/src/pages/CompanyWiki.tsx b/src/pages/CompanyWiki.tsx
index fd4acaf..31c4ab5 100644
--- a/src/pages/CompanyWiki.tsx
+++ b/src/pages/CompanyWiki.tsx
@@ -73,7 +73,7 @@ const CompanyWiki: React.FC = () => {
};
return (
-
+
{error && (
{error}
diff --git a/src/pages/EmployeeData.tsx b/src/pages/EmployeeData.tsx
index 57ef559..a7cba3e 100644
--- a/src/pages/EmployeeData.tsx
+++ b/src/pages/EmployeeData.tsx
@@ -7,7 +7,7 @@ import ScoreBarList from '../components/charts/ScoreBarList';
import { SAMPLE_COMPANY_REPORT } from '../constants';
import ReportDetail from './ReportDetail';
-interface EmployeeDataProps {
+interface EmployeeReportProps {
mode: 'submissions' | 'reports';
}
@@ -337,7 +337,7 @@ const EmployeeCard: React.FC<{
);
};
-const EmployeeData: React.FC
= ({ mode }) => {
+const EmployeeReport: React.FC = ({ mode }) => {
const { employees, reports, user, isOwner, getFullCompanyReportHistory, generateEmployeeReport, generateCompanyReport, saveReport, orgId } = useOrg();
const [companyReport, setCompanyReport] = useState(null);
const [generatingReports, setGeneratingReports] = useState>(new Set());
@@ -523,4 +523,4 @@ const EmployeeData: React.FC = ({ mode }) => {
);
};
-export default EmployeeData;
+export default EmployeeReport;
diff --git a/src/pages/EmployeeQuestionnaire.tsx b/src/pages/EmployeeQuestionnaire.tsx
index 7e0a95a..670b6af 100644
--- a/src/pages/EmployeeQuestionnaire.tsx
+++ b/src/pages/EmployeeQuestionnaire.tsx
@@ -28,7 +28,7 @@ const AuditlyIcon: React.FC = () => (
// Progress Bar Component for Section Headers
const SectionProgressBar: React.FC<{ currentSection: number; totalSections: number }> = ({ currentSection, totalSections }) => {
return (
-
+
{Array.from({ length: 7 }, (_, index) => {
const isActive = index === 0; // First step is always active for section start
return (
@@ -59,14 +59,14 @@ const YesNoChoice: React.FC<{
totalSteps?: number;
}> = ({ question, value, onChange, onBack, onNext, onSkip, currentStep, totalSteps }) => {
return (
-
+
-
{question}
+
{question}
onChange('No')}
- className={`w-20 h-20 relative rounded-[999px] overflow-hidden cursor-pointer transition-colors ${value === 'No' ? 'bg-Neutrals-NeutralSlate800' : 'bg-Neutrals-NeutralSlate100 hover:bg-Neutrals-NeutralSlate200'}`}
+ className={`w-20 h-20 relative rounded-[999px] overflow-hidden cursor-pointer transition-colors ${value === 'No' ? 'bg-Neutrals-NeutralSlate800' : 'bg-[--Neutrals-NeutralSlate100] hover:bg-Neutrals-NeutralSlate200'}`}
>
No
@@ -74,7 +74,7 @@ const YesNoChoice: React.FC<{
onChange('Yes')}
- className={`w-20 h-20 relative rounded-[999px] overflow-hidden cursor-pointer transition-colors ${value === 'Yes' ? 'bg-Neutrals-NeutralSlate800' : 'bg-Neutrals-NeutralSlate100 hover:bg-Neutrals-NeutralSlate200'}`}
+ className={`w-20 h-20 relative rounded-[999px] overflow-hidden cursor-pointer transition-colors ${value === 'Yes' ? 'bg-Neutrals-NeutralSlate800' : 'bg-[--Neutrals-NeutralSlate100] hover:bg-Neutrals-NeutralSlate200'}`}
>
Yes
@@ -86,10 +86,10 @@ const YesNoChoice: React.FC<{
{onBack && (
)}
@@ -109,21 +109,21 @@ const YesNoChoice: React.FC<{
{onSkip && (
)}
{/* Progress indicators */}
{currentStep && totalSteps && (
<>
-
-
{currentStep} of {totalSteps}
+
+
{currentStep} of {totalSteps}
-
Leadership & Organizational Structure
+
Leadership & Organizational Structure
>
)}
@@ -142,7 +142,7 @@ const SectionIntro: React.FC<{
}> = ({ sectionNumber, title, description, onStart, imageUrl = "https://placehold.co/560x682" }) => {
return (
-
+
@@ -152,11 +152,11 @@ const SectionIntro: React.FC<{
-
-
{sectionNumber}
+
-
{title}
-
{description}
+
{title}
+
{description}