update the chat to use the new chat, fix file uploads, mentions, and message area scaling
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -58,3 +58,4 @@ dist-ssr
|
|||||||
/deprecated
|
/deprecated
|
||||||
/figma-code
|
/figma-code
|
||||||
/*ignore.*
|
/*ignore.*
|
||||||
|
/document.svg
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
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<ChatLayoutProps> = ({ children }) => {
|
|
||||||
const { employees } = useOrg();
|
|
||||||
const [selectedEmployees, setSelectedEmployees] = useState<Employee[]>([]);
|
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
|
||||||
const [inputValue, setInputValue] = useState('');
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(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 (
|
|
||||||
<div className="w-full h-screen bg-Neutrals-NeutralSlate0 inline-flex overflow-hidden">
|
|
||||||
{/* Sidebar */}
|
|
||||||
<ChatSidebar currentPage="chat" onNavigate={handleNavigation} />
|
|
||||||
|
|
||||||
{/* Main Content Area */}
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
|
||||||
{/* Header with Employee Selection */}
|
|
||||||
<div className="px-6 py-4 bg-Neutrals-NeutralSlate0 border-b border-Neutrals-NeutralSlate200 flex justify-between items-center">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<h1 className="text-xl font-semibold text-Neutrals-NeutralSlate950">Chat</h1>
|
|
||||||
{selectedEmployees.length > 0 && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-sm text-Neutrals-NeutralSlate500">Analyzing:</span>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{selectedEmployees.slice(0, 3).map((emp, index) => (
|
|
||||||
<div key={emp.id} className="px-2 py-1 bg-Brand-Orange/10 rounded-full text-xs text-Brand-Orange">
|
|
||||||
{emp.name}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{selectedEmployees.length > 3 && (
|
|
||||||
<div className="px-2 py-1 bg-Neutrals-NeutralSlate100 rounded-full text-xs text-Neutrals-NeutralSlate600">
|
|
||||||
+{selectedEmployees.length - 3} more
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Messages Area */}
|
|
||||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
|
||||||
{hasMessages ? (
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
<MessageThread
|
|
||||||
messages={messages}
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
<div ref={messagesEndRef} />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
children
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Input Area */}
|
|
||||||
<div className="px-6 py-4 bg-Neutrals-NeutralSlate0 border-t border-Neutrals-NeutralSlate200">
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
<div className="flex items-end gap-3">
|
|
||||||
<FileUploadInput
|
|
||||||
value={inputValue}
|
|
||||||
onChange={setInputValue}
|
|
||||||
onKeyDown={handleKeyPress}
|
|
||||||
placeholder="Ask about your team's performance, culture, or any insights..."
|
|
||||||
disabled={isLoading}
|
|
||||||
uploadedFiles={uploadedFiles}
|
|
||||||
onRemoveFile={handleRemoveFile}
|
|
||||||
onFilesSelected={handleFilesSelected}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Send Button */}
|
|
||||||
<button
|
|
||||||
onClick={handleSendMessage}
|
|
||||||
disabled={!inputValue.trim() && uploadedFiles.length === 0}
|
|
||||||
className="px-4 py-3 bg-Brand-Orange text-white rounded-xl hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex-shrink-0"
|
|
||||||
>
|
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M18.3346 1.66797L9.16797 10.8346M18.3346 1.66797L12.5013 18.3346L9.16797 10.8346M18.3346 1.66797L1.66797 7.5013L9.16797 10.8346" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChatLayout;
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useOrg } from '../../contexts/OrgContext';
|
|
||||||
|
|
||||||
interface NavItemProps {
|
|
||||||
icon: React.ReactNode;
|
|
||||||
label: string;
|
|
||||||
isActive?: boolean;
|
|
||||||
onClick?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NavItem: React.FC<NavItemProps> = ({ icon, label, isActive, onClick }) => (
|
|
||||||
<div
|
|
||||||
className={`w-60 px-4 py-2.5 ${isActive ? 'bg-Neutrals-NeutralSlate100' : ''} rounded-[34px] inline-flex justify-start items-center gap-2 cursor-pointer hover:bg-Neutrals-NeutralSlate50 transition-colors`}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
<div className={`justify-start text-sm font-medium font-['Inter'] leading-tight ${isActive ? 'text-Neutrals-NeutralSlate950' : 'text-Neutrals-NeutralSlate500'}`}>
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
interface ChatSidebarProps {
|
|
||||||
currentPage?: string;
|
|
||||||
onNavigate?: (page: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChatSidebar: React.FC<ChatSidebarProps> = ({ currentPage = 'chat', onNavigate }) => {
|
|
||||||
const { org } = useOrg();
|
|
||||||
|
|
||||||
const handleNavigation = (page: string) => {
|
|
||||||
if (onNavigate) {
|
|
||||||
onNavigate(page);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-64 self-stretch max-w-64 min-w-64 px-3 pt-4 pb-3 bg-Neutrals-NeutralSlate0 border-r border-Neutrals-NeutralSlate200 inline-flex flex-col justify-between items-center overflow-hidden">
|
|
||||||
{/* Company Selection */}
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-5">
|
|
||||||
<div className="w-60 pl-2 pr-4 py-2 bg-Neutrals-NeutralSlate0 rounded-3xl outline outline-1 outline-offset-[-1px] outline-Neutrals-NeutralSlate200 inline-flex justify-between items-center overflow-hidden">
|
|
||||||
<div className="flex-1 flex justify-start items-center gap-2">
|
|
||||||
<div className="w-8 h-8 rounded-full flex justify-start items-center gap-2.5">
|
|
||||||
<div className="w-8 h-8 relative bg-Brand-Orange rounded-full outline outline-[1.60px] outline-offset-[-1.60px] outline-white/10 overflow-hidden">
|
|
||||||
<div className="w-8 h-8 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
|
||||||
<div className="left-[8.80px] top-[7.20px] absolute">
|
|
||||||
{/* Company Icon SVG */}
|
|
||||||
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g filter="url(#filter0_d_1042_694)">
|
|
||||||
<path opacity="0.5" fillRule="evenodd" clipRule="evenodd" d="M4.34342 10.6855C4.66998 11.0162 4.66998 11.5524 4.34341 11.8831L4.32669 11.9C4.00012 12.2307 3.47065 12.2307 3.14409 11.9C2.81753 11.5693 2.81753 11.0331 3.1441 10.7024L3.16082 10.6855C3.48739 10.3548 4.01686 10.3548 4.34342 10.6855Z" fill="url(#paint0_linear_1042_694)" />
|
|
||||||
<path opacity="0.7" fillRule="evenodd" clipRule="evenodd" d="M8.27545 10.9405C8.60142 11.2718 8.60046 11.808 8.27331 12.1381L5.95697 14.4752C5.62981 14.8053 5.10035 14.8043 4.77437 14.473C4.4484 14.1417 4.44936 13.6056 4.77651 13.2755L7.09285 10.9383C7.42001 10.6082 7.94947 10.6092 8.27545 10.9405Z" fill="url(#paint1_linear_1042_694)" />
|
|
||||||
<path opacity="0.5" fillRule="evenodd" clipRule="evenodd" d="M11.4179 14.9631C11.6741 14.574 12.1932 14.4688 12.5775 14.7282L12.6277 14.7621C13.012 15.0215 13.1158 15.5473 12.8596 15.9364C12.6034 16.3255 12.0842 16.4307 11.7 16.1713L11.6498 16.1374C11.2655 15.878 11.1617 15.3522 11.4179 14.9631Z" fill="url(#paint2_linear_1042_694)" />
|
|
||||||
<path opacity="0.7" fillRule="evenodd" clipRule="evenodd" d="M16.9376 10.6347C17.2642 10.9654 17.2642 11.5016 16.9376 11.8323L15.8003 12.9839C15.4738 13.3146 14.9443 13.3146 14.6177 12.9839C14.2912 12.6532 14.2912 12.1171 14.6177 11.7864L15.755 10.6347C16.0816 10.304 16.611 10.304 16.9376 10.6347Z" fill="url(#paint3_linear_1042_694)" />
|
|
||||||
<path fillRule="evenodd" clipRule="evenodd" d="M16.9544 6.37693C17.2809 6.70762 17.2809 7.24378 16.9544 7.57447L8.55033 16.0847C8.22376 16.4154 7.69429 16.4154 7.36773 16.0847C7.04116 15.754 7.04116 15.2179 7.36773 14.8872L15.7718 6.37693C16.0983 6.04623 16.6278 6.04623 16.9544 6.37693Z" fill="url(#paint4_linear_1042_694)" />
|
|
||||||
<path fillRule="evenodd" clipRule="evenodd" d="M15.3649 3.75974C15.6915 4.09043 15.6915 4.62659 15.3649 4.95728L10.5315 9.85174C10.205 10.1824 9.67549 10.1824 9.34893 9.85174C9.02236 9.52104 9.02236 8.98489 9.34893 8.65419L14.1823 3.75974C14.5089 3.42905 15.0383 3.42905 15.3649 3.75974Z" fill="url(#paint5_linear_1042_694)" />
|
|
||||||
<path fillRule="evenodd" clipRule="evenodd" d="M12.8146 2.09918C13.1414 2.42965 13.1417 2.96581 12.8154 3.29672L6.60224 9.59685C6.27589 9.92777 5.74642 9.92813 5.41964 9.59766C5.09285 9.26719 5.0925 8.73103 5.41884 8.40011L11.632 2.09998C11.9583 1.76907 12.4878 1.76871 12.8146 2.09918Z" fill="url(#paint6_linear_1042_694)" />
|
|
||||||
<path opacity="0.7" fillRule="evenodd" clipRule="evenodd" d="M6.66127 4.11624C6.98727 4.4475 6.98636 4.98366 6.65923 5.31378L4.22582 7.76948C3.89869 8.0996 3.36923 8.09868 3.04322 7.76741C2.71722 7.43615 2.71813 6.9 3.04526 6.56987L5.47867 4.11418C5.8058 3.78405 6.33526 3.78498 6.66127 4.11624Z" fill="url(#paint7_linear_1042_694)" />
|
|
||||||
<path opacity="0.5" fillRule="evenodd" clipRule="evenodd" d="M8.15116 1.66406C8.613 1.66406 8.98739 2.04318 8.98739 2.51085V2.59553C8.98739 3.0632 8.613 3.44232 8.15116 3.44232C7.68933 3.44232 7.31494 3.0632 7.31494 2.59553V2.51085C7.31494 2.04318 7.68933 1.66406 8.15116 1.66406Z" fill="url(#paint8_linear_1042_694)" />
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<filter id="filter0_d_1042_694" x="0.399316" y="-0.400781" width="19.2006" height="22.4016" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
|
|
||||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
|
||||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
|
||||||
<feMorphology radius="1.2" operator="erode" in="SourceAlpha" result="effect1_dropShadow_1042_694" />
|
|
||||||
<feOffset dy="1.8" />
|
|
||||||
<feGaussianBlur stdDeviation="1.8" />
|
|
||||||
<feComposite in2="hardAlpha" operator="out" />
|
|
||||||
<feColorMatrix type="matrix" values="0 0 0 0 0.141176 0 0 0 0 0.141176 0 0 0 0 0.141176 0 0 0 0.1 0" />
|
|
||||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1042_694" />
|
|
||||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1042_694" result="shape" />
|
|
||||||
</filter>
|
|
||||||
<linearGradient id="paint0_linear_1042_694" x1="3.74376" y1="10.4375" x2="3.74376" y2="12.148" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint1_linear_1042_694" x1="6.52491" y1="10.6914" x2="6.52491" y2="14.7221" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint2_linear_1042_694" x1="12.1387" y1="14.5859" x2="12.1387" y2="16.3136" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint3_linear_1042_694" x1="15.7777" y1="10.3867" x2="15.7777" y2="13.2319" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint4_linear_1042_694" x1="12.161" y1="6.12891" x2="12.161" y2="16.3327" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint5_linear_1042_694" x1="12.3569" y1="3.51172" x2="12.3569" y2="10.0998" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint6_linear_1042_694" x1="9.11711" y1="1.85156" x2="9.11711" y2="9.84527" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint7_linear_1042_694" x1="4.85224" y1="3.86719" x2="4.85224" y2="8.01647" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint8_linear_1042_694" x1="8.15117" y1="1.66406" x2="8.15117" y2="3.44232" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 inline-flex flex-col justify-start items-start gap-0.5">
|
|
||||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate950 text-base font-medium font-['Inter'] leading-normal">
|
|
||||||
{org?.name || 'Zitlac Media'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M5.83325 12.4987L9.99992 16.6654L14.1666 12.4987M5.83325 7.4987L9.99992 3.33203L14.1666 7.4987" stroke="var(--Neutrals-NeutralSlate400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Navigation Menu */}
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-5">
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-1.5">
|
|
||||||
<NavItem
|
|
||||||
icon={
|
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M7.5 17.5016V11.3349C7.5 10.8682 7.5 10.6348 7.59083 10.4566C7.67072 10.2998 7.79821 10.1723 7.95501 10.0924C8.13327 10.0016 8.36662 10.0016 8.83333 10.0016H11.1667C11.6334 10.0016 11.8667 10.0016 12.045 10.0924C12.2018 10.1723 12.3293 10.2998 12.4092 10.4566C12.5 10.6348 12.5 10.8682 12.5 11.3349V17.5016M9.18141 2.30492L3.52949 6.70086C3.15168 6.99471 2.96278 7.14163 2.82669 7.32563C2.70614 7.48862 2.61633 7.67224 2.56169 7.86746C2.5 8.08785 2.5 8.32717 2.5 8.8058V14.8349C2.5 15.7683 2.5 16.235 2.68166 16.5916C2.84144 16.9052 3.09641 17.1601 3.41002 17.3199C3.76654 17.5016 4.23325 17.5016 5.16667 17.5016H14.8333C15.7668 17.5016 16.2335 17.5016 16.59 17.3199C16.9036 17.1601 17.1586 16.9052 17.3183 16.5916C17.5 16.235 17.5 15.7683 17.5 14.8349V8.8058C17.5 8.32717 17.5 8.08785 17.4383 7.86746C17.3837 7.67224 17.2939 7.48862 17.1733 7.32563C17.0372 7.14163 16.8483 6.99471 16.4705 6.70086L10.8186 2.30492C10.5258 2.07721 10.3794 1.96335 10.2178 1.91959C10.0752 1.88097 9.92484 1.88097 9.78221 1.91959C9.62057 1.96335 9.47418 2.07721 9.18141 2.30492Z" stroke="var(--Neutrals-NeutralSlate400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
label="Company Wiki"
|
|
||||||
isActive={currentPage === 'wiki'}
|
|
||||||
onClick={() => handleNavigation('wiki')}
|
|
||||||
/>
|
|
||||||
<NavItem
|
|
||||||
icon={
|
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M11.6666 9.16797H6.66659M8.33325 12.5013H6.66659M13.3333 5.83464H6.66659M16.6666 5.66797V14.3346C16.6666 15.7348 16.6666 16.4348 16.3941 16.9696C16.1544 17.44 15.772 17.8225 15.3016 18.0622C14.7668 18.3346 14.0667 18.3346 12.6666 18.3346H7.33325C5.93312 18.3346 5.23306 18.3346 4.69828 18.0622C4.22787 17.8225 3.84542 17.44 3.60574 16.9696C3.33325 16.4348 3.33325 15.7348 3.33325 14.3346V5.66797C3.33325 4.26784 3.33325 3.56777 3.60574 3.03299C3.84542 2.56259 4.22787 2.18014 4.69828 1.94045C5.23306 1.66797 5.93312 1.66797 7.33325 1.66797H12.6666C14.0667 1.66797 14.7668 1.66797 15.3016 1.94045C15.772 2.18014 16.1544 2.56259 16.3941 3.03299C16.6666 3.56777 16.6666 4.26784 16.6666 5.66797Z" stroke="var(--Neutrals-NeutralSlate400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
label="Submissions"
|
|
||||||
isActive={currentPage === 'submissions'}
|
|
||||||
onClick={() => handleNavigation('submissions')}
|
|
||||||
/>
|
|
||||||
<NavItem
|
|
||||||
icon={
|
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clipPath="url(#clip0_1042_720)">
|
|
||||||
<path d="M10.0001 1.66797C11.0944 1.66797 12.1781 1.88352 13.1891 2.30231C14.2002 2.7211 15.1188 3.33493 15.8926 4.10875C16.6665 4.88257 17.2803 5.80123 17.6991 6.81228C18.1179 7.82332 18.3334 8.90696 18.3334 10.0013M10.0001 1.66797V10.0013M10.0001 1.66797C5.39771 1.66797 1.66675 5.39893 1.66675 10.0013C1.66675 14.6037 5.39771 18.3346 10.0001 18.3346C14.6025 18.3346 18.3334 14.6037 18.3334 10.0013M10.0001 1.66797C14.6025 1.66797 18.3334 5.39893 18.3334 10.0013M18.3334 10.0013L10.0001 10.0013M18.3334 10.0013C18.3334 11.3164 18.0222 12.6128 17.4251 13.7846C16.8281 14.9563 15.9622 15.9701 14.8983 16.7431L10.0001 10.0013" stroke="var(--Neutrals-NeutralSlate400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_1042_720">
|
|
||||||
<rect width="20" height="20" fill="white" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
label="Reports"
|
|
||||||
isActive={currentPage === 'reports'}
|
|
||||||
onClick={() => handleNavigation('reports')}
|
|
||||||
/>
|
|
||||||
<NavItem
|
|
||||||
icon={
|
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M17.4996 9.58333C17.4996 13.4953 14.3283 16.6667 10.4163 16.6667C9.51896 16.6667 8.66061 16.4998 7.87057 16.1954C7.72612 16.1398 7.6539 16.112 7.59647 16.0987C7.53998 16.0857 7.49908 16.0803 7.44116 16.0781C7.38226 16.0758 7.31764 16.0825 7.18841 16.0958L2.92089 16.537C2.51402 16.579 2.31059 16.6001 2.19058 16.5269C2.08606 16.4631 2.01487 16.3566 1.99592 16.2356C1.97416 16.0968 2.07138 15.9168 2.2658 15.557L3.62885 13.034C3.7411 12.8262 3.79723 12.7223 3.82265 12.6225C3.84776 12.5238 3.85383 12.4527 3.8458 12.3512C3.83766 12.2484 3.79258 12.1147 3.70241 11.8472C3.46281 11.1363 3.33294 10.375 3.33294 9.58333C3.33294 5.67132 6.50426 2.5 10.4163 2.5C14.3283 2.5 17.4996 5.67132 17.4996 9.58333Z" stroke="var(--Brand-Orange, #3399FF)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
label="Chat"
|
|
||||||
isActive={currentPage === 'chat'}
|
|
||||||
onClick={() => handleNavigation('chat')}
|
|
||||||
/>
|
|
||||||
<NavItem
|
|
||||||
icon={
|
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clipPath="url(#clip0_1042_728)">
|
|
||||||
<path d="M7.57508 7.5013C7.771 6.94436 8.15771 6.47472 8.66671 6.17558C9.17571 5.87643 9.77416 5.76708 10.3561 5.8669C10.938 5.96671 11.4658 6.26924 11.846 6.72091C12.2262 7.17258 12.4343 7.74424 12.4334 8.33464C12.4334 10.0013 9.93342 10.8346 9.93342 10.8346M10.0001 14.168H10.0084M18.3334 10.0013C18.3334 14.6037 14.6025 18.3346 10.0001 18.3346C5.39771 18.3346 1.66675 14.6037 1.66675 10.0013C1.66675 5.39893 5.39771 1.66797 10.0001 1.66797C14.6025 1.66797 18.3334 5.39893 18.3334 10.0013Z" stroke="var(--Neutrals-NeutralSlate400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_1042_728">
|
|
||||||
<rect width="20" height="20" fill="white" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
label="Help"
|
|
||||||
isActive={currentPage === 'help'}
|
|
||||||
onClick={() => handleNavigation('help')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bottom Section with Settings and Company Report Builder */}
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
|
||||||
<NavItem
|
|
||||||
icon={
|
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clipPath="url(#clip0_1042_733)">
|
|
||||||
<path d="M10.0001 12.5013C11.3808 12.5013 12.5001 11.382 12.5001 10.0013C12.5001 8.62059 11.3808 7.5013 10.0001 7.5013C8.61937 7.5013 7.50008 8.62059 7.50008 10.0013C7.50008 11.382 8.61937 12.5013 10.0001 12.5013Z" stroke="var(--Neutrals-NeutralSlate400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
<path d="M15.6061 12.274C15.5053 12.5025 15.4752 12.756 15.5198 13.0017C15.5643 13.2475 15.6815 13.4743 15.8561 13.6528L15.9016 13.6983C16.0425 13.839 16.1542 14.0061 16.2305 14.19C16.3067 14.374 16.346 14.5711 16.346 14.7702C16.346 14.9694 16.3067 15.1665 16.2305 15.3505C16.1542 15.5344 16.0425 15.7015 15.9016 15.8422C15.7609 15.9831 15.5938 16.0948 15.4098 16.1711C15.2259 16.2473 15.0287 16.2866 14.8296 16.2866C14.6305 16.2866 14.4334 16.2473 14.2494 16.1711C14.0655 16.0948 13.8984 15.9831 13.7577 15.8422L13.7122 15.7968C13.5337 15.6221 13.3069 15.505 13.0611 15.4604C12.8154 15.4158 12.5619 15.4459 12.3334 15.5468C12.1093 15.6428 11.9183 15.8022 11.7836 16.0055C11.649 16.2087 11.5768 16.4469 11.5758 16.6907V16.8195C11.5758 17.2213 11.4162 17.6067 11.1321 17.8909C10.8479 18.175 10.4625 18.3346 10.0607 18.3346C9.65884 18.3346 9.27346 18.175 8.98931 17.8909C8.70517 17.6067 8.54554 17.2213 8.54554 16.8195V16.7513C8.53967 16.5005 8.45851 16.2574 8.31259 16.0533C8.16667 15.8493 7.96276 15.6939 7.72735 15.6074C7.49886 15.5065 7.24539 15.4764 6.99964 15.521C6.75388 15.5656 6.52711 15.6827 6.34857 15.8574L6.30311 15.9028C6.1624 16.0437 5.99529 16.1554 5.81135 16.2317C5.62742 16.3079 5.43026 16.3472 5.23114 16.3472C5.03203 16.3472 4.83487 16.3079 4.65093 16.2317C4.46699 16.1554 4.29989 16.0437 4.15917 15.9028C4.0183 15.7621 3.90654 15.595 3.83029 15.4111C3.75405 15.2271 3.7148 15.03 3.7148 14.8308C3.7148 14.6317 3.75405 14.4346 3.83029 14.2506C3.90654 14.0667 4.0183 13.8996 4.15917 13.7589L4.20463 13.7134C4.37928 13.5349 4.49643 13.3081 4.54099 13.0624C4.58555 12.8166 4.55547 12.5631 4.45463 12.3346C4.35859 12.1106 4.19914 11.9195 3.99589 11.7849C3.79264 11.6503 3.55447 11.578 3.31069 11.5771H3.1819C2.78006 11.5771 2.39467 11.4174 2.11053 11.1333C1.82638 10.8491 1.66675 10.4638 1.66675 10.0619C1.66675 9.66007 1.82638 9.27468 2.11053 8.99053C2.39467 8.70639 2.78006 8.54676 3.1819 8.54676H3.25008C3.50083 8.54089 3.74402 8.45973 3.94804 8.31381C4.15205 8.1679 4.30744 7.96398 4.39402 7.72858C4.49487 7.50008 4.52495 7.24661 4.48039 7.00086C4.43583 6.7551 4.31867 6.52833 4.14402 6.34979L4.09857 6.30433C3.95769 6.16362 3.84594 5.99651 3.76969 5.81258C3.69344 5.62864 3.65419 5.43148 3.65419 5.23236C3.65419 5.03325 3.69344 4.83609 3.76969 4.65215C3.84594 4.46821 3.95769 4.30111 4.09857 4.16039C4.23928 4.01952 4.40639 3.90776 4.59032 3.83151C4.77426 3.75527 4.97142 3.71602 5.17054 3.71602C5.36965 3.71602 5.56681 3.75527 5.75075 3.83151C5.93469 3.90776 6.10179 4.01952 6.24251 4.16039L6.28796 4.20585C6.46651 4.3805 6.69328 4.49765 6.93903 4.54221C7.18478 4.58677 7.43825 4.55669 7.66675 4.45585H7.72735C7.95142 4.35982 8.14252 4.20036 8.27712 3.99711C8.41172 3.79386 8.48396 3.55569 8.48493 3.31191V3.18312C8.48493 2.78128 8.64456 2.39589 8.92871 2.11175C9.21285 1.8276 9.59824 1.66797 10.0001 1.66797C10.4019 1.66797 10.7873 1.8276 11.0715 2.11175C11.3556 2.39589 11.5152 2.78128 11.5152 3.18312V3.2513C11.5162 3.49508 11.5884 3.73325 11.723 3.9365C11.8576 4.13975 12.0487 4.29921 12.2728 4.39524C12.5013 4.49609 12.7548 4.52617 13.0005 4.48161C13.2463 4.43705 13.4731 4.31989 13.6516 4.14524L13.6971 4.09979C13.8378 3.95891 14.0049 3.84716 14.1888 3.77091C14.3727 3.69466 14.5699 3.65541 14.769 3.65541C14.9681 3.65541 15.1653 3.69466 15.3492 3.77091C15.5332 3.84716 15.7003 3.95891 15.841 4.09979C15.9819 4.2405 16.0936 4.40761 16.1699 4.59154C16.2461 4.77548 16.2854 4.97264 16.2854 5.17176C16.2854 5.37087 16.2461 5.56803 16.1699 5.75197C16.0936 5.93591 15.9819 6.10301 15.841 6.24373L15.7955 6.28918C15.6209 6.46773 15.5037 6.6945 15.4592 6.94025C15.4146 7.186 15.4447 7.43947 15.5455 7.66797V7.72858C15.6416 7.95264 15.801 8.14374 16.0043 8.27834C16.2075 8.41295 16.4457 8.48518 16.6895 8.48615H16.8183C17.2201 8.48615 17.6055 8.64578 17.8896 8.92993C18.1738 9.21407 18.3334 9.59946 18.3334 10.0013C18.3334 10.4031 18.1738 10.7885 17.8896 11.0727C17.6055 11.3568 17.2201 11.5165 16.8183 11.5165H16.7501C16.5063 11.5174 16.2681 11.5897 16.0649 11.7243C15.8616 11.8589 15.7022 12.05 15.6061 12.274Z" stroke="var(--Neutrals-NeutralSlate400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_1042_733">
|
|
||||||
<rect width="20" height="20" fill="white" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
label="Settings"
|
|
||||||
isActive={currentPage === 'settings'}
|
|
||||||
onClick={() => handleNavigation('settings')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Company Report Builder Card */}
|
|
||||||
<div className="self-stretch bg-Neutrals-NeutralSlate0 rounded-[20px] shadow-[0px_1px_4px_0px_rgba(14,18,27,0.04)] outline outline-1 outline-offset-[-1px] outline-Neutrals-NeutralSlate200 flex flex-col justify-start items-start overflow-hidden">
|
|
||||||
<div className="self-stretch h-24 relative">
|
|
||||||
<div className="w-60 h-32 left-0 top-[-0.50px] absolute bg-gradient-to-b from-black to-black/0" />
|
|
||||||
<div className="w-60 p-3 left-[18.12px] top-[42.52px] absolute origin-top-left rotate-[-28.34deg] bg-Neutrals-NeutralSlate0 rounded-xl shadow-[0px_10px_20px_4px_rgba(14,18,27,0.08)] outline outline-1 outline-offset-[-1px] outline-Neutrals-NeutralSlate200 inline-flex flex-col justify-start items-start gap-3 overflow-hidden" />
|
|
||||||
<div className="w-60 p-3 left-[31.44px] top-[22px] absolute origin-top-left rotate-[-28.34deg] bg-Neutrals-NeutralSlate0 rounded-xl shadow-[0px_10px_20px_4px_rgba(14,18,27,0.08)] outline outline-1 outline-offset-[-1px] outline-Neutrals-NeutralSlate200 inline-flex flex-col justify-start items-start gap-3 overflow-hidden" />
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch p-3 flex flex-col justify-start items-start gap-1">
|
|
||||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate800 text-sm font-semibold font-['Inter'] leading-tight">
|
|
||||||
Build {org?.name || '[Company]'}'s Report
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate500 text-xs font-normal font-['Inter'] leading-none">
|
|
||||||
Share this form with your team members to capture valuable info about your company to train Auditly.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch px-3 pb-3 flex flex-col justify-start items-start gap-8">
|
|
||||||
<div className="self-stretch inline-flex justify-start items-start gap-2">
|
|
||||||
<div className="flex-1 px-3 py-1.5 bg-Button-Secondary rounded-[999px] flex justify-center items-center gap-0.5 overflow-hidden">
|
|
||||||
<div className="px-1 flex justify-center items-center">
|
|
||||||
<div className="justify-center text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">Invite</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M7.99992 3.33203V12.6654M3.33325 7.9987H12.6666" stroke="var(--Neutrals-NeutralSlate950, #0A0D12)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 px-3 py-1.5 bg-Brand-Orange rounded-[999px] outline outline-1 outline-offset-[-1px] outline-blue-400 flex justify-center items-center gap-0.5 overflow-hidden">
|
|
||||||
<div className="relative">
|
|
||||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M8.97179 12.2442L8.02898 13.1871C6.72723 14.4888 4.61668 14.4888 3.31493 13.1871C2.01319 11.8853 2.01319 9.77476 3.31493 8.47301L4.25774 7.5302M12.743 8.47301L13.6858 7.5302C14.9876 6.22845 14.9876 4.1179 13.6858 2.81615C12.3841 1.51441 10.2735 1.51441 8.97179 2.81615L8.02898 3.75896M6.16705 10.3349L10.8337 5.66826" stroke="var(--Other-White, white)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="px-1 flex justify-center items-center">
|
|
||||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Copy</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChatSidebar;
|
|
||||||
@@ -912,7 +912,7 @@ exports.chat = onRequest({ cors: true }, async (req, res) => {
|
|||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { message, employeeId, context } = req.body;
|
const { message, employeeId, context, mentions, attachments } = req.body;
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return res.status(400).json({ error: "Message is required" });
|
return res.status(400).json({ error: "Message is required" });
|
||||||
@@ -932,9 +932,44 @@ Current Context:
|
|||||||
${JSON.stringify(context, null, 2)}
|
${JSON.stringify(context, null, 2)}
|
||||||
` : ''}
|
` : ''}
|
||||||
|
|
||||||
|
${mentions && mentions.length > 0 ? `
|
||||||
|
Mentioned Employees:
|
||||||
|
${mentions.map(emp => `- ${emp.name} (${emp.role || 'Employee'})`).join('\n')}
|
||||||
|
` : ''}
|
||||||
|
|
||||||
Provide helpful, actionable insights while maintaining professional confidentiality and focusing on constructive feedback.
|
Provide helpful, actionable insights while maintaining professional confidentiality and focusing on constructive feedback.
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
|
// Build the user message content
|
||||||
|
let userContent = [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: message
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add image attachments if present
|
||||||
|
if (attachments && attachments.length > 0) {
|
||||||
|
attachments.forEach(attachment => {
|
||||||
|
if (attachment.type.startsWith('image/') && attachment.data) {
|
||||||
|
userContent.push({
|
||||||
|
type: "image_url",
|
||||||
|
image_url: {
|
||||||
|
url: attachment.data,
|
||||||
|
detail: "high"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// For non-image files, add them as text context
|
||||||
|
else if (attachment.data) {
|
||||||
|
userContent.push({
|
||||||
|
type: "text",
|
||||||
|
text: `[Attached file: ${attachment.name} (${attachment.type})]`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const completion = await openai.chat.completions.create({
|
const completion = await openai.chat.completions.create({
|
||||||
model: "gpt-4o",
|
model: "gpt-4o",
|
||||||
messages: [
|
messages: [
|
||||||
@@ -944,21 +979,25 @@ Provide helpful, actionable insights while maintaining professional confidential
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: message
|
content: userContent
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
max_tokens: 500,
|
max_tokens: 1000, // Increased for more detailed responses when analyzing images
|
||||||
});
|
});
|
||||||
|
|
||||||
response = completion.choices[0].message.content;
|
response = completion.choices[0].message.content;
|
||||||
} else {
|
} else {
|
||||||
// Fallback responses when OpenAI is not available
|
// Fallback responses when OpenAI is not available
|
||||||
|
const attachmentText = attachments && attachments.length > 0
|
||||||
|
? ` I can see you've attached ${attachments.length} file(s), but I'm currently unable to process attachments.`
|
||||||
|
: '';
|
||||||
|
|
||||||
const responses = [
|
const responses = [
|
||||||
"That's an interesting point about performance metrics. Based on the data, I'd recommend focusing on...",
|
`That's an interesting point about performance metrics.${attachmentText} Based on the data, I'd recommend focusing on...`,
|
||||||
"I can see from the employee report that there are opportunities for growth in...",
|
`I can see from the employee report that there are opportunities for growth in...${attachmentText}`,
|
||||||
"The company analysis suggests that this area needs attention. Here's what I would suggest...",
|
`The company analysis suggests that this area needs attention.${attachmentText} Here's what I would suggest...`,
|
||||||
"Based on the performance data, this employee shows strong potential in...",
|
`Based on the performance data, this employee shows strong potential in...${attachmentText}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
response = responses[Math.floor(Math.random() * responses.length)];
|
response = responses[Math.floor(Math.random() * responses.length)];
|
||||||
|
|||||||
14
index.css
14
index.css
@@ -1,5 +1,19 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* Blinking cursor animation for chat input */
|
||||||
|
@keyframes blink {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
51%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App';
|
import App from './src/App';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const rootElement = document.getElementById('root');
|
const rootElement = document.getElementById('root');
|
||||||
|
|||||||
@@ -1,390 +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 Sidebar from '../components/figma/Sidebar';
|
|
||||||
|
|
||||||
interface Message {
|
|
||||||
id: string;
|
|
||||||
role: 'user' | 'assistant';
|
|
||||||
content: string;
|
|
||||||
timestamp: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChatState {
|
|
||||||
messages: Message[];
|
|
||||||
isLoading: boolean;
|
|
||||||
showEmployeeMenu: boolean;
|
|
||||||
mentionQuery: string;
|
|
||||||
selectedEmployees: string[];
|
|
||||||
hasUploadedFiles: boolean;
|
|
||||||
uploadedFiles: Array<{
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
size: number;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChatNew: React.FC = () => {
|
|
||||||
const { user } = useAuth();
|
|
||||||
const { employees, orgId } = useOrg();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const [state, setState] = useState<ChatState>({
|
|
||||||
messages: [],
|
|
||||||
isLoading: false,
|
|
||||||
showEmployeeMenu: false,
|
|
||||||
mentionQuery: '',
|
|
||||||
selectedEmployees: [],
|
|
||||||
hasUploadedFiles: false,
|
|
||||||
uploadedFiles: []
|
|
||||||
});
|
|
||||||
|
|
||||||
const [currentInput, setCurrentInput] = useState('');
|
|
||||||
const [selectedCategory, setSelectedCategory] = useState('Accountability');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!user) {
|
|
||||||
navigate('/login');
|
|
||||||
}
|
|
||||||
}, [user, navigate]);
|
|
||||||
|
|
||||||
const questionStarters = [
|
|
||||||
"How can the company serve them better?",
|
|
||||||
"How can the company serve them better?",
|
|
||||||
"How can the company serve them better?",
|
|
||||||
"How can the company serve them better?"
|
|
||||||
];
|
|
||||||
|
|
||||||
const categories = ['Accountability', 'Employee Growth', 'Customer Focus', 'Teamwork'];
|
|
||||||
|
|
||||||
const handleSendMessage = async () => {
|
|
||||||
if (!currentInput.trim() && state.uploadedFiles.length === 0) return;
|
|
||||||
|
|
||||||
const newMessage: Message = {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
role: 'user',
|
|
||||||
content: currentInput.trim(),
|
|
||||||
timestamp: new Date()
|
|
||||||
};
|
|
||||||
|
|
||||||
setState(prev => ({
|
|
||||||
...prev,
|
|
||||||
messages: [...prev.messages, newMessage],
|
|
||||||
isLoading: true
|
|
||||||
}));
|
|
||||||
|
|
||||||
setCurrentInput('');
|
|
||||||
|
|
||||||
// Simulate AI response
|
|
||||||
setTimeout(() => {
|
|
||||||
const aiResponse: Message = {
|
|
||||||
id: (Date.now() + 1).toString(),
|
|
||||||
role: 'assistant',
|
|
||||||
content: "Based on the information provided and the company data, here are my insights and recommendations...",
|
|
||||||
timestamp: new Date()
|
|
||||||
};
|
|
||||||
|
|
||||||
setState(prev => ({
|
|
||||||
...prev,
|
|
||||||
messages: [...prev.messages, aiResponse],
|
|
||||||
isLoading: false
|
|
||||||
}));
|
|
||||||
}, 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
setCurrentInput(value);
|
|
||||||
|
|
||||||
// Check for @ mentions
|
|
||||||
const atIndex = value.lastIndexOf('@');
|
|
||||||
if (atIndex !== -1 && atIndex === value.length - 1) {
|
|
||||||
setState(prev => ({
|
|
||||||
...prev,
|
|
||||||
showEmployeeMenu: true,
|
|
||||||
mentionQuery: ''
|
|
||||||
}));
|
|
||||||
} else if (atIndex !== -1 && value.length > atIndex + 1) {
|
|
||||||
const query = value.substring(atIndex + 1);
|
|
||||||
setState(prev => ({
|
|
||||||
...prev,
|
|
||||||
showEmployeeMenu: true,
|
|
||||||
mentionQuery: query
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
setState(prev => ({
|
|
||||||
...prev,
|
|
||||||
showEmployeeMenu: false,
|
|
||||||
mentionQuery: ''
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEmployeeSelect = (employeeName: string) => {
|
|
||||||
const atIndex = currentInput.lastIndexOf('@');
|
|
||||||
if (atIndex !== -1) {
|
|
||||||
const newInput = currentInput.substring(0, atIndex) + '@' + employeeName + ' ';
|
|
||||||
setCurrentInput(newInput);
|
|
||||||
}
|
|
||||||
setState(prev => ({
|
|
||||||
...prev,
|
|
||||||
showEmployeeMenu: false,
|
|
||||||
mentionQuery: ''
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const files = Array.from(e.target.files || []);
|
|
||||||
if (files.length > 0) {
|
|
||||||
const uploadedFiles = files.map(file => ({
|
|
||||||
name: file.name,
|
|
||||||
type: file.type,
|
|
||||||
size: file.size
|
|
||||||
}));
|
|
||||||
|
|
||||||
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) return null;
|
|
||||||
|
|
||||||
const filteredEmployees = employees.filter(emp =>
|
|
||||||
emp.name.toLowerCase().includes(state.mentionQuery.toLowerCase())
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="absolute bottom-full left-[285px] mb-2 w-48 p-2 bg-Text-White-00 rounded-2xl shadow-[0px_1px_4px_4px_rgba(14,18,27,0.08)] flex flex-col justify-start items-start gap-1 z-10">
|
|
||||||
{filteredEmployees.slice(0, 3).map((employee, index) => (
|
|
||||||
<div
|
|
||||||
key={employee.id}
|
|
||||||
onClick={() => handleEmployeeSelect(employee.name)}
|
|
||||||
className={`self-stretch px-3 py-2 rounded-xl flex flex-col justify-start items-start gap-2.5 overflow-hidden cursor-pointer hover:bg-Text-Gray-100 ${index === 2 ? 'bg-Text-Gray-100' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className={`self-stretch justify-start text-Text-Dark-950 text-sm leading-tight ${index === 2 ? 'font-medium' : 'font-normal'
|
|
||||||
}`}>
|
|
||||||
{employee.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderUploadedFiles = () => {
|
|
||||||
if (state.uploadedFiles.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="inline-flex justify-start items-center gap-3 mb-4">
|
|
||||||
{state.uploadedFiles.map((file, index) => (
|
|
||||||
<div key={index} className="w-40 max-w-40 p-2 bg-Text-White-00 rounded-full outline outline-1 outline-offset-[-1px] outline-Outline-Outline-Gray-200 inline-flex flex-col justify-start items-start gap-2.5 overflow-hidden">
|
|
||||||
<div className="self-stretch pr-2 inline-flex justify-between items-center">
|
|
||||||
<div className="flex-1 flex justify-start items-center gap-1.5">
|
|
||||||
<div className="w-6 h-6 relative bg-Icon-Gray-600 rounded-full overflow-hidden">
|
|
||||||
<div className="left-[6px] top-[6px] absolute">
|
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M7 1.13477V3.20004C7 3.48006 7 3.62007 7.0545 3.72703C7.10243 3.82111 7.17892 3.8976 7.273 3.94554C7.37996 4.00004 7.51997 4.00004 7.8 4.00004H9.86527M8 6.5H4M8 8.5H4M5 4.5H4M7 1H4.4C3.55992 1 3.13988 1 2.81901 1.16349C2.53677 1.3073 2.3073 1.53677 2.16349 1.81901C2 2.13988 2 2.55992 2 3.4V8.6C2 9.44008 2 9.86012 2.16349 10.181C2.3073 10.4632 2.53677 10.6927 2.81901 10.8365C3.13988 11 3.55992 11 4.4 11H7.6C8.44008 11 8.86012 11 9.18099 10.8365C9.46323 10.6927 9.6927 10.4632 9.83651 10.181C10 9.86012 10 9.44008 10 8.6V4L7 1Z" stroke="var(--Text-White-00, #FDFDFD)" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 justify-start text-Text-Dark-950 text-sm font-medium font-['Inter'] leading-tight">{file.name}</div>
|
|
||||||
</div>
|
|
||||||
<div onClick={() => removeFile(index)} className="cursor-pointer">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M12 4L4 12M4 4L12 12" stroke="var(--Icon-Gray-400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderChatInterface = () => {
|
|
||||||
if (state.messages.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="w-[736px] flex-1 max-w-[736px] pt-48 flex flex-col justify-between items-center">
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-center gap-6">
|
|
||||||
<div className="justify-start text-Text-Gray-800 text-2xl font-medium font-['Neue_Montreal'] leading-normal">What would you like to understand?</div>
|
|
||||||
<div className="p-1 bg-Neutrals-NeutralSlate100 rounded-xl inline-flex justify-start items-center gap-1">
|
|
||||||
{categories.map((category) => (
|
|
||||||
<div
|
|
||||||
key={category}
|
|
||||||
onClick={() => 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' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="px-0.5 flex justify-center items-center">
|
|
||||||
<div className={`justify-start text-xs font-medium font-['Inter'] leading-none ${selectedCategory === category ? 'text-Neutrals-NeutralSlate900' : 'text-Neutrals-NeutralSlate600'
|
|
||||||
}`}>{category}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
|
||||||
<div className="self-stretch inline-flex justify-start items-center gap-3">
|
|
||||||
{questionStarters.map((question, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
onClick={() => handleQuestionClick(question)}
|
|
||||||
className="flex-1 h-48 px-3 py-4 bg-Main-BG-Gray-50 rounded-2xl inline-flex flex-col justify-between items-start overflow-hidden cursor-pointer hover:bg-Main-BG-Gray-100"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clipPath="url(#clip0_818_19557)">
|
|
||||||
<path d="M7.57496 7.5013C7.77088 6.94436 8.15759 6.47472 8.66659 6.17558C9.17559 5.87643 9.77404 5.76708 10.3559 5.8669C10.9378 5.96671 11.4656 6.26924 11.8459 6.72091C12.2261 7.17258 12.4342 7.74424 12.4333 8.33464C12.4333 10.0013 9.93329 10.8346 9.93329 10.8346M9.99996 14.168H10.0083M18.3333 10.0013C18.3333 14.6037 14.6023 18.3346 9.99996 18.3346C5.39759 18.3346 1.66663 14.6037 1.66663 10.0013C1.66663 5.39893 5.39759 1.66797 9.99996 1.66797C14.6023 1.66797 18.3333 5.39893 18.3333 10.0013Z" stroke="var(--Text-Gray-500, #717680)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_818_19557">
|
|
||||||
<rect width="20" height="20" fill="white" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch justify-start text-Text-Gray-800 text-base font-normal font-['Inter'] leading-normal">{question}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{renderChatInput()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-[736px] flex-1 max-w-[736px] flex flex-col">
|
|
||||||
<div className="flex-1 overflow-y-auto py-6">
|
|
||||||
{state.messages.map((message) => (
|
|
||||||
<div key={message.id} className={`mb-4 flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
|
||||||
<div className={`max-w-[80%] p-4 rounded-2xl ${message.role === 'user'
|
|
||||||
? 'bg-Brand-Orange text-white'
|
|
||||||
: 'bg-Main-BG-Gray-50 text-Text-Gray-800'
|
|
||||||
}`}>
|
|
||||||
{message.content}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{state.isLoading && (
|
|
||||||
<div className="flex justify-start mb-4">
|
|
||||||
<div className="bg-Main-BG-Gray-50 text-Text-Gray-800 p-4 rounded-2xl">
|
|
||||||
<div className="flex space-x-1">
|
|
||||||
<div className="w-2 h-2 bg-Text-Gray-500 rounded-full animate-bounce"></div>
|
|
||||||
<div className="w-2 h-2 bg-Text-Gray-500 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
|
|
||||||
<div className="w-2 h-2 bg-Text-Gray-500 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{renderChatInput()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderChatInput = () => {
|
|
||||||
return (
|
|
||||||
<div className="self-stretch pl-5 pr-3 pt-5 pb-3 relative bg-Main-BG-Gray-50 rounded-3xl flex flex-col justify-start items-start gap-4">
|
|
||||||
{renderUploadedFiles()}
|
|
||||||
<div className="self-stretch justify-start text-Text-Gray-500 text-base font-normal font-['Inter'] leading-normal">
|
|
||||||
{currentInput || "Ask anything, use @ to tag staff and ask questions."}
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch inline-flex justify-between items-center">
|
|
||||||
<div className="flex justify-start items-center gap-4">
|
|
||||||
<div onClick={() => fileInputRef.current?.click()} className="cursor-pointer">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M17.6271 9.08442L10.1141 16.5974C8.40556 18.306 5.63546 18.306 3.92692 16.5974C2.21837 14.8889 2.21837 12.1188 3.92692 10.4102L11.4399 2.89724C12.579 1.75821 14.4257 1.75821 15.5647 2.89724C16.7037 4.03627 16.7037 5.883 15.5647 7.02203L8.34633 14.2404C7.77682 14.8099 6.85345 14.8099 6.28394 14.2404C5.71442 13.6709 5.71442 12.7475 6.28394 12.178L12.6184 5.84352" stroke="var(--Text-Gray-500, #717680)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div onClick={() => fileInputRef.current?.click()} className="cursor-pointer">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M13.5 17.5H5.77614C5.2713 17.5 5.01887 17.5 4.90199 17.4002C4.80056 17.3135 4.74674 17.1836 4.75721 17.0506C4.76927 16.8974 4.94776 16.7189 5.30474 16.3619L12.3905 9.27614C12.7205 8.94613 12.8855 8.78112 13.0758 8.7193C13.2432 8.66492 13.4235 8.66492 13.5908 8.7193C13.7811 8.78112 13.9461 8.94613 14.2761 9.27614L17.5 12.5V13.5M13.5 17.5C14.9001 17.5 15.6002 17.5 16.135 17.2275C16.6054 16.9878 16.9878 16.6054 17.2275 16.135C17.5 15.6002 17.5 14.9001 17.5 13.5M13.5 17.5H6.5C5.09987 17.5 4.3998 17.5 3.86502 17.2275C3.39462 16.9878 3.01217 16.6054 2.77248 16.135C2.5 15.6002 2.5 14.9001 2.5 13.5V6.5C2.5 5.09987 2.5 4.3998 2.77248 3.86502C3.01217 3.39462 3.39462 3.01217 3.86502 2.77248C4.3998 2.5 5.09987 2.5 6.5 2.5H13.5C14.9001 2.5 15.6002 2.5 16.135 2.77248C16.6054 3.01217 16.9878 3.39462 17.2275 3.86502C17.5 4.3998 17.5 5.09987 17.5 6.5V13.5M8.75 7.08333C8.75 8.00381 8.00381 8.75 7.08333 8.75C6.16286 8.75 5.41667 8.00381 5.41667 7.08333C5.41667 6.16286 6.16286 5.41667 7.08333 5.41667C8.00381 5.41667 8.75 6.16286 8.75 7.08333Z" stroke="var(--Text-Gray-500, #717680)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="cursor-pointer">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clipPath="url(#clip0_818_19694)">
|
|
||||||
<path d="M13.3334 6.66745V10.8341C13.3334 11.4972 13.5968 12.133 14.0657 12.6019C14.5345 13.0707 15.1704 13.3341 15.8334 13.3341C16.4965 13.3341 17.1324 13.0707 17.6012 12.6019C18.07 12.133 18.3334 11.4972 18.3334 10.8341V10.0008C18.3333 8.11998 17.6969 6.29452 16.5278 4.82123C15.3587 3.34794 13.7256 2.31347 11.894 1.88603C10.0624 1.45859 8.14003 1.66332 6.43955 2.46692C4.73906 3.27053 3.36042 4.62575 2.5278 6.31222C1.69519 7.99869 1.45756 9.91723 1.85356 11.7559C2.24956 13.5945 3.2559 15.2451 4.70895 16.4393C6.16199 17.6335 7.97628 18.3011 9.85681 18.3334C11.7373 18.3657 13.5735 17.761 15.0668 16.6175M13.3334 10.0008C13.3334 11.8417 11.841 13.3341 10.0001 13.3341C8.15914 13.3341 6.66676 11.8417 6.66676 10.0008C6.66676 8.15984 8.15914 6.66745 10.0001 6.66745C11.841 6.66745 13.3334 8.15984 13.3334 10.0008Z" stroke="var(--Text-Gray-500, #717680)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_818_19694">
|
|
||||||
<rect width="20" height="20" fill="white" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={handleSendMessage}
|
|
||||||
className={`p-2.5 rounded-[999px] flex justify-start items-center gap-2.5 overflow-hidden cursor-pointer ${currentInput.trim() || state.uploadedFiles.length > 0
|
|
||||||
? 'bg-Main-BG-Gray-800'
|
|
||||||
: 'bg-Text-Gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M8 13.3346V2.66797M8 2.66797L4 6.66797M8 2.66797L12 6.66797" stroke="var(--Text-White-00, #FDFDFD)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{renderEmployeeMenu()}
|
|
||||||
<textarea
|
|
||||||
ref={inputRef}
|
|
||||||
value={currentInput}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSendMessage();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="absolute inset-0 w-full h-full opacity-0 resize-none outline-none"
|
|
||||||
placeholder=""
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
multiple
|
|
||||||
onChange={handleFileUpload}
|
|
||||||
className="hidden"
|
|
||||||
accept=".pdf,.doc,.docx,.txt,.png,.jpg,.jpeg"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-[1440px] h-[810px] p-4 bg-Neutrals-NeutralSlate200 inline-flex justify-start items-start overflow-hidden">
|
|
||||||
<div className="flex-1 self-stretch rounded-3xl shadow-[0px_0px_15px_0px_rgba(0,0,0,0.08)] flex justify-between items-start overflow-hidden">
|
|
||||||
<Sidebar companyName="Zitlac Media" />
|
|
||||||
<div className="flex-1 self-stretch py-6 bg-Neutrals-NeutralSlate0 inline-flex flex-col justify-center items-center gap-2.5">
|
|
||||||
{renderChatInterface()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChatNew;
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
|
||||||
import { useOrg } from '../contexts/OrgContext';
|
|
||||||
import { Card } from '../components/UiKit';
|
|
||||||
|
|
||||||
const DebugEmployee: React.FC = () => {
|
|
||||||
const { user } = useAuth();
|
|
||||||
const { employees, org } = useOrg();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4">
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
<h1 className="text-3xl font-bold text-[--text-primary] mb-8 text-center">
|
|
||||||
Employee Debug Information
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
{/* Current User Info */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">
|
|
||||||
Current User
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">Email:</span>
|
|
||||||
<span className="ml-2 text-[--text-secondary]">{user?.email || 'Not logged in'}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">Display Name:</span>
|
|
||||||
<span className="ml-2 text-[--text-secondary]">{user?.displayName || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">UID:</span>
|
|
||||||
<span className="ml-2 text-[--text-secondary] text-xs break-all">{user?.uid || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Organization Info */}
|
|
||||||
<Card className="p-6">
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">
|
|
||||||
Organization
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">Name:</span>
|
|
||||||
<span className="ml-2 text-[--text-secondary]">{org?.name || 'Not set'}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">Org ID:</span>
|
|
||||||
<span className="ml-2 text-[--text-secondary] text-xs break-all">{org?.orgId || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">Onboarding Complete:</span>
|
|
||||||
<span className="ml-2 text-[--text-secondary]">{org?.onboardingCompleted ? 'Yes' : 'No'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Employee Matching Analysis */}
|
|
||||||
<Card className="p-6 lg:col-span-2">
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">
|
|
||||||
Employee Matching Analysis
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">Total Employees:</span>
|
|
||||||
<span className="ml-2 text-[--text-secondary]">{employees.length}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{user?.email && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h3 className="font-medium text-[--text-primary]">Matching Results:</h3>
|
|
||||||
|
|
||||||
{/* Exact match */}
|
|
||||||
<div className="pl-4">
|
|
||||||
<span className="text-sm font-medium">Exact Email Match:</span>
|
|
||||||
<span className="ml-2 text-[--text-secondary]">
|
|
||||||
{employees.find(emp => emp.email === user.email) ? '✅ Found' : '❌ Not found'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Case insensitive match */}
|
|
||||||
<div className="pl-4">
|
|
||||||
<span className="text-sm font-medium">Case-Insensitive Match:</span>
|
|
||||||
<span className="ml-2 text-[--text-secondary]">
|
|
||||||
{employees.find(emp => emp.email?.toLowerCase() === user.email?.toLowerCase()) ? '✅ Found' : '❌ Not found'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Domain match */}
|
|
||||||
<div className="pl-4">
|
|
||||||
<span className="text-sm font-medium">Same Domain Match:</span>
|
|
||||||
<span className="ml-2 text-[--text-secondary]">
|
|
||||||
{(() => {
|
|
||||||
const userDomain = user.email?.split('@')[1];
|
|
||||||
const domainMatch = employees.find(emp => emp.email?.split('@')[1] === userDomain);
|
|
||||||
return domainMatch ? `✅ Found: ${domainMatch.name} (${domainMatch.email})` : '❌ Not found';
|
|
||||||
})()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Username partial match */}
|
|
||||||
<div className="pl-4">
|
|
||||||
<span className="text-sm font-medium">Username Partial Match:</span>
|
|
||||||
<span className="ml-2 text-[--text-secondary]">
|
|
||||||
{(() => {
|
|
||||||
const username = user.email?.split('@')[0];
|
|
||||||
const partialMatch = employees.find(emp =>
|
|
||||||
emp.email?.toLowerCase().includes(username?.toLowerCase() || '')
|
|
||||||
);
|
|
||||||
return partialMatch ? `✅ Found: ${partialMatch.name} (${partialMatch.email})` : '❌ Not found';
|
|
||||||
})()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* All Employees List */}
|
|
||||||
<Card className="p-6 lg:col-span-2">
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">
|
|
||||||
All Employees ({employees.length})
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
{employees.length === 0 ? (
|
|
||||||
<p className="text-[--text-secondary] italic">No employees found</p>
|
|
||||||
) : (
|
|
||||||
employees.map((employee, index) => (
|
|
||||||
<div key={employee.id} className="border border-[--border-color] rounded-lg p-3">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">Name:</span>
|
|
||||||
<span className="ml-2">{employee.name}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">Email:</span>
|
|
||||||
<span className="ml-2 text-sm">{employee.email || 'Not set'}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">Role:</span>
|
|
||||||
<span className="ml-2 text-sm">{employee.role || 'Not set'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2">
|
|
||||||
<span className="font-medium">ID:</span>
|
|
||||||
<span className="ml-2 text-xs text-[--text-secondary] break-all">{employee.id}</span>
|
|
||||||
</div>
|
|
||||||
{user?.email && employee.email && (
|
|
||||||
<div className="mt-2 text-sm">
|
|
||||||
<span className="font-medium">Match Analysis:</span>
|
|
||||||
<span className="ml-2">
|
|
||||||
{employee.email === user.email && (
|
|
||||||
<span className="bg-green-100 text-green-800 px-2 py-1 rounded text-xs mr-1">Exact</span>
|
|
||||||
)}
|
|
||||||
{employee.email?.toLowerCase() === user.email?.toLowerCase() && employee.email !== user.email && (
|
|
||||||
<span className="bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-xs mr-1">Case Diff</span>
|
|
||||||
)}
|
|
||||||
{employee.email?.split('@')[1] === user.email?.split('@')[1] && (
|
|
||||||
<span className="bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs mr-1">Same Domain</span>
|
|
||||||
)}
|
|
||||||
{employee.email?.toLowerCase().includes(user.email?.split('@')[0]?.toLowerCase() || '') && (
|
|
||||||
<span className="bg-purple-100 text-purple-800 px-2 py-1 rounded text-xs mr-1">Username Match</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Quick Actions */}
|
|
||||||
<Card className="p-6 lg:col-span-2">
|
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4">
|
|
||||||
Quick Actions
|
|
||||||
</h2>
|
|
||||||
<div className="flex flex-wrap gap-3">
|
|
||||||
<a
|
|
||||||
href="#/employee-questionnaire"
|
|
||||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors text-sm"
|
|
||||||
>
|
|
||||||
Try Traditional Questionnaire
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="#/employee-questionnaire-steps"
|
|
||||||
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors text-sm"
|
|
||||||
>
|
|
||||||
Try Stepped Questionnaire
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="#/reports"
|
|
||||||
className="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600 transition-colors text-sm"
|
|
||||||
>
|
|
||||||
Go to Reports
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
onClick={() => window.location.reload()}
|
|
||||||
className="px-4 py-2 bg-orange-500 text-white rounded hover:bg-orange-600 transition-colors text-sm"
|
|
||||||
>
|
|
||||||
Refresh Page
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DebugEmployee;
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
const EmployeeFormNew: React.FC = () => {
|
|
||||||
const [currentStep, setCurrentStep] = useState(1);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-[1440px] bg-white inline-flex justify-start items-center overflow-hidden">
|
|
||||||
<div className="flex-1 h-[810px] px-32 py-48 bg-Neutrals-NeutralSlate0 flex justify-center items-center gap-2.5 overflow-hidden">
|
|
||||||
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
|
||||||
<div className="w-12 h-12 relative bg-Brand-Orange rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
|
||||||
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
|
||||||
<div className="left-[12px] top-[9.33px] absolute">
|
|
||||||
<svg width="24" height="30" viewBox="0 0 24 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path opacity="0.5" fillRule="evenodd" clipRule="evenodd" d="M2.57408 17.8138C3.11835 18.3649 3.11834 19.2585 2.57406 19.8097L2.54619 19.8379C2.00191 20.389 1.11946 20.389 0.57519 19.8379C0.030919 19.2867 0.0309274 18.3931 0.575208 17.842L0.603083 17.8137C1.14736 17.2626 2.02981 17.2626 2.57408 17.8138Z" fill="url(#paint0_linear_981_10577)" />
|
|
||||||
<path opacity="0.7" fillRule="evenodd" clipRule="evenodd" d="M9.12583 18.2374C9.66912 18.7896 9.66752 19.6832 9.12226 20.2333L5.2617 24.1286C4.71644 24.6787 3.83399 24.6771 3.2907 24.125C2.74741 23.5728 2.74901 22.6792 3.29427 22.1291L7.15483 18.2338C7.70009 17.6837 8.58254 17.6853 9.12583 18.2374Z" fill="url(#paint1_linear_981_10577)" />
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="paint0_linear_981_10577" x1="1.57463" y1="17.4004" x2="1.57463" y2="20.2513" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint1_linear_981_10577" x1="6.20827" y1="17.8223" x2="6.20827" y2="24.5401" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
|
||||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate950 text-2xl font-semibold font-['Inter'] leading-8">Welcome to the Auditly Employee Assessment</div>
|
|
||||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate500 text-base font-normal font-['Inter'] leading-normal">Let's learn about your role, contribution and help us get a better understand of how you work best.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-8">
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
|
||||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate950 text-sm font-normal font-['Inter'] leading-tight">Your Role & Output</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate500 text-sm font-normal font-['Inter'] leading-tight">Tell us about your current role and what you work on</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
|
||||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate900 text-sm font-normal font-['Inter'] leading-tight">Your Name</div>
|
|
||||||
<div className="justify-start text-Brand-Orange text-sm font-medium font-['Inter'] leading-tight">*</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
|
||||||
<div className="self-stretch px-4 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="flex-1 bg-transparent text-Neutrals-NeutralSlate950 text-sm font-normal font-['Inter'] leading-tight placeholder:text-Neutrals-NeutralSlate500 outline-none"
|
|
||||||
placeholder="Enter your full name"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
|
||||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate900 text-sm font-normal font-['Inter'] leading-tight">What is your role at the company?</div>
|
|
||||||
<div className="justify-start text-Brand-Orange text-sm font-medium font-['Inter'] leading-tight">*</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
|
||||||
<div className="self-stretch px-4 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="flex-1 bg-transparent text-Neutrals-NeutralSlate950 text-sm font-normal font-['Inter'] leading-tight placeholder:text-Neutrals-NeutralSlate500 outline-none"
|
|
||||||
placeholder="e.g. Software Engineer, Marketing Manager"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
|
||||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate900 text-sm font-normal font-['Inter'] leading-tight">What department do you work in?</div>
|
|
||||||
<div className="justify-start text-Brand-Orange text-sm font-medium font-['Inter'] leading-tight">*</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
|
||||||
<div className="self-stretch px-4 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="flex-1 bg-transparent text-Neutrals-NeutralSlate950 text-sm font-normal font-['Inter'] leading-tight placeholder:text-Neutrals-NeutralSlate500 outline-none"
|
|
||||||
placeholder="e.g. Engineering, Sales, Marketing"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch inline-flex justify-start items-center gap-3">
|
|
||||||
<button className="flex-1 px-6 py-3.5 bg-Brand-Orange rounded-[999px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
|
||||||
<div className="justify-center text-Neutrals-NeutralSlate0 text-base font-medium font-['Inter'] leading-normal">Continue</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EmployeeFormNew;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,668 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useNavigate, useLocation, useParams } from 'react-router-dom';
|
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
|
||||||
import { useOrg } from '../contexts/OrgContext';
|
|
||||||
import { EMPLOYEE_QUESTIONS, EmployeeSubmissionAnswers, EmployeeQuestion } from '../employeeQuestions';
|
|
||||||
import { API_URL } from '../constants';
|
|
||||||
|
|
||||||
// Icon SVG Component
|
|
||||||
const AuditlyIcon: React.FC = () => (
|
|
||||||
<svg width="24" height="30" viewBox="0 0 24 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path opacity="0.5" fillRule="evenodd" clipRule="evenodd" d="M2.57408 17.8138C3.11835 18.3649 3.11834 19.2585 2.57406 19.8097L2.54619 19.8379C2.00191 20.389 1.11946 20.389 0.57519 19.8379C0.030919 19.2867 0.0309274 18.3931 0.575208 17.842L0.603083 17.8137C1.14736 17.2626 2.02981 17.2626 2.57408 17.8138Z" fill="url(#paint0_linear_981_10577)" />
|
|
||||||
<path opacity="0.7" fillRule="evenodd" clipRule="evenodd" d="M9.12583 18.2374C9.66912 18.7896 9.66752 19.6832 9.12226 20.2333L5.2617 24.1286C4.71644 24.6787 3.83399 24.6771 3.2907 24.125C2.74741 23.5728 2.74901 22.6792 3.29427 22.1291L7.15483 18.2338C7.70009 17.6837 8.58254 17.6853 9.12583 18.2374Z" fill="url(#paint1_linear_981_10577)" />
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="paint0_linear_981_10577" x1="1.57463" y1="17.4004" x2="1.57463" y2="20.2513" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint1_linear_981_10577" x1="6.20827" y1="17.8223" x2="6.20827" y2="24.5401" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Progress Bar Component for Section Headers
|
|
||||||
const SectionProgressBar: React.FC<{ currentStep: number; totalSteps: number }> = ({ currentStep, totalSteps }) => {
|
|
||||||
return (
|
|
||||||
<div className="p-4 bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
|
||||||
{Array.from({ length: Math.min(7, totalSteps) }, (_, index) => {
|
|
||||||
const isActive = index < Math.ceil((currentStep / totalSteps) * 7);
|
|
||||||
return (
|
|
||||||
<div key={index}>
|
|
||||||
{isActive ? (
|
|
||||||
<div className="w-6 h-1 bg-Brand-Orange rounded-3xl" />
|
|
||||||
) : (
|
|
||||||
<svg width="4" height="4" viewBox="0 0 4 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="4" height="4" rx="2" fill="var(--Neutrals-NeutralSlate300, #D5D7DA)" />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Question Input Component
|
|
||||||
const QuestionInput: React.FC<{
|
|
||||||
question: EmployeeQuestion;
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
}> = ({ question, value, onChange }) => {
|
|
||||||
switch (question.type) {
|
|
||||||
case 'scale':
|
|
||||||
return (
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
|
||||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
|
||||||
{question.prompt}
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex justify-between items-center gap-2">
|
|
||||||
<span className="text-sm text-Neutrals-NeutralSlate500">{question.scaleLabels?.min}</span>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{Array.from({ length: question.scaleMax! - question.scaleMin! + 1 }, (_, index) => {
|
|
||||||
const ratingValue = question.scaleMin! + index;
|
|
||||||
const isSelected = parseInt(value) === ratingValue;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={ratingValue}
|
|
||||||
onClick={() => onChange(ratingValue.toString())}
|
|
||||||
className={`w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium transition-colors ${isSelected
|
|
||||||
? 'bg-Brand-Orange text-white'
|
|
||||||
: 'bg-Neutrals-NeutralSlate100 text-Neutrals-NeutralSlate700 hover:bg-Neutrals-NeutralSlate200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{ratingValue}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-Neutrals-NeutralSlate500">{question.scaleLabels?.max}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'yesno':
|
|
||||||
return (
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
|
||||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
|
||||||
{question.prompt}
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch inline-flex justify-center items-center gap-3">
|
|
||||||
<div
|
|
||||||
onClick={() => 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'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className={`absolute inset-0 flex items-center justify-center text-center text-base font-normal font-['Inter'] leading-normal ${value === 'No' ? 'text-Neutrals-NeutralSlate0' : 'text-Neutrals-NeutralSlate950'
|
|
||||||
}`}>
|
|
||||||
No
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={() => 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'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className={`absolute inset-0 flex items-center justify-center text-center text-base font-normal font-['Inter'] leading-normal ${value === 'Yes' ? 'text-Neutrals-NeutralSlate0' : 'text-Neutrals-NeutralSlate950'
|
|
||||||
}`}>
|
|
||||||
Yes
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'textarea':
|
|
||||||
return (
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
|
||||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
|
||||||
{question.prompt}
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch min-h-40 p-5 relative bg-Neutrals-NeutralSlate100 rounded-xl inline-flex justify-start items-start gap-2.5">
|
|
||||||
<textarea
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
className="flex-1 bg-transparent text-Neutrals-NeutralSlate950 text-base font-normal font-['Inter'] leading-normal placeholder:text-Neutrals-NeutralSlate500 outline-none resize-none"
|
|
||||||
placeholder={question.placeholder || "Type your answer...."}
|
|
||||||
rows={6}
|
|
||||||
/>
|
|
||||||
<div className="w-3 h-3 absolute right-5 bottom-5">
|
|
||||||
<div className="w-2 h-2 absolute top-0.5 left-0.5 outline outline-1 outline-offset-[-0.50px] outline-Neutrals-NeutralSlate500" />
|
|
||||||
<div className="w-1 h-1 absolute bottom-0 right-0 outline outline-1 outline-offset-[-0.50px] outline-Neutrals-NeutralSlate500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
default: // text input
|
|
||||||
return (
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
|
||||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
|
||||||
{question.prompt}
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch px-4 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
|
||||||
<input
|
|
||||||
type={question.id === 'email' ? 'email' : 'text'}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
className="flex-1 bg-transparent text-Neutrals-NeutralSlate950 text-sm font-normal font-['Inter'] leading-tight placeholder:text-Neutrals-NeutralSlate500 outline-none"
|
|
||||||
placeholder={question.placeholder || "Enter your answer..."}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Navigation Buttons Component
|
|
||||||
const NavigationButtons: React.FC<{
|
|
||||||
onBack?: () => void;
|
|
||||||
onNext: () => void;
|
|
||||||
onSkip?: () => void;
|
|
||||||
nextDisabled?: boolean;
|
|
||||||
isSubmitting?: boolean;
|
|
||||||
currentStep: number;
|
|
||||||
totalSteps: number;
|
|
||||||
isLastStep?: boolean;
|
|
||||||
}> = ({ onBack, onNext, onSkip, nextDisabled, isSubmitting, currentStep, totalSteps, isLastStep }) => {
|
|
||||||
return (
|
|
||||||
<div className="self-stretch inline-flex justify-start items-start gap-2">
|
|
||||||
{onBack && (
|
|
||||||
<button
|
|
||||||
onClick={onBack}
|
|
||||||
className="h-12 px-8 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-Neutrals-NeutralSlate200"
|
|
||||||
>
|
|
||||||
<div className="px-1 flex justify-center items-center">
|
|
||||||
<div className="justify-center text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">Back</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={onNext}
|
|
||||||
disabled={nextDisabled || isSubmitting}
|
|
||||||
className="flex-1 h-12 px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
<div className="px-1 flex justify-center items-center">
|
|
||||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">
|
|
||||||
{isSubmitting ? 'Submitting...' : (isLastStep ? 'Submit' : 'Next')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{onSkip && (
|
|
||||||
<div
|
|
||||||
onClick={onSkip}
|
|
||||||
className="px-3 py-1.5 absolute right-6 top-6 bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden cursor-pointer hover:bg-Neutrals-NeutralSlate200"
|
|
||||||
>
|
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] leading-none">Skip</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Welcome Screen Component
|
|
||||||
const WelcomeScreen: React.FC<{
|
|
||||||
onStart: () => void;
|
|
||||||
currentEmployee?: any;
|
|
||||||
isInviteFlow: boolean;
|
|
||||||
error?: string;
|
|
||||||
}> = ({ onStart, currentEmployee, isInviteFlow, error }) => {
|
|
||||||
return (
|
|
||||||
<div className="w-full h-screen bg-white inline-flex justify-start items-center overflow-hidden">
|
|
||||||
<div className="flex-1 h-full px-32 py-48 bg-Neutrals-NeutralSlate0 flex justify-center items-center gap-2.5 overflow-hidden">
|
|
||||||
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
|
||||||
<div className="w-12 h-12 relative bg-Brand-Orange rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
|
||||||
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
|
||||||
<div className="left-[12px] top-[9.33px] absolute">
|
|
||||||
<AuditlyIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
|
||||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate800 text-5xl font-medium font-['Neue_Montreal'] leading-[48px]">
|
|
||||||
Welcome to Auditly!
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch justify-center text-Neutrals-NeutralSlate500 text-base font-normal font-['Inter'] leading-normal">
|
|
||||||
Please complete this questionnaire to help us understand your role and create personalized insights.
|
|
||||||
</div>
|
|
||||||
{currentEmployee && (
|
|
||||||
<div className="inline-flex items-center px-4 py-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
|
|
||||||
<span className="text-sm text-blue-800 dark:text-blue-200">
|
|
||||||
👋 Hello {currentEmployee.name}! {currentEmployee.role && `(${currentEmployee.role})`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{error && (
|
|
||||||
<div className="inline-flex items-center px-4 py-2 bg-red-100 dark:bg-red-900 rounded-lg">
|
|
||||||
<span className="text-sm text-red-800 dark:text-red-200">
|
|
||||||
⚠️ {error}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={onStart}
|
|
||||||
className="self-stretch px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden"
|
|
||||||
>
|
|
||||||
<div className="px-1 flex justify-center items-center">
|
|
||||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Start Assessment</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 h-full px-20 py-16 flex justify-center items-center gap-2.5 overflow-hidden">
|
|
||||||
<div className="flex-1 self-stretch origin-top-left rotate-180 rounded-3xl inline-flex flex-col justify-center items-center gap-2.5 overflow-hidden">
|
|
||||||
<img className="self-stretch flex-1" src="https://placehold.co/560x682" alt="Welcome" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Question Step Component
|
|
||||||
const QuestionStep: React.FC<{
|
|
||||||
question: EmployeeQuestion;
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
onNext: () => void;
|
|
||||||
onBack?: () => void;
|
|
||||||
onSkip?: () => void;
|
|
||||||
currentStep: number;
|
|
||||||
totalSteps: number;
|
|
||||||
isSubmitting?: boolean;
|
|
||||||
isLastStep?: boolean;
|
|
||||||
}> = ({ question, value, onChange, onNext, onBack, onSkip, currentStep, totalSteps, isSubmitting, isLastStep }) => {
|
|
||||||
const isRequired = question.required;
|
|
||||||
const isAnswered = value && value.trim().length > 0;
|
|
||||||
const nextDisabled = isRequired ? !isAnswered : false;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full h-screen py-6 relative bg-Neutrals-NeutralSlate0 inline-flex flex-col justify-center items-center gap-9">
|
|
||||||
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
|
|
||||||
<QuestionInput question={question} value={value} onChange={onChange} />
|
|
||||||
<NavigationButtons
|
|
||||||
onBack={onBack}
|
|
||||||
onNext={onNext}
|
|
||||||
onSkip={onSkip}
|
|
||||||
nextDisabled={nextDisabled}
|
|
||||||
isSubmitting={isSubmitting}
|
|
||||||
currentStep={currentStep}
|
|
||||||
totalSteps={totalSteps}
|
|
||||||
isLastStep={isLastStep}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress indicators */}
|
|
||||||
<div className="px-3 py-1.5 left-[24px] top-[24px] absolute bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] uppercase leading-none">
|
|
||||||
{currentStep} of {totalSteps}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-[464px] max-w-[464px] min-w-[464px] left-[488px] top-[24px] absolute flex flex-col justify-start items-center gap-4">
|
|
||||||
<SectionProgressBar currentStep={currentStep} totalSteps={totalSteps} />
|
|
||||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate500 text-base font-medium font-['Neue_Montreal'] leading-normal">
|
|
||||||
Employee Assessment
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Thank You Page Component
|
|
||||||
const ThankYouPage: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<div className="w-full h-screen bg-white inline-flex justify-start items-center overflow-hidden">
|
|
||||||
<div className="flex-1 h-full px-32 py-48 bg-Neutrals-NeutralSlate0 flex justify-center items-center gap-2.5 overflow-hidden">
|
|
||||||
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
|
||||||
<div className="w-12 h-12 relative bg-Brand-Orange rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
|
||||||
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
|
||||||
<div className="left-[12px] top-[9.33px] absolute">
|
|
||||||
<AuditlyIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
|
||||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate800 text-5xl font-medium font-['Neue_Montreal'] leading-[48px]">
|
|
||||||
Thank you! Your assessment has been submitted!
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch justify-center text-Neutrals-NeutralSlate500 text-base font-normal font-['Inter'] leading-normal">
|
|
||||||
Your responses have been recorded and your AI-powered performance report will be generated shortly.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 h-full px-20 py-16 flex justify-center items-center gap-2.5 overflow-hidden">
|
|
||||||
<div className="flex-1 self-stretch origin-top-left rotate-180 rounded-3xl inline-flex flex-col justify-center items-center gap-2.5 overflow-hidden">
|
|
||||||
<img className="self-stretch flex-1" src="https://placehold.co/560x682" alt="Thank you" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Main Component
|
|
||||||
const EmployeeQuestionnaireMerged: React.FC = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
const params = useParams();
|
|
||||||
const { user } = useAuth();
|
|
||||||
|
|
||||||
// Check if this is an invite-based flow (no auth/org needed)
|
|
||||||
const inviteCode = params.inviteCode;
|
|
||||||
const isInviteFlow = !!inviteCode;
|
|
||||||
|
|
||||||
// Only use org context for authenticated flows
|
|
||||||
let submitEmployeeAnswers, generateEmployeeReport, employees;
|
|
||||||
if (!isInviteFlow) {
|
|
||||||
const orgContext = useOrg();
|
|
||||||
({ submitEmployeeAnswers, generateEmployeeReport, employees } = orgContext);
|
|
||||||
} else {
|
|
||||||
// For invite flows, we don't need these functions from org context
|
|
||||||
submitEmployeeAnswers = null;
|
|
||||||
generateEmployeeReport = null;
|
|
||||||
employees = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const [currentStep, setCurrentStep] = useState(0); // 0 = welcome screen
|
|
||||||
const [answers, setAnswers] = useState<EmployeeSubmissionAnswers>({});
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const [error, setError] = useState('');
|
|
||||||
const [inviteEmployee, setInviteEmployee] = useState<any>(null);
|
|
||||||
const [isLoadingInvite, setIsLoadingInvite] = useState(false);
|
|
||||||
|
|
||||||
// Get non-followup questions (we'll handle followups conditionally)
|
|
||||||
const visibleQuestions = EMPLOYEE_QUESTIONS.filter(q => !q.followupTo);
|
|
||||||
const totalSteps = visibleQuestions.length;
|
|
||||||
|
|
||||||
// Load invite details if this is an invite flow
|
|
||||||
useEffect(() => {
|
|
||||||
if (inviteCode) {
|
|
||||||
loadInviteDetails(inviteCode);
|
|
||||||
}
|
|
||||||
}, [inviteCode]);
|
|
||||||
|
|
||||||
const loadInviteDetails = async (code: string) => {
|
|
||||||
setIsLoadingInvite(true);
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_URL}/getInvitationStatus?code=${code}`);
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.used) {
|
|
||||||
setError('This invitation has already been used');
|
|
||||||
} else if (data.employee) {
|
|
||||||
setInviteEmployee(data.employee);
|
|
||||||
setError('');
|
|
||||||
} else {
|
|
||||||
setError('Invalid invitation data');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
||||||
setError(errorData.error || 'Invalid or expired invitation link');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error loading invite details:', err);
|
|
||||||
setError('Failed to load invitation details');
|
|
||||||
} finally {
|
|
||||||
setIsLoadingInvite(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get employee info from multiple sources
|
|
||||||
const invitedEmployee = location.state?.invitedEmployee;
|
|
||||||
|
|
||||||
// Determine current employee - for invite flow, use invite employee data
|
|
||||||
let currentEmployee;
|
|
||||||
if (isInviteFlow) {
|
|
||||||
currentEmployee = inviteEmployee;
|
|
||||||
} else {
|
|
||||||
// Original auth-based logic
|
|
||||||
currentEmployee = invitedEmployee || employees.find(emp => emp.email === user?.email);
|
|
||||||
|
|
||||||
if (!currentEmployee && user?.email) {
|
|
||||||
// Try case-insensitive email matching
|
|
||||||
currentEmployee = employees.find(emp =>
|
|
||||||
emp.email?.toLowerCase() === user.email?.toLowerCase()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!currentEmployee && invitedEmployee) {
|
|
||||||
currentEmployee = employees.find(emp =>
|
|
||||||
emp.name === invitedEmployee.name || emp.id === invitedEmployee.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Demo mode fallbacks
|
|
||||||
if (!currentEmployee && user?.email === 'demo@auditly.local' && employees.length > 0) {
|
|
||||||
currentEmployee = employees[employees.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentEmployee && employees.length === 1) {
|
|
||||||
currentEmployee = employees[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAnswerChange = (questionId: string, value: string) => {
|
|
||||||
setAnswers(prev => ({ ...prev, [questionId]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitViaInvite = async (answers: EmployeeSubmissionAnswers, inviteCode: string) => {
|
|
||||||
try {
|
|
||||||
// First, consume the invite to mark it as used
|
|
||||||
const consumeResponse = await fetch(`${API_URL}/consumeInvitation`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ code: inviteCode })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!consumeResponse.ok) {
|
|
||||||
throw new Error('Failed to process invitation');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get orgId from the consume response
|
|
||||||
const consumeData = await consumeResponse.json();
|
|
||||||
const orgId = consumeData.orgId;
|
|
||||||
|
|
||||||
// Submit the questionnaire answers using Cloud Function
|
|
||||||
const submitResponse = await fetch(`${API_URL}/submitEmployeeAnswers`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
inviteCode: inviteCode,
|
|
||||||
answers: answers,
|
|
||||||
orgId: orgId
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!submitResponse.ok) {
|
|
||||||
const errorData = await submitResponse.json();
|
|
||||||
throw new Error(errorData.error || 'Failed to submit questionnaire');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await submitResponse.json();
|
|
||||||
return { success: true, reportGenerated: !!result.report };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Invite submission error:', error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
setError('');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Validate required questions
|
|
||||||
const requiredQuestions = visibleQuestions.filter(q => q.required);
|
|
||||||
const missingAnswers = requiredQuestions.filter(q => !answers[q.id]?.trim());
|
|
||||||
|
|
||||||
if (missingAnswers.length > 0) {
|
|
||||||
setError(`Please answer all required questions`);
|
|
||||||
setIsSubmitting(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit answers - different logic for invite vs auth flow
|
|
||||||
let result;
|
|
||||||
if (isInviteFlow) {
|
|
||||||
// Direct API submission for invite flow (no auth needed)
|
|
||||||
result = await submitViaInvite(answers, inviteCode);
|
|
||||||
} else {
|
|
||||||
// Use org context for authenticated flow
|
|
||||||
if (!currentEmployee) {
|
|
||||||
// Enhanced fallback logic for authenticated users
|
|
||||||
if (employees.length > 0) {
|
|
||||||
let fallbackEmployee = employees.find(emp =>
|
|
||||||
emp.email?.toLowerCase().includes(user?.email?.toLowerCase().split('@')[0] || '')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!fallbackEmployee) {
|
|
||||||
const userDomain = user?.email?.split('@')[1];
|
|
||||||
fallbackEmployee = employees.find(emp =>
|
|
||||||
emp.email?.split('@')[1] === userDomain
|
|
||||||
) || employees[employees.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
const success = await submitEmployeeAnswers(fallbackEmployee.id, answers);
|
|
||||||
if (success) {
|
|
||||||
try {
|
|
||||||
const report = await generateEmployeeReport(fallbackEmployee);
|
|
||||||
console.log('Report generated successfully:', report);
|
|
||||||
} catch (reportError) {
|
|
||||||
console.error('Failed to generate report:', reportError);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to completion
|
|
||||||
setCurrentStep(totalSteps + 1); // Thank you page
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setError(`We couldn't match your account (${user?.email}) with an employee record. Please contact your administrator.`);
|
|
||||||
setIsSubmitting(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = await submitEmployeeAnswers(currentEmployee.id, answers);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
// Show thank you page
|
|
||||||
setCurrentStep(totalSteps + 1);
|
|
||||||
} else {
|
|
||||||
setError(result.message || 'Failed to submit questionnaire');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Submission error:', error);
|
|
||||||
setError('Failed to submit questionnaire. Please try again.');
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNext = () => {
|
|
||||||
if (currentStep === 0) {
|
|
||||||
// From welcome screen to first question
|
|
||||||
setCurrentStep(1);
|
|
||||||
} else if (currentStep === totalSteps) {
|
|
||||||
// From last question to submission
|
|
||||||
handleSubmit();
|
|
||||||
} else {
|
|
||||||
// Between questions
|
|
||||||
setCurrentStep(currentStep + 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBack = () => {
|
|
||||||
if (currentStep > 0) {
|
|
||||||
setCurrentStep(currentStep - 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSkip = () => {
|
|
||||||
if (currentStep < totalSteps) {
|
|
||||||
setCurrentStep(currentStep + 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Early return for invite flow loading state
|
|
||||||
if (isInviteFlow && isLoadingInvite) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4 flex items-center justify-center">
|
|
||||||
<div className="max-w-4xl mx-auto text-center">
|
|
||||||
<div className="w-16 h-16 bg-[--accent] rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
|
|
||||||
A
|
|
||||||
</div>
|
|
||||||
<h1 className="text-3xl font-bold text-[--text-primary] mb-4">Loading Your Invitation...</h1>
|
|
||||||
<p className="text-[--text-secondary]">Please wait while we verify your invitation.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Early return for invite flow error state
|
|
||||||
if (isInviteFlow && error && currentStep === 0) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4 flex items-center justify-center">
|
|
||||||
<div className="max-w-4xl mx-auto text-center">
|
|
||||||
<div className="w-16 h-16 bg-red-500 rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
|
|
||||||
!
|
|
||||||
</div>
|
|
||||||
<h1 className="text-3xl font-bold text-[--text-primary] mb-4">Invitation Error</h1>
|
|
||||||
<p className="text-[--text-secondary] mb-6">{error}</p>
|
|
||||||
<button
|
|
||||||
onClick={() => window.location.href = '/'}
|
|
||||||
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Return to Homepage
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render current step
|
|
||||||
if (currentStep === 0) {
|
|
||||||
// Welcome screen
|
|
||||||
return (
|
|
||||||
<WelcomeScreen
|
|
||||||
onStart={handleNext}
|
|
||||||
currentEmployee={currentEmployee}
|
|
||||||
isInviteFlow={isInviteFlow}
|
|
||||||
error={!currentEmployee && !isInviteFlow ? `Employee info not found. User: ${user?.email}` : undefined}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (currentStep > totalSteps) {
|
|
||||||
// Thank you page
|
|
||||||
return <ThankYouPage />;
|
|
||||||
} else {
|
|
||||||
// Question step
|
|
||||||
const question = visibleQuestions[currentStep - 1];
|
|
||||||
const isLastStep = currentStep === totalSteps;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<QuestionStep
|
|
||||||
question={question}
|
|
||||||
value={answers[question.id] || ''}
|
|
||||||
onChange={(value) => handleAnswerChange(question.id, value)}
|
|
||||||
onNext={handleNext}
|
|
||||||
onBack={currentStep > 1 ? handleBack : undefined}
|
|
||||||
onSkip={!question.required ? handleSkip : undefined}
|
|
||||||
currentStep={currentStep}
|
|
||||||
totalSteps={totalSteps}
|
|
||||||
isSubmitting={isSubmitting}
|
|
||||||
isLastStep={isLastStep}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EmployeeQuestionnaireMerged;
|
|
||||||
@@ -1,675 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useNavigate, useLocation, useParams } from 'react-router-dom';
|
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
|
||||||
import { useOrg } from '../contexts/OrgContext';
|
|
||||||
import { EMPLOYEE_QUESTIONS, EmployeeSubmissionAnswers } from '../employeeQuestions';
|
|
||||||
import { API_URL } from '../constants';
|
|
||||||
import { FigmaRatingScale, FigmaTextArea, FigmaNavigationButtons } from '../components/figma/FigmaQuestion';
|
|
||||||
import { FigmaMultipleChoice } from '../components/figma/FigmaMultipleChoice';
|
|
||||||
|
|
||||||
// Icon SVG Component
|
|
||||||
const AuditlyIcon: React.FC = () => (
|
|
||||||
<svg width="24" height="30" viewBox="0 0 24 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path opacity="0.5" fillRule="evenodd" clipRule="evenodd" d="M2.57408 17.8138C3.11835 18.3649 3.11834 19.2585 2.57406 19.8097L2.54619 19.8379C2.00191 20.389 1.11946 20.389 0.57519 19.8379C0.030919 19.2867 0.0309274 18.3931 0.575208 17.842L0.603083 17.8137C1.14736 17.2626 2.02981 17.2626 2.57408 17.8138Z" fill="url(#paint0_linear_981_10577)" />
|
|
||||||
<path opacity="0.7" fillRule="evenodd" clipRule="evenodd" d="M9.12583 18.2374C9.66912 18.7896 9.66752 19.6832 9.12226 20.2333L5.2617 24.1286C4.71644 24.6787 3.83399 24.6771 3.2907 24.125C2.74741 23.5728 2.74901 22.6792 3.29427 22.1291L7.15483 18.2338C7.70009 17.6837 8.58254 17.6853 9.12583 18.2374Z" fill="url(#paint1_linear_981_10577)" />
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="paint0_linear_981_10577" x1="1.57463" y1="17.4004" x2="1.57463" y2="20.2513" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint1_linear_981_10577" x1="6.20827" y1="17.8223" x2="6.20827" y2="24.5401" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Progress Bar Component for Section Headers
|
|
||||||
const SectionProgressBar: React.FC<{ currentStep: number; totalSteps: number }> = ({ currentStep, totalSteps }) => {
|
|
||||||
return (
|
|
||||||
<div className="p-4 bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
|
||||||
{Array.from({ length: Math.min(7, totalSteps) }, (_, index) => {
|
|
||||||
const isActive = index < Math.ceil((currentStep / totalSteps) * 7);
|
|
||||||
return (
|
|
||||||
<div key={index}>
|
|
||||||
{isActive ? (
|
|
||||||
<div className="w-6 h-1 bg-Brand-Orange rounded-3xl" />
|
|
||||||
) : (
|
|
||||||
<svg width="4" height="4" viewBox="0 0 4 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="4" height="4" rx="2" fill="var(--Neutrals-NeutralSlate300, #D5D7DA)" />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Question Input Component
|
|
||||||
const QuestionInput: React.FC<{
|
|
||||||
question: EmployeeQuestion;
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
}> = ({ question, value, onChange }) => {
|
|
||||||
switch (question.type) {
|
|
||||||
case 'scale':
|
|
||||||
return (
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
|
||||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
|
||||||
{question.prompt}
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex justify-between items-center gap-2">
|
|
||||||
<span className="text-sm text-Neutrals-NeutralSlate500">{question.scaleLabels?.min}</span>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{Array.from({ length: question.scaleMax! - question.scaleMin! + 1 }, (_, index) => {
|
|
||||||
const ratingValue = question.scaleMin! + index;
|
|
||||||
const isSelected = parseInt(value) === ratingValue;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={ratingValue}
|
|
||||||
onClick={() => onChange(ratingValue.toString())}
|
|
||||||
className={`w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium transition-colors ${
|
|
||||||
isSelected
|
|
||||||
? 'bg-Brand-Orange text-white'
|
|
||||||
: 'bg-Neutrals-NeutralSlate100 text-Neutrals-NeutralSlate700 hover:bg-Neutrals-NeutralSlate200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{ratingValue}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-Neutrals-NeutralSlate500">{question.scaleLabels?.max}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'yesno':
|
|
||||||
return (
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
|
||||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
|
||||||
{question.prompt}
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch inline-flex justify-center items-center gap-3">
|
|
||||||
<div
|
|
||||||
onClick={() => 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'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className={`absolute inset-0 flex items-center justify-center text-center text-base font-normal font-['Inter'] leading-normal ${
|
|
||||||
value === 'No' ? 'text-Neutrals-NeutralSlate0' : 'text-Neutrals-NeutralSlate950'
|
|
||||||
}`}>
|
|
||||||
No
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={() => 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'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className={`absolute inset-0 flex items-center justify-center text-center text-base font-normal font-['Inter'] leading-normal ${
|
|
||||||
value === 'Yes' ? 'text-Neutrals-NeutralSlate0' : 'text-Neutrals-NeutralSlate950'
|
|
||||||
}`}>
|
|
||||||
Yes
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'textarea':
|
|
||||||
return (
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
|
||||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
|
||||||
{question.prompt}
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch min-h-40 p-5 relative bg-Neutrals-NeutralSlate100 rounded-xl inline-flex justify-start items-start gap-2.5">
|
|
||||||
<textarea
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
className="flex-1 bg-transparent text-Neutrals-NeutralSlate950 text-base font-normal font-['Inter'] leading-normal placeholder:text-Neutrals-NeutralSlate500 outline-none resize-none"
|
|
||||||
placeholder={question.placeholder || "Type your answer...."}
|
|
||||||
rows={6}
|
|
||||||
/>
|
|
||||||
<div className="w-3 h-3 absolute right-5 bottom-5">
|
|
||||||
<div className="w-2 h-2 absolute top-0.5 left-0.5 outline outline-1 outline-offset-[-0.50px] outline-Neutrals-NeutralSlate500" />
|
|
||||||
<div className="w-1 h-1 absolute bottom-0 right-0 outline outline-1 outline-offset-[-0.50px] outline-Neutrals-NeutralSlate500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
default: // text input
|
|
||||||
return (
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
|
||||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
|
||||||
{question.prompt}
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch px-4 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
|
||||||
<input
|
|
||||||
type={question.id === 'email' ? 'email' : 'text'}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
className="flex-1 bg-transparent text-Neutrals-NeutralSlate950 text-sm font-normal font-['Inter'] leading-tight placeholder:text-Neutrals-NeutralSlate500 outline-none"
|
|
||||||
placeholder={question.placeholder || "Enter your answer..."}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Navigation Buttons Component
|
|
||||||
const NavigationButtons: React.FC<{
|
|
||||||
onBack?: () => void;
|
|
||||||
onNext: () => void;
|
|
||||||
onSkip?: () => void;
|
|
||||||
nextDisabled?: boolean;
|
|
||||||
isSubmitting?: boolean;
|
|
||||||
currentStep: number;
|
|
||||||
totalSteps: number;
|
|
||||||
isLastStep?: boolean;
|
|
||||||
}> = ({ onBack, onNext, onSkip, nextDisabled, isSubmitting, currentStep, totalSteps, isLastStep }) => {
|
|
||||||
return (
|
|
||||||
<div className="self-stretch inline-flex justify-start items-start gap-2">
|
|
||||||
{onBack && (
|
|
||||||
<button
|
|
||||||
onClick={onBack}
|
|
||||||
className="h-12 px-8 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-Neutrals-NeutralSlate200"
|
|
||||||
>
|
|
||||||
<div className="px-1 flex justify-center items-center">
|
|
||||||
<div className="justify-center text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">Back</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={onNext}
|
|
||||||
disabled={nextDisabled || isSubmitting}
|
|
||||||
className="flex-1 h-12 px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
<div className="px-1 flex justify-center items-center">
|
|
||||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">
|
|
||||||
{isSubmitting ? 'Submitting...' : (isLastStep ? 'Submit' : 'Next')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{onSkip && (
|
|
||||||
<div
|
|
||||||
onClick={onSkip}
|
|
||||||
className="px-3 py-1.5 absolute right-6 top-6 bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden cursor-pointer hover:bg-Neutrals-NeutralSlate200"
|
|
||||||
>
|
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] leading-none">Skip</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Welcome Screen Component
|
|
||||||
const WelcomeScreen: React.FC<{
|
|
||||||
onStart: () => void;
|
|
||||||
currentEmployee?: any;
|
|
||||||
isInviteFlow: boolean;
|
|
||||||
error?: string;
|
|
||||||
}> = ({ onStart, currentEmployee, isInviteFlow, error }) => {
|
|
||||||
return (
|
|
||||||
<div className="w-full h-screen bg-white inline-flex justify-start items-center overflow-hidden">
|
|
||||||
<div className="flex-1 h-full px-32 py-48 bg-Neutrals-NeutralSlate0 flex justify-center items-center gap-2.5 overflow-hidden">
|
|
||||||
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
|
||||||
<div className="w-12 h-12 relative bg-Brand-Orange rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
|
||||||
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
|
||||||
<div className="left-[12px] top-[9.33px] absolute">
|
|
||||||
<AuditlyIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
|
||||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate800 text-5xl font-medium font-['Neue_Montreal'] leading-[48px]">
|
|
||||||
Welcome to Auditly!
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch justify-center text-Neutrals-NeutralSlate500 text-base font-normal font-['Inter'] leading-normal">
|
|
||||||
Please complete this questionnaire to help us understand your role and create personalized insights.
|
|
||||||
</div>
|
|
||||||
{currentEmployee && (
|
|
||||||
<div className="inline-flex items-center px-4 py-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
|
|
||||||
<span className="text-sm text-blue-800 dark:text-blue-200">
|
|
||||||
👋 Hello {currentEmployee.name}! {currentEmployee.role && `(${currentEmployee.role})`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{error && (
|
|
||||||
<div className="inline-flex items-center px-4 py-2 bg-red-100 dark:bg-red-900 rounded-lg">
|
|
||||||
<span className="text-sm text-red-800 dark:text-red-200">
|
|
||||||
⚠️ {error}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={onStart}
|
|
||||||
className="self-stretch px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 inline-flex justify-center items-center gap-1 overflow-hidden"
|
|
||||||
>
|
|
||||||
<div className="px-1 flex justify-center items-center">
|
|
||||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Start Assessment</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 h-full px-20 py-16 flex justify-center items-center gap-2.5 overflow-hidden">
|
|
||||||
<div className="flex-1 self-stretch origin-top-left rotate-180 rounded-3xl inline-flex flex-col justify-center items-center gap-2.5 overflow-hidden">
|
|
||||||
<img className="self-stretch flex-1" src="https://placehold.co/560x682" alt="Welcome" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Question Step Component
|
|
||||||
const QuestionStep: React.FC<{
|
|
||||||
question: EmployeeQuestion;
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
onNext: () => void;
|
|
||||||
onBack?: () => void;
|
|
||||||
onSkip?: () => void;
|
|
||||||
currentStep: number;
|
|
||||||
totalSteps: number;
|
|
||||||
isSubmitting?: boolean;
|
|
||||||
isLastStep?: boolean;
|
|
||||||
}> = ({ question, value, onChange, onNext, onBack, onSkip, currentStep, totalSteps, isSubmitting, isLastStep }) => {
|
|
||||||
const isRequired = question.required;
|
|
||||||
const isAnswered = value && value.trim().length > 0;
|
|
||||||
const nextDisabled = isRequired ? !isAnswered : false;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full h-screen py-6 relative bg-Neutrals-NeutralSlate0 inline-flex flex-col justify-center items-center gap-9">
|
|
||||||
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
|
|
||||||
<QuestionInput question={question} value={value} onChange={onChange} />
|
|
||||||
<NavigationButtons
|
|
||||||
onBack={onBack}
|
|
||||||
onNext={onNext}
|
|
||||||
onSkip={onSkip}
|
|
||||||
nextDisabled={nextDisabled}
|
|
||||||
isSubmitting={isSubmitting}
|
|
||||||
currentStep={currentStep}
|
|
||||||
totalSteps={totalSteps}
|
|
||||||
isLastStep={isLastStep}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress indicators */}
|
|
||||||
<div className="px-3 py-1.5 left-[24px] top-[24px] absolute bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] uppercase leading-none">
|
|
||||||
{currentStep} of {totalSteps}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-[464px] max-w-[464px] min-w-[464px] left-[488px] top-[24px] absolute flex flex-col justify-start items-center gap-4">
|
|
||||||
<SectionProgressBar currentStep={currentStep} totalSteps={totalSteps} />
|
|
||||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate500 text-base font-medium font-['Neue_Montreal'] leading-normal">
|
|
||||||
Employee Assessment
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Thank You Page Component
|
|
||||||
const ThankYouPage: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<div className="w-full h-screen bg-white inline-flex justify-start items-center overflow-hidden">
|
|
||||||
<div className="flex-1 h-full px-32 py-48 bg-Neutrals-NeutralSlate0 flex justify-center items-center gap-2.5 overflow-hidden">
|
|
||||||
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-12">
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-6">
|
|
||||||
<div className="w-12 h-12 relative bg-Brand-Orange rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
|
||||||
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
|
||||||
<div className="left-[12px] top-[9.33px] absolute">
|
|
||||||
<AuditlyIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
|
||||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate800 text-5xl font-medium font-['Neue_Montreal'] leading-[48px]">
|
|
||||||
Thank you! Your assessment has been submitted!
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch justify-center text-Neutrals-NeutralSlate500 text-base font-normal font-['Inter'] leading-normal">
|
|
||||||
Your responses have been recorded and your AI-powered performance report will be generated shortly.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 h-full px-20 py-16 flex justify-center items-center gap-2.5 overflow-hidden">
|
|
||||||
<div className="flex-1 self-stretch origin-top-left rotate-180 rounded-3xl inline-flex flex-col justify-center items-center gap-2.5 overflow-hidden">
|
|
||||||
<img className="self-stretch flex-1" src="https://placehold.co/560x682" alt="Thank you" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Main Component
|
|
||||||
const EmployeeQuestionnaire: React.FC = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
const params = useParams();
|
|
||||||
const { user } = useAuth();
|
|
||||||
|
|
||||||
// Check if this is an invite-based flow (no auth/org needed)
|
|
||||||
const inviteCode = params.inviteCode;
|
|
||||||
const isInviteFlow = !!inviteCode;
|
|
||||||
|
|
||||||
// Only use org context for authenticated flows
|
|
||||||
let submitEmployeeAnswers, generateEmployeeReport, employees;
|
|
||||||
if (!isInviteFlow) {
|
|
||||||
const orgContext = useOrg();
|
|
||||||
({ submitEmployeeAnswers, generateEmployeeReport, employees } = orgContext);
|
|
||||||
} else {
|
|
||||||
// For invite flows, we don't need these functions from org context
|
|
||||||
submitEmployeeAnswers = null;
|
|
||||||
generateEmployeeReport = null;
|
|
||||||
employees = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const [currentStep, setCurrentStep] = useState(0); // 0 = welcome screen
|
|
||||||
const [answers, setAnswers] = useState<EmployeeSubmissionAnswers>({});
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const [error, setError] = useState('');
|
|
||||||
const [inviteEmployee, setInviteEmployee] = useState<any>(null);
|
|
||||||
const [isLoadingInvite, setIsLoadingInvite] = useState(false);
|
|
||||||
|
|
||||||
// Get non-followup questions (we'll handle followups conditionally)
|
|
||||||
const visibleQuestions = EMPLOYEE_QUESTIONS.filter(q => !q.followupTo);
|
|
||||||
const totalSteps = visibleQuestions.length;
|
|
||||||
|
|
||||||
// Load invite details if this is an invite flow
|
|
||||||
useEffect(() => {
|
|
||||||
if (inviteCode) {
|
|
||||||
loadInviteDetails(inviteCode);
|
|
||||||
}
|
|
||||||
}, [inviteCode]);
|
|
||||||
|
|
||||||
const loadInviteDetails = async (code: string) => {
|
|
||||||
setIsLoadingInvite(true);
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_URL}/getInvitationStatus?code=${code}`);
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.used) {
|
|
||||||
setError('This invitation has already been used');
|
|
||||||
} else if (data.employee) {
|
|
||||||
setInviteEmployee(data.employee);
|
|
||||||
setError('');
|
|
||||||
} else {
|
|
||||||
setError('Invalid invitation data');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
||||||
setError(errorData.error || 'Invalid or expired invitation link');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error loading invite details:', err);
|
|
||||||
setError('Failed to load invitation details');
|
|
||||||
} finally {
|
|
||||||
setIsLoadingInvite(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get employee info from multiple sources
|
|
||||||
const invitedEmployee = location.state?.invitedEmployee;
|
|
||||||
|
|
||||||
// Determine current employee - for invite flow, use invite employee data
|
|
||||||
let currentEmployee;
|
|
||||||
if (isInviteFlow) {
|
|
||||||
currentEmployee = inviteEmployee;
|
|
||||||
} else {
|
|
||||||
// Original auth-based logic
|
|
||||||
currentEmployee = invitedEmployee || employees.find(emp => emp.email === user?.email);
|
|
||||||
|
|
||||||
if (!currentEmployee && user?.email) {
|
|
||||||
// Try case-insensitive email matching
|
|
||||||
currentEmployee = employees.find(emp =>
|
|
||||||
emp.email?.toLowerCase() === user.email?.toLowerCase()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!currentEmployee && invitedEmployee) {
|
|
||||||
currentEmployee = employees.find(emp =>
|
|
||||||
emp.name === invitedEmployee.name || emp.id === invitedEmployee.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Demo mode fallbacks
|
|
||||||
if (!currentEmployee && user?.email === 'demo@auditly.local' && employees.length > 0) {
|
|
||||||
currentEmployee = employees[employees.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentEmployee && employees.length === 1) {
|
|
||||||
currentEmployee = employees[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAnswerChange = (questionId: string, value: string) => {
|
|
||||||
setAnswers(prev => ({ ...prev, [questionId]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitViaInvite = async (answers: EmployeeSubmissionAnswers, inviteCode: string) => {
|
|
||||||
try {
|
|
||||||
// First, consume the invite to mark it as used
|
|
||||||
const consumeResponse = await fetch(`${API_URL}/consumeInvitation`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ code: inviteCode })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!consumeResponse.ok) {
|
|
||||||
throw new Error('Failed to process invitation');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get orgId from the consume response
|
|
||||||
const consumeData = await consumeResponse.json();
|
|
||||||
const orgId = consumeData.orgId;
|
|
||||||
|
|
||||||
// Submit the questionnaire answers using Cloud Function
|
|
||||||
const submitResponse = await fetch(`${API_URL}/submitEmployeeAnswers`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
inviteCode: inviteCode,
|
|
||||||
answers: answers,
|
|
||||||
orgId: orgId
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!submitResponse.ok) {
|
|
||||||
const errorData = await submitResponse.json();
|
|
||||||
throw new Error(errorData.error || 'Failed to submit questionnaire');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await submitResponse.json();
|
|
||||||
return { success: true, reportGenerated: !!result.report };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Invite submission error:', error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
setError('');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Validate required questions
|
|
||||||
const requiredQuestions = visibleQuestions.filter(q => q.required);
|
|
||||||
const missingAnswers = requiredQuestions.filter(q => !answers[q.id]?.trim());
|
|
||||||
|
|
||||||
if (missingAnswers.length > 0) {
|
|
||||||
setError(`Please answer all required questions`);
|
|
||||||
setIsSubmitting(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit answers - different logic for invite vs auth flow
|
|
||||||
let result;
|
|
||||||
if (isInviteFlow) {
|
|
||||||
// Direct API submission for invite flow (no auth needed)
|
|
||||||
result = await submitViaInvite(answers, inviteCode);
|
|
||||||
} else {
|
|
||||||
// Use org context for authenticated flow
|
|
||||||
if (!currentEmployee) {
|
|
||||||
// Enhanced fallback logic for authenticated users
|
|
||||||
if (employees.length > 0) {
|
|
||||||
let fallbackEmployee = employees.find(emp =>
|
|
||||||
emp.email?.toLowerCase().includes(user?.email?.toLowerCase().split('@')[0] || '')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!fallbackEmployee) {
|
|
||||||
const userDomain = user?.email?.split('@')[1];
|
|
||||||
fallbackEmployee = employees.find(emp =>
|
|
||||||
emp.email?.split('@')[1] === userDomain
|
|
||||||
) || employees[employees.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
const success = await submitEmployeeAnswers(fallbackEmployee.id, answers);
|
|
||||||
if (success) {
|
|
||||||
try {
|
|
||||||
const report = await generateEmployeeReport(fallbackEmployee);
|
|
||||||
console.log('Report generated successfully:', report);
|
|
||||||
} catch (reportError) {
|
|
||||||
console.error('Failed to generate report:', reportError);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to completion
|
|
||||||
setCurrentStep(totalSteps + 1); // Thank you page
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setError(`We couldn't match your account (${user?.email}) with an employee record. Please contact your administrator.`);
|
|
||||||
setIsSubmitting(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = await submitEmployeeAnswers(currentEmployee.id, answers);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
// Show thank you page
|
|
||||||
setCurrentStep(totalSteps + 1);
|
|
||||||
} else {
|
|
||||||
setError(result.message || 'Failed to submit questionnaire');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Submission error:', error);
|
|
||||||
setError('Failed to submit questionnaire. Please try again.');
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNext = () => {
|
|
||||||
if (currentStep === 0) {
|
|
||||||
// From welcome screen to first question
|
|
||||||
setCurrentStep(1);
|
|
||||||
} else if (currentStep === totalSteps) {
|
|
||||||
// From last question to submission
|
|
||||||
handleSubmit();
|
|
||||||
} else {
|
|
||||||
// Between questions
|
|
||||||
setCurrentStep(currentStep + 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBack = () => {
|
|
||||||
if (currentStep > 0) {
|
|
||||||
setCurrentStep(currentStep - 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSkip = () => {
|
|
||||||
if (currentStep < totalSteps) {
|
|
||||||
setCurrentStep(currentStep + 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Early return for invite flow loading state
|
|
||||||
if (isInviteFlow && isLoadingInvite) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4 flex items-center justify-center">
|
|
||||||
<div className="max-w-4xl mx-auto text-center">
|
|
||||||
<div className="w-16 h-16 bg-[--accent] rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
|
|
||||||
A
|
|
||||||
</div>
|
|
||||||
<h1 className="text-3xl font-bold text-[--text-primary] mb-4">Loading Your Invitation...</h1>
|
|
||||||
<p className="text-[--text-secondary]">Please wait while we verify your invitation.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Early return for invite flow error state
|
|
||||||
if (isInviteFlow && error && currentStep === 0) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-[--background-primary] py-8 px-4 flex items-center justify-center">
|
|
||||||
<div className="max-w-4xl mx-auto text-center">
|
|
||||||
<div className="w-16 h-16 bg-red-500 rounded-full flex items-center justify-center font-bold text-white text-2xl mx-auto mb-4">
|
|
||||||
!
|
|
||||||
</div>
|
|
||||||
<h1 className="text-3xl font-bold text-[--text-primary] mb-4">Invitation Error</h1>
|
|
||||||
<p className="text-[--text-secondary] mb-6">{error}</p>
|
|
||||||
<button
|
|
||||||
onClick={() => window.location.href = '/'}
|
|
||||||
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Return to Homepage
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render current step
|
|
||||||
if (currentStep === 0) {
|
|
||||||
// Welcome screen
|
|
||||||
return (
|
|
||||||
<WelcomeScreen
|
|
||||||
onStart={handleNext}
|
|
||||||
currentEmployee={currentEmployee}
|
|
||||||
isInviteFlow={isInviteFlow}
|
|
||||||
error={!currentEmployee && !isInviteFlow ? `Employee info not found. User: ${user?.email}` : undefined}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (currentStep > totalSteps) {
|
|
||||||
// Thank you page
|
|
||||||
return <ThankYouPage />;
|
|
||||||
} else {
|
|
||||||
// Question step
|
|
||||||
const question = visibleQuestions[currentStep - 1];
|
|
||||||
const isLastStep = currentStep === totalSteps;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<QuestionStep
|
|
||||||
question={question}
|
|
||||||
value={answers[question.id] || ''}
|
|
||||||
onChange={(value) => handleAnswerChange(question.id, value)}
|
|
||||||
onNext={handleNext}
|
|
||||||
onBack={currentStep > 1 ? handleBack : undefined}
|
|
||||||
onSkip={!question.required ? handleSkip : undefined}
|
|
||||||
currentStep={currentStep}
|
|
||||||
totalSteps={totalSteps}
|
|
||||||
isSubmitting={isSubmitting}
|
|
||||||
isLastStep={isLastStep}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EmployeeQuestionnaire;
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
|
||||||
|
|
||||||
const OTPVerification: React.FC = () => {
|
|
||||||
const [otp, setOtp] = useState(['', '', '', '', '', '']);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const { verifyOTP } = useAuth();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const email = location.state?.email || '';
|
|
||||||
|
|
||||||
const handleOtpChange = (index: number, value: string) => {
|
|
||||||
if (value.length <= 1) {
|
|
||||||
const newOtp = [...otp];
|
|
||||||
newOtp[index] = value;
|
|
||||||
setOtp(newOtp);
|
|
||||||
|
|
||||||
// Auto-focus next input
|
|
||||||
if (value && index < 5) {
|
|
||||||
const nextInput = document.getElementById(`otp-${index + 1}`);
|
|
||||||
nextInput?.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (index: number, e: React.KeyboardEvent) => {
|
|
||||||
if (e.key === 'Backspace' && !otp[index] && index > 0) {
|
|
||||||
const prevInput = document.getElementById(`otp-${index - 1}`);
|
|
||||||
prevInput?.focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVerify = async () => {
|
|
||||||
const otpCode = otp.join('');
|
|
||||||
if (otpCode.length !== 6) return;
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
await verifyOTP(email, otpCode);
|
|
||||||
navigate('/company-wiki');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('OTP verification failed:', error);
|
|
||||||
// Reset OTP on error
|
|
||||||
setOtp(['', '', '', '', '', '']);
|
|
||||||
document.getElementById('otp-0')?.focus();
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-[1440px] h-[810px] bg-Neutrals-NeutralSlate0 inline-flex justify-start items-end overflow-hidden">
|
|
||||||
<div className="flex-1 self-stretch px-32 py-48 flex justify-center items-center gap-2.5 overflow-hidden">
|
|
||||||
<div className="flex-1 max-w-[464px] inline-flex flex-col justify-start items-start gap-6">
|
|
||||||
<div className="w-12 h-12 relative bg-Brand-Orange rounded-xl outline outline-2 outline-offset-[-2px] outline-blue-400 overflow-hidden">
|
|
||||||
<div className="w-12 h-12 left-0 top-0 absolute bg-gradient-to-b from-white/0 to-white/10" />
|
|
||||||
<div className="left-[12px] top-[9.33px] absolute">
|
|
||||||
<svg width="24" height="30" viewBox="0 0 24 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path opacity="0.5" fillRule="evenodd" clipRule="evenodd" d="M2.57425 17.8128C3.11852 18.3639 3.11851 19.2575 2.57423 19.8087L2.54635 19.8369C2.00207 20.3881 1.11963 20.3881 0.575357 19.8369C0.0310869 19.2857 0.0310953 18.3921 0.575376 17.841L0.603251 17.8128C1.14753 17.2616 2.02998 17.2616 2.57425 17.8128Z" fill="url(#paint0_linear_710_14140)" />
|
|
||||||
<path opacity="0.7" fillRule="evenodd" clipRule="evenodd" d="M9.12599 18.2379C9.66928 18.7901 9.66769 19.6837 9.12243 20.2338L5.26187 24.1291C4.71661 24.6792 3.83416 24.6776 3.29087 24.1255C2.74758 23.5733 2.74918 22.6797 3.29444 22.1296L7.155 18.2343C7.70026 17.6842 8.5827 17.6858 9.12599 18.2379Z" fill="url(#paint1_linear_710_14140)" />
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="paint0_linear_710_14140" x1="1.5748" y1="17.3994" x2="1.5748" y2="20.2503" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint1_linear_710_14140" x1="6.20843" y1="17.8228" x2="6.20843" y2="24.5406" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stopColor="white" stopOpacity="0.8" />
|
|
||||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
|
||||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate950 text-2xl font-semibold font-['Inter'] leading-8">Verify your email</div>
|
|
||||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate500 text-base font-normal font-['Inter'] leading-normal">
|
|
||||||
Enter the 6-digit code sent to {email}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-4">
|
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
|
||||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
|
||||||
<div className="justify-start text-Neutrals-NeutralSlate900 text-sm font-normal font-['Inter'] leading-tight">Verification Code</div>
|
|
||||||
<div className="justify-start text-Brand-Orange text-sm font-medium font-['Inter'] leading-tight">*</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch flex justify-center items-center gap-3">
|
|
||||||
{otp.map((digit, index) => (
|
|
||||||
<input
|
|
||||||
key={index}
|
|
||||||
id={`otp-${index}`}
|
|
||||||
type="text"
|
|
||||||
value={digit}
|
|
||||||
onChange={(e) => handleOtpChange(index, e.target.value)}
|
|
||||||
onKeyDown={(e) => handleKeyDown(index, e)}
|
|
||||||
className="w-12 h-12 bg-Neutrals-NeutralSlate100 rounded-xl text-center text-Neutrals-NeutralSlate950 text-lg font-semibold font-['Inter'] outline-none focus:bg-Neutrals-NeutralSlate200 focus:outline-2 focus:outline-Brand-Orange"
|
|
||||||
maxLength={1}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={handleVerify}
|
|
||||||
disabled={otp.join('').length !== 6 || isLoading}
|
|
||||||
className="self-stretch px-6 py-3.5 bg-Brand-Orange rounded-[999px] inline-flex justify-center items-center gap-2 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
<div className="justify-center text-Neutrals-NeutralSlate0 text-base font-medium font-['Inter'] leading-normal">
|
|
||||||
{isLoading ? 'Verifying...' : 'Verify Code'}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<div className="self-stretch text-center text-Neutrals-NeutralSlate500 text-sm font-normal font-['Inter'] leading-tight">
|
|
||||||
Didn't receive the code? <button className="text-Brand-Orange font-medium">Resend</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OTPVerification;
|
|
||||||
Binary file not shown.
@@ -19,7 +19,6 @@ import EmployeeQuestionnaire from './pages/EmployeeQuestionnaire';
|
|||||||
import EmployeeQuestionnaireSteps from './pages/EmployeeQuestionnaireSteps';
|
import EmployeeQuestionnaireSteps from './pages/EmployeeQuestionnaireSteps';
|
||||||
import QuestionTypesDemo from './pages/QuestionTypesDemo';
|
import QuestionTypesDemo from './pages/QuestionTypesDemo';
|
||||||
import FormsDashboard from './pages/FormsDashboard';
|
import FormsDashboard from './pages/FormsDashboard';
|
||||||
import DebugEmployee from './pages/DebugEmployee';
|
|
||||||
import QuestionnaireComplete from './pages/QuestionnaireComplete';
|
import QuestionnaireComplete from './pages/QuestionnaireComplete';
|
||||||
import SubscriptionSetup from './pages/SubscriptionSetup';
|
import SubscriptionSetup from './pages/SubscriptionSetup';
|
||||||
|
|
||||||
@@ -178,7 +177,7 @@ function App() {
|
|||||||
|
|
||||||
{/* New Figma Chat Implementation - Standalone route */}
|
{/* New Figma Chat Implementation - Standalone route */}
|
||||||
<Route
|
<Route
|
||||||
path="/chat-new"
|
path="/chat"
|
||||||
element={
|
element={
|
||||||
<RequireAuth>
|
<RequireAuth>
|
||||||
<RequireOrgSelection>
|
<RequireOrgSelection>
|
||||||
@@ -242,7 +241,7 @@ function App() {
|
|||||||
<Route path="/company-wiki" element={<CompanyWiki />} />
|
<Route path="/company-wiki" element={<CompanyWiki />} />
|
||||||
<Route path="/submissions" element={<EmployeeData mode="submissions" />} />
|
<Route path="/submissions" element={<EmployeeData mode="submissions" />} />
|
||||||
<Route path="/reports" element={<EmployeeData mode="reports" />} />
|
<Route path="/reports" element={<EmployeeData mode="reports" />} />
|
||||||
<Route path="/chat" element={<Chat />} />
|
<Route path="/chat-old" element={<Chat />} />
|
||||||
<Route path="/help" element={<HelpAndSettings />} />
|
<Route path="/help" element={<HelpAndSettings />} />
|
||||||
<Route path="/settings" element={<HelpAndSettings />} />
|
<Route path="/settings" element={<HelpAndSettings />} />
|
||||||
</Route>
|
</Route>
|
||||||
@@ -272,18 +271,6 @@ function App() {
|
|||||||
</RequireAuth>
|
</RequireAuth>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
path="/debug-employee"
|
|
||||||
element={
|
|
||||||
<RequireAuth>
|
|
||||||
<RequireOrgSelection>
|
|
||||||
<OrgProviderWrapper>
|
|
||||||
<DebugEmployee />
|
|
||||||
</OrgProviderWrapper>
|
|
||||||
</RequireOrgSelection>
|
|
||||||
</RequireAuth>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</UserOrganizationsProvider>
|
</UserOrganizationsProvider>
|
||||||
@@ -10,17 +10,17 @@ interface SuggestionCardProps {
|
|||||||
|
|
||||||
const SuggestionCard: React.FC<SuggestionCardProps> = ({ category, title, description, icon, onClick }) => (
|
const SuggestionCard: React.FC<SuggestionCardProps> = ({ category, title, description, icon, onClick }) => (
|
||||||
<div
|
<div
|
||||||
className="p-4 bg-Neutrals-NeutralSlate0 rounded-2xl border border-Neutrals-NeutralSlate200 hover:border-Brand-Orange hover:shadow-sm transition-all cursor-pointer group"
|
className="p-4 bg-[var(--Neutrals-NeutralSlate0)] rounded-2xl border border-[var(--Neutrals-NeutralSlate200)] hover:border-[var(--Brand-Orange)] hover:shadow-sm transition-all cursor-pointer group"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className="w-10 h-10 bg-Brand-Orange/10 rounded-xl flex items-center justify-center text-Brand-Orange group-hover:bg-Brand-Orange group-hover:text-white transition-colors">
|
<div className="w-10 h-10 bg-[var(--Brand-Orange)]/10 rounded-xl flex items-center justify-center text-[var(--Brand-Orange)] group-hover:bg-[var(--Brand-Orange)] group-hover:text-white transition-colors">
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-xs text-Brand-Orange font-medium mb-1">{category}</div>
|
<div className="text-xs text-[var(--Brand-Orange)] font-medium mb-1">{category}</div>
|
||||||
<div className="text-sm font-medium text-Neutrals-NeutralSlate950 mb-1">{title}</div>
|
<div className="text-sm font-medium text-[var(--Neutrals-NeutralSlate950)] mb-1">{title}</div>
|
||||||
<div className="text-xs text-Neutrals-NeutralSlate500 leading-relaxed">{description}</div>
|
<div className="text-xs text-[var(--Neutrals-NeutralSlate500)] leading-relaxed">{description}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,8 +36,8 @@ const CategoryTab: React.FC<CategoryTabProps> = ({ label, isActive, onClick }) =
|
|||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${isActive
|
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${isActive
|
||||||
? 'bg-Brand-Orange text-white'
|
? 'bg-[var(--Brand-Orange)] text-white'
|
||||||
: 'bg-Neutrals-NeutralSlate100 text-Neutrals-NeutralSlate600 hover:bg-Neutrals-NeutralSlate200'
|
: 'bg-[var(--Neutrals-NeutralSlate100)] text-[var(--Neutrals-NeutralSlate600)] hover:bg-[var(--Neutrals-NeutralSlate200)]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
@@ -131,10 +131,10 @@ const ChatEmptyState: React.FC = () => {
|
|||||||
<div className="flex flex-col items-center justify-center min-h-[60vh] px-4">
|
<div className="flex flex-col items-center justify-center min-h-[60vh] px-4">
|
||||||
{/* Welcome Message */}
|
{/* Welcome Message */}
|
||||||
<div className="text-center mb-8 max-w-2xl">
|
<div className="text-center mb-8 max-w-2xl">
|
||||||
<h2 className="text-2xl font-semibold text-Neutrals-NeutralSlate950 mb-3">
|
<h2 className="text-2xl font-semibold text-[var(--Neutrals-NeutralSlate950)] mb-3">
|
||||||
Welcome to Auditly Chat
|
Welcome to Auditly Chat
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-Neutrals-NeutralSlate600 text-lg leading-relaxed">
|
<p className="text-[var(--Neutrals-NeutralSlate600)] text-lg leading-relaxed">
|
||||||
Ask me anything about your team's performance, company culture, or organizational insights.
|
Ask me anything about your team's performance, company culture, or organizational insights.
|
||||||
I can analyze employee data, generate reports, and provide actionable recommendations.
|
I can analyze employee data, generate reports, and provide actionable recommendations.
|
||||||
</p>
|
</p>
|
||||||
@@ -167,7 +167,7 @@ const ChatEmptyState: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Additional Help Text */}
|
{/* Additional Help Text */}
|
||||||
<div className="mt-8 text-center text-sm text-Neutrals-NeutralSlate500 max-w-xl">
|
<div className="mt-8 text-center text-sm text-[var(--Neutrals-NeutralSlate500)] max-w-xl">
|
||||||
<p>
|
<p>
|
||||||
You can also upload files, mention specific employees using @, or ask custom questions about your organization.
|
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.
|
I'll provide insights based on your team's data and industry best practices.
|
||||||
159
src/components/chat/ChatLayout.tsx
Normal file
159
src/components/chat/ChatLayout.tsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { useOrg } from '../../contexts/OrgContext';
|
||||||
|
import { Employee } from '../../types';
|
||||||
|
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<ChatLayoutProps> = ({ children }) => {
|
||||||
|
const { employees } = useOrg();
|
||||||
|
const [selectedEmployees, setSelectedEmployees] = useState<Employee[]>([]);
|
||||||
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="w-full h-full flex flex-col overflow-hidden">
|
||||||
|
{/* Header with Employee Selection */}
|
||||||
|
<div className="px-6 py-4 bg-[var(--Neutrals-NeutralSlate0)] border-b border-[var(--Neutrals-NeutralSlate200)] flex justify-between items-center">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<h1 className="text-xl font-semibold text-[var(--Neutrals-NeutralSlate950)]">Chat</h1>
|
||||||
|
{selectedEmployees.length > 0 && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-[var(--Neutrals-NeutralSlate500)]">Analyzing:</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{selectedEmployees.slice(0, 3).map((emp, index) => (
|
||||||
|
<div key={emp.id} className="px-2 py-1 bg-[var(--Brand-Orange)]/10 rounded-full text-xs text-[var(--Brand-Orange)]">
|
||||||
|
{emp.name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{selectedEmployees.length > 3 && (
|
||||||
|
<div className="px-2 py-1 bg-[var(--Neutrals-NeutralSlate100)] rounded-full text-xs text-[var(--Neutrals-NeutralSlate600)]">
|
||||||
|
+{selectedEmployees.length - 3} more
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Messages Area */}
|
||||||
|
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||||
|
{hasMessages ? (
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<MessageThread
|
||||||
|
messages={messages}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input Area */}
|
||||||
|
<div className="px-6 py-4 bg-[var(--Neutrals-NeutralSlate0)] border-t border-[var(--Neutrals-NeutralSlate200)]">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<div className="flex items-end gap-3">
|
||||||
|
<FileUploadInput
|
||||||
|
value={inputValue}
|
||||||
|
onChange={setInputValue}
|
||||||
|
onKeyDown={handleKeyPress}
|
||||||
|
placeholder="Ask about your team's performance, culture, or any insights..."
|
||||||
|
disabled={isLoading}
|
||||||
|
uploadedFiles={uploadedFiles}
|
||||||
|
onRemoveFile={handleRemoveFile}
|
||||||
|
onFilesSelected={handleFilesSelected}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Send Button */}
|
||||||
|
<button
|
||||||
|
onClick={handleSendMessage}
|
||||||
|
disabled={!inputValue.trim() && uploadedFiles.length === 0}
|
||||||
|
className="px-4 py-3 bg-[var(--Brand-Orange)] text-white rounded-xl hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex-shrink-0"
|
||||||
|
>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M18.3346 1.66797L9.16797 10.8346M18.3346 1.66797L12.5013 18.3346L9.16797 10.8346M18.3346 1.66797L1.66797 7.5013L9.16797 10.8346" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatLayout;
|
||||||
@@ -59,13 +59,13 @@ const FileUploadPreview: React.FC<FileUploadPreviewProps> = ({ files, onRemoveFi
|
|||||||
{files.map((file, index) => (
|
{files.map((file, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="inline-flex items-center gap-2 px-3 py-2 bg-Neutrals-NeutralSlate100 rounded-lg hover:bg-Neutrals-NeutralSlate150 transition-colors group"
|
className="inline-flex items-center gap-2 px-3 py-2 bg-[var(--Neutrals-NeutralSlate100)] rounded-lg hover:bg-[var(--Neutrals-NeutralSlate200)] transition-colors group"
|
||||||
>
|
>
|
||||||
{getFileIcon(file)}
|
{getFileIcon(file)}
|
||||||
<span className="text-sm text-Neutrals-NeutralSlate700 max-w-[150px] truncate">{file}</span>
|
<span className="text-sm text-[var(--Neutrals-NeutralSlate700)] max-w-[150px] truncate">{file}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => onRemoveFile(index)}
|
onClick={() => onRemoveFile(index)}
|
||||||
className="w-5 h-5 text-Neutrals-NeutralSlate400 hover:text-red-500 hover:bg-red-50 rounded transition-colors flex items-center justify-center"
|
className="w-5 h-5 text-[var(--Neutrals-NeutralSlate400)] hover:text-red-500 hover:bg-red-50 rounded transition-colors flex items-center justify-center"
|
||||||
title="Remove file"
|
title="Remove file"
|
||||||
>
|
>
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
@@ -142,9 +142,8 @@ const FileUploadDropzone: React.FC<FileUploadDropzoneProps> = ({
|
|||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onClick={handleClick}
|
|
||||||
className={`
|
className={`
|
||||||
relative cursor-pointer transition-all
|
relative transition-all
|
||||||
${isDragOver ? 'opacity-80' : ''}
|
${isDragOver ? 'opacity-80' : ''}
|
||||||
${disabled ? 'cursor-not-allowed opacity-50' : ''}
|
${disabled ? 'cursor-not-allowed opacity-50' : ''}
|
||||||
`}
|
`}
|
||||||
@@ -163,8 +162,8 @@ const FileUploadDropzone: React.FC<FileUploadDropzoneProps> = ({
|
|||||||
|
|
||||||
{/* Drag overlay */}
|
{/* Drag overlay */}
|
||||||
{isDragOver && (
|
{isDragOver && (
|
||||||
<div className="absolute inset-0 bg-Brand-Orange/10 border-2 border-dashed border-Brand-Orange rounded-xl flex items-center justify-center">
|
<div className="absolute inset-0 bg-[var(--Brand-Orange)]/10 border-2 border-dashed border-[var(--Brand-Orange)] rounded-xl flex items-center justify-center">
|
||||||
<div className="text-Brand-Orange font-medium">Drop files here</div>
|
<div className="text-[var(--Brand-Orange)] font-medium">Drop files here</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -192,6 +191,8 @@ const FileUploadInput: React.FC<FileUploadInputProps> = ({
|
|||||||
onRemoveFile,
|
onRemoveFile,
|
||||||
onFilesSelected
|
onFilesSelected
|
||||||
}) => {
|
}) => {
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const handleFilesSelected = (files: File[]) => {
|
const handleFilesSelected = (files: File[]) => {
|
||||||
// For demo purposes, we'll just add the file names
|
// For demo purposes, we'll just add the file names
|
||||||
// In a real implementation, you'd upload the files and get URLs back
|
// In a real implementation, you'd upload the files and get URLs back
|
||||||
@@ -199,11 +200,39 @@ const FileUploadInput: React.FC<FileUploadInputProps> = ({
|
|||||||
onFilesSelected(files);
|
onFilesSelected(files);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = Array.from(e.target.files || []);
|
||||||
|
if (files.length > 0) {
|
||||||
|
handleFilesSelected(files);
|
||||||
|
}
|
||||||
|
// Reset input value to allow selecting the same file again
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUploadClick = () => {
|
||||||
|
if (!disabled && fileInputRef.current) {
|
||||||
|
fileInputRef.current.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{/* File Upload Preview */}
|
{/* File Upload Preview */}
|
||||||
<FileUploadPreview files={uploadedFiles} onRemoveFile={onRemoveFile} />
|
<FileUploadPreview files={uploadedFiles} onRemoveFile={onRemoveFile} />
|
||||||
|
|
||||||
|
{/* Hidden File Input */}
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".pdf,.doc,.docx,.xls,.xlsx,.txt,.jpg,.jpeg,.png,.gif"
|
||||||
|
multiple
|
||||||
|
onChange={handleFileInputChange}
|
||||||
|
className="hidden"
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Input Field with File Upload */}
|
{/* Input Field with File Upload */}
|
||||||
<FileUploadDropzone
|
<FileUploadDropzone
|
||||||
onFilesSelected={handleFilesSelected}
|
onFilesSelected={handleFilesSelected}
|
||||||
@@ -216,9 +245,10 @@ const FileUploadInput: React.FC<FileUploadInputProps> = ({
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="w-full min-h-[44px] max-h-32 px-4 py-3 pr-12 border border-Neutrals-NeutralSlate200 rounded-xl resize-none focus:outline-none focus:border-Brand-Orange focus:ring-1 focus:ring-Brand-Orange disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
className="w-full min-h-[44px] max-h-32 px-4 py-3 pr-12 border border-[var(--Neutrals-NeutralSlate200)] rounded-xl resize-none focus:outline-none focus:border-[var(--Brand-Orange)] focus:ring-1 focus:ring-[var(--Brand-Orange)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors bg-[var(--Neutrals-NeutralSlate0)] text-[var(--Neutrals-NeutralSlate950)]"
|
||||||
rows={1}
|
rows={1}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -226,7 +256,11 @@ const FileUploadInput: React.FC<FileUploadInputProps> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="absolute right-3 top-3 w-6 h-6 text-Neutrals-NeutralSlate400 hover:text-Brand-Orange disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleUploadClick();
|
||||||
|
}}
|
||||||
|
className="absolute right-3 top-3 w-6 h-6 text-[var(--Neutrals-NeutralSlate400)] hover:text-[var(--Brand-Orange)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
title="Upload files"
|
title="Upload files"
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
@@ -24,7 +24,7 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex justify-end mb-4">
|
<div className="flex justify-end mb-4">
|
||||||
<div className="max-w-[70%] flex flex-col items-end">
|
<div className="max-w-[70%] flex flex-col items-end">
|
||||||
<div className="bg-Brand-Orange text-white px-4 py-3 rounded-2xl rounded-br-md">
|
<div className="bg-[var(--Brand-Orange)] text-white px-4 py-3 rounded-2xl rounded-br-md">
|
||||||
{message.files && message.files.length > 0 && (
|
{message.files && message.files.length > 0 && (
|
||||||
<div className="mb-2 flex flex-wrap gap-2">
|
<div className="mb-2 flex flex-wrap gap-2">
|
||||||
{message.files.map((file, index) => (
|
{message.files.map((file, index) => (
|
||||||
@@ -36,7 +36,7 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => {
|
|||||||
)}
|
)}
|
||||||
<div className="text-sm leading-relaxed">{message.text}</div>
|
<div className="text-sm leading-relaxed">{message.text}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-Neutrals-NeutralSlate400 mt-1">
|
<div className="text-xs text-[var(--Neutrals-NeutralSlate400)] mt-1">
|
||||||
{formatTime(message.timestamp)}
|
{formatTime(message.timestamp)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,17 +48,17 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => {
|
|||||||
<div className="flex justify-start mb-4">
|
<div className="flex justify-start mb-4">
|
||||||
<div className="max-w-[85%] flex items-start gap-3">
|
<div className="max-w-[85%] flex items-start gap-3">
|
||||||
{/* AI Avatar */}
|
{/* AI Avatar */}
|
||||||
<div className="w-8 h-8 bg-gradient-to-br from-Brand-Orange to-orange-600 rounded-full flex items-center justify-center flex-shrink-0 mt-1">
|
<div className="w-8 h-8 bg-gradient-to-br from-[var(--Brand-Orange)] to-orange-600 rounded-full flex items-center justify-center flex-shrink-0 mt-1">
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M8 2C8.73438 2 9.375 2.64062 9.375 3.375V4.5C9.375 5.23438 8.73438 5.875 8 5.875C7.26562 5.875 6.625 5.23438 6.625 4.5V3.375C6.625 2.64062 7.26562 2 8 2ZM8 10.125C8.73438 10.125 9.375 10.7656 9.375 11.5V12.625C9.375 13.3594 8.73438 14 8 14C7.26562 14 6.625 13.3594 6.625 12.625V11.5C6.625 10.7656 7.26562 10.125 8 10.125ZM12.625 6.625C13.3594 6.625 14 7.26562 14 8C14 8.73438 13.3594 9.375 12.625 9.375H11.5C10.7656 9.375 10.125 8.73438 10.125 8C10.125 7.26562 10.7656 6.625 11.5 6.625H12.625ZM5.875 8C5.875 8.73438 5.23438 9.375 4.5 9.375H3.375C2.64062 9.375 2 8.73438 2 8C2 7.26562 2.64062 6.625 3.375 6.625H4.5C5.23438 6.625 5.875 7.26562 5.875 8Z" fill="white" />
|
<path d="M8 2C8.73438 2 9.375 2.64062 9.375 3.375V4.5C9.375 5.23438 8.73438 5.875 8 5.875C7.26562 5.875 6.625 5.23438 6.625 4.5V3.375C6.625 2.64062 7.26562 2 8 2ZM8 10.125C8.73438 10.125 9.375 10.7656 9.375 11.5V12.625C9.375 13.3594 8.73438 14 8 14C7.26562 14 6.625 13.3594 6.625 12.625V11.5C6.625 10.7656 7.26562 10.125 8 10.125ZM12.625 6.625C13.3594 6.625 14 7.26562 14 8C14 8.73438 13.3594 9.375 12.625 9.375H11.5C10.7656 9.375 10.125 8.73438 10.125 8C10.125 7.26562 10.7656 6.625 11.5 6.625H12.625ZM5.875 8C5.875 8.73438 5.23438 9.375 4.5 9.375H3.375C2.64062 9.375 2 8.73438 2 8C2 7.26562 2.64062 6.625 3.375 6.625H4.5C5.23438 6.625 5.875 7.26562 5.875 8Z" fill="white" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="bg-Neutrals-NeutralSlate100 text-Neutrals-NeutralSlate950 px-4 py-3 rounded-2xl rounded-bl-md">
|
<div className="bg-[var(--Neutrals-NeutralSlate100)] text-[var(--Neutrals-NeutralSlate950)] px-4 py-3 rounded-2xl rounded-bl-md">
|
||||||
<div className="text-sm leading-relaxed whitespace-pre-wrap">{message.text}</div>
|
<div className="text-sm leading-relaxed whitespace-pre-wrap">{message.text}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-Neutrals-NeutralSlate400 mt-1">
|
<div className="text-xs text-[var(--Neutrals-NeutralSlate400)] mt-1">
|
||||||
AI • {formatTime(message.timestamp)}
|
AI • {formatTime(message.timestamp)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,17 +75,17 @@ const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({ className = '' }) =
|
|||||||
<div className={`flex justify-start mb-4 ${className}`}>
|
<div className={`flex justify-start mb-4 ${className}`}>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
{/* AI Avatar */}
|
{/* AI Avatar */}
|
||||||
<div className="w-8 h-8 bg-gradient-to-br from-Brand-Orange to-orange-600 rounded-full flex items-center justify-center flex-shrink-0 mt-1">
|
<div className="w-8 h-8 bg-gradient-to-br from-[var(--Brand-Orange)] to-orange-600 rounded-full flex items-center justify-center flex-shrink-0 mt-1">
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M8 2C8.73438 2 9.375 2.64062 9.375 3.375V4.5C9.375 5.23438 8.73438 5.875 8 5.875C7.26562 5.875 6.625 5.23438 6.625 4.5V3.375C6.625 2.64062 7.26562 2 8 2ZM8 10.125C8.73438 10.125 9.375 10.7656 9.375 11.5V12.625C9.375 13.3594 8.73438 14 8 14C7.26562 14 6.625 13.3594 6.625 12.625V11.5C6.625 10.7656 7.26562 10.125 8 10.125ZM12.625 6.625C13.3594 6.625 14 7.26562 14 8C14 8.73438 13.3594 9.375 12.625 9.375H11.5C10.7656 9.375 10.125 8.73438 10.125 8C10.125 7.26562 10.7656 6.625 11.5 6.625H12.625ZM5.875 8C5.875 8.73438 5.23438 9.375 4.5 9.375H3.375C2.64062 9.375 2 8.73438 2 8C2 7.26562 2.64062 6.625 3.375 6.625H4.5C5.23438 6.625 5.875 7.26562 5.875 8Z" fill="white" />
|
<path d="M8 2C8.73438 2 9.375 2.64062 9.375 3.375V4.5C9.375 5.23438 8.73438 5.875 8 5.875C7.26562 5.875 6.625 5.23438 6.625 4.5V3.375C6.625 2.64062 7.26562 2 8 2ZM8 10.125C8.73438 10.125 9.375 10.7656 9.375 11.5V12.625C9.375 13.3594 8.73438 14 8 14C7.26562 14 6.625 13.3594 6.625 12.625V11.5C6.625 10.7656 7.26562 10.125 8 10.125ZM12.625 6.625C13.3594 6.625 14 7.26562 14 8C14 8.73438 13.3594 9.375 12.625 9.375H11.5C10.7656 9.375 10.125 8.73438 10.125 8C10.125 7.26562 10.7656 6.625 11.5 6.625H12.625ZM5.875 8C5.875 8.73438 5.23438 9.375 4.5 9.375H3.375C2.64062 9.375 2 8.73438 2 8C2 7.26562 2.64062 6.625 3.375 6.625H4.5C5.23438 6.625 5.875 7.26562 5.875 8Z" fill="white" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-Neutrals-NeutralSlate100 px-4 py-3 rounded-2xl rounded-bl-md">
|
<div className="bg-[var(--Neutrals-NeutralSlate100)] px-4 py-3 rounded-2xl rounded-bl-md">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-2 h-2 bg-Neutrals-NeutralSlate400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
<div className="w-2 h-2 bg-[var(--Neutrals-NeutralSlate400)] rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
||||||
<div className="w-2 h-2 bg-Neutrals-NeutralSlate400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
<div className="w-2 h-2 bg-[var(--Neutrals-NeutralSlate400)] rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
||||||
<div className="w-2 h-2 bg-Neutrals-NeutralSlate400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
<div className="w-2 h-2 bg-[var(--Neutrals-NeutralSlate400)] rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,7 +96,7 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-64 max-w-64 min-w-64 px-3 pt-4 pb-3 bg-[--Neutrals-NeutralSlate0] border-r border-[--Neutrals-NeutralSlate200] inline-flex flex-col justify-between items-center overflow-hidden">
|
<div className="h-full w-64 max-w-64 min-w-64 px-3 pt-4 pb-3 bg-[--Neutrals-NeutralSlate0] border-r border-[--Neutrals-NeutralSlate200] inline-flex flex-col justify-between items-center overflow-hidden">
|
||||||
{/* Header Section */}
|
{/* Header Section */}
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-5">
|
<div className="self-stretch flex flex-col justify-start items-start gap-5">
|
||||||
{/* Company Selector */}
|
{/* Company Selector */}
|
||||||
@@ -202,17 +202,20 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
|
|||||||
{/* Build Report Card */}
|
{/* Build Report Card */}
|
||||||
<div className="self-stretch bg-[--Neutrals-NeutralSlate0] rounded-[20px] shadow-[0px_1px_4px_0px_rgba(14,18,27,0.04)] outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] flex flex-col justify-start items-start overflow-hidden">
|
<div className="self-stretch bg-[--Neutrals-NeutralSlate0] rounded-[20px] shadow-[0px_1px_4px_0px_rgba(14,18,27,0.04)] outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] flex flex-col justify-start items-start overflow-hidden">
|
||||||
<div className="self-stretch h-24 relative">
|
<div className="self-stretch h-24 relative">
|
||||||
|
<div className="w-60 p-3 origin-top-left rotate-[-28.34deg] bg-[--Neutrals-NeutralSlate0] rounded-xl shadow-[0px_10px_20px_4px_rgba(14,18,27,0.08)] outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate100]
|
||||||
|
inline-flex flex-col justify-start items-start gap-3 overflow-hidden">
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
||||||
<div className="self-stretch h-2 bg-Neutrals-NeutralSlate100 rounded-sm" />
|
<div className="self-stretch h-2 bg-[--Neutrals-NeutralSlate100] rounded-sm" />
|
||||||
<div className="w-20 h-2 bg-Neutrals-NeutralSlate100 rounded-sm" />
|
<div className="w-20 h-2 bg-[--Neutrals-NeutralSlate100] rounded-sm" />
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-1.5">
|
<div className="self-stretch flex flex-col justify-start items-start gap-1.5">
|
||||||
<div className="w-20 h-2 bg-Neutrals-NeutralSlate100 rounded-sm" />
|
<div className="w-20 h-2 bg-[--Neutrals-NeutralSlate100] rounded-sm" />
|
||||||
<div className="self-stretch h-5 bg-Neutrals-NeutralSlate100 rounded-sm" />
|
<div className="self-stretch h-5 bg-[--Neutrals-NeutralSlate100] rounded-sm" />
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch flex flex-col justify-start items-start gap-1.5">
|
<div className="self-stretch flex flex-col justify-start items-start gap-1.5">
|
||||||
<div className="w-20 h-2 bg-Neutrals-NeutralSlate100 rounded-sm" />
|
<div className="w-20 h-2 bg-[--Neutrals-NeutralSlate100] rounded-sm" />
|
||||||
<div className="self-stretch h-5 bg-Neutrals-NeutralSlate100 rounded-sm" />
|
<div className="self-stretch h-5 bg-[--Neutrals-NeutralSlate100] rounded-sm" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-stretch p-3 flex flex-col justify-start items-start gap-1">
|
<div className="self-stretch p-3 flex flex-col justify-start items-start gap-1">
|
||||||
@@ -221,7 +224,7 @@ export default function Sidebar({ companyName = "Zitlac Media", collapsed = fals
|
|||||||
</div>
|
</div>
|
||||||
<div className="self-stretch px-3 pb-3 flex flex-col justify-start items-start gap-8">
|
<div className="self-stretch px-3 pb-3 flex flex-col justify-start items-start gap-8">
|
||||||
<div className="self-stretch inline-flex justify-start items-start gap-2">
|
<div className="self-stretch inline-flex justify-start items-start gap-2">
|
||||||
<div className="flex-1 px-3 py-1.5 bg-Button-Secondary rounded-[999px] flex justify-center items-center gap-0.5 overflow-hidden">
|
<div className="flex-1 px-3 py-1.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-0.5 overflow-hidden">
|
||||||
<div className="px-1 flex justify-center items-center">
|
<div className="px-1 flex justify-center items-center">
|
||||||
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Invite</div>
|
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Invite</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Employee, Report, Submission, FaqItem, CompanyReport } from './types';
|
import { Employee, Report, Submission, FaqItem, CompanyReport } from './src/types';
|
||||||
|
|
||||||
// URL Configuration - reads from environment variables with fallbacks
|
// URL Configuration - reads from environment variables with fallbacks
|
||||||
export const SITE_URL = import.meta.env.VITE_SITE_URL || 'http://localhost:5173';
|
export const SITE_URL = import.meta.env.VITE_SITE_URL || 'http://localhost:5173';
|
||||||
@@ -6,6 +6,8 @@ import { Employee, Report, Submission, CompanyReport } from '../types';
|
|||||||
import { SAMPLE_COMPANY_REPORT, API_URL } from '../constants';
|
import { SAMPLE_COMPANY_REPORT, API_URL } from '../constants';
|
||||||
import { demoStorage } from '../services/demoStorage';
|
import { demoStorage } from '../services/demoStorage';
|
||||||
import { apiPost, apiPut } from '../services/api';
|
import { apiPost, apiPut } from '../services/api';
|
||||||
|
import { User } from 'firebase/auth';
|
||||||
|
import { EmployeeSubmissionAnswers } from '../employeeQuestions';
|
||||||
|
|
||||||
interface OrgData {
|
interface OrgData {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
@@ -37,6 +39,7 @@ interface OrgData {
|
|||||||
|
|
||||||
interface OrgContextType {
|
interface OrgContextType {
|
||||||
org: OrgData | null;
|
org: OrgData | null;
|
||||||
|
user?: User;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
employees: Employee[];
|
employees: Employee[];
|
||||||
submissions: Record<string, Submission>;
|
submissions: Record<string, Submission>;
|
||||||
@@ -58,7 +61,7 @@ interface OrgContextType {
|
|||||||
generateCompanyWiki: (orgOverride?: OrgData) => Promise<CompanyReport>;
|
generateCompanyWiki: (orgOverride?: OrgData) => Promise<CompanyReport>;
|
||||||
seedInitialData: () => Promise<void>;
|
seedInitialData: () => Promise<void>;
|
||||||
isOwner: (employeeId?: string) => boolean;
|
isOwner: (employeeId?: string) => boolean;
|
||||||
submitEmployeeAnswers: (employeeId: string, answers: Record<string, string>) => Promise<boolean>;
|
submitEmployeeAnswers: (employeeId: string, answers: Record<string, string>) => Promise<any>;
|
||||||
generateEmployeeReport: (employee: Employee) => Promise<Report | null>;
|
generateEmployeeReport: (employee: Employee) => Promise<Report | null>;
|
||||||
getEmployeeReport: (employeeId: string) => Promise<{ success: boolean; report?: Report; error?: string }>;
|
getEmployeeReport: (employeeId: string) => Promise<{ success: boolean; report?: Report; error?: string }>;
|
||||||
getEmployeeReports: () => Promise<{ success: boolean; reports?: Report[]; error?: string }>;
|
getEmployeeReports: () => Promise<{ success: boolean; reports?: Report[]; error?: string }>;
|
||||||
677
src/pages/ChatNew.tsx
Normal file
677
src/pages/ChatNew.tsx
Normal file
@@ -0,0 +1,677 @@
|
|||||||
|
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<HTMLTextAreaElement>(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [state, setState] = useState<ChatState>({
|
||||||
|
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<HTMLTextAreaElement>) => {
|
||||||
|
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<HTMLInputElement>) => {
|
||||||
|
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<string>((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 (
|
||||||
|
<div className="absolute bottom-full left-[285px] mb-2 w-64 p-2 bg-[--Neutrals-NeutralSlate0] rounded-2xl shadow-[0px_1px_4px_4px_rgba(14,18,27,0.08)] border border-[--Neutrals-NeutralSlate200] max-h-64 overflow-y-auto z-50">
|
||||||
|
{state.mentionQuery && (
|
||||||
|
<div className="px-3 py-2 text-xs text-[--Neutrals-NeutralSlate500] border-b border-[--Neutrals-NeutralSlate100]">
|
||||||
|
{filteredEmployees.length} employee{filteredEmployees.length !== 1 ? 's' : ''} found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{filteredEmployees.map((employee, index) => (
|
||||||
|
<div
|
||||||
|
key={employee.id}
|
||||||
|
onClick={() => 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]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="w-8 h-8 bg-[--Brand-Orange] rounded-full flex items-center justify-center text-white text-sm font-medium">
|
||||||
|
{employee.initials || employee.name.split(' ').map(n => n[0]).join('').toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className={`font-medium text-sm text-[--Neutrals-NeutralSlate950] truncate ${index === state.selectedEmployeeIndex ? 'font-medium' : 'font-normal'
|
||||||
|
}`}>
|
||||||
|
{employee.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-[--Neutrals-NeutralSlate500] truncate">
|
||||||
|
{employee.role || employee.email}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderUploadedFiles = () => {
|
||||||
|
if (state.uploadedFiles.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="inline-flex justify-start items-center gap-3 mb-4">
|
||||||
|
{state.uploadedFiles.map((file, index) => (
|
||||||
|
<div key={index} className="w-40 max-w-40 p-2 bg-[--Neutrals-NeutralSlate0] rounded-full outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] inline-flex flex-col justify-start items-start gap-2.5 overflow-hidden">
|
||||||
|
<div className="self-stretch pr-2 inline-flex justify-between items-center">
|
||||||
|
<div className="flex-1 flex justify-start items-center gap-1.5">
|
||||||
|
<div className="w-6 h-6 relative bg-[--Neutrals-NeutralSlate600] rounded-full overflow-hidden">
|
||||||
|
<div className="left-[6px] top-[6px] absolute">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7 1.13477V3.20004C7 3.48006 7 3.62007 7.0545 3.72703C7.10243 3.82111 7.17892 3.8976 7.273 3.94554C7.37996 4.00004 7.51997 4.00004 7.8 4.00004H9.86527M8 6.5H4M8 8.5H4M5 4.5H4M7 1H4.4C3.55992 1 3.13988 1 2.81901 1.16349C2.53677 1.3073 2.3073 1.53677 2.16349 1.81901C2 2.13988 2 2.55992 2 3.4V8.6C2 9.44008 2 9.86012 2.16349 10.181C2.3073 10.4632 2.53677 10.6927 2.81901 10.8365C3.13988 11 3.55992 11 4.4 11H7.6C8.44008 11 8.86012 11 9.18099 10.8365C9.46323 10.6927 9.6927 10.4632 9.83651 10.181C10 9.86012 10 9.44008 10 8.6V4L7 1Z" stroke="[--Text-White-00, #FDFDFD]" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 justify-start text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">{file.name}</div>
|
||||||
|
</div>
|
||||||
|
<div onClick={() => removeFile(index)} className="cursor-pointer">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 4L4 12M4 4L12 12" stroke="[--Icon-Gray-400, #A4A7AE]" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderChatInterface = () => {
|
||||||
|
if (state.messages.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="w-[736px] flex-1 max-w-[736px] pt-48 flex flex-col justify-between items-center">
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-center gap-6">
|
||||||
|
<div className="justify-start text-[--Neutrals-NeutralSlate800] text-2xl font-medium font-['Neue_Montreal'] leading-normal">What would you like to understand?</div>
|
||||||
|
<div className="p-1 bg-[--Neutrals-NeutralSlate100] rounded-xl inline-flex justify-start items-center gap-1">
|
||||||
|
{categories.map((category) => (
|
||||||
|
<div
|
||||||
|
key={category}
|
||||||
|
onClick={() => 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' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="px-0.5 flex justify-center items-center">
|
||||||
|
<div className={`justify-start text-xs font-medium font-['Inter'] leading-none ${selectedCategory === category ? 'text-[--Neutrals-NeutralSlate900]' : 'text-[--Neutrals-NeutralSlate600]'
|
||||||
|
}`}>{category}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||||
|
<div className="self-stretch inline-flex justify-start items-center gap-3">
|
||||||
|
{questionStarters.map((question, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
onClick={() => 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]"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clipPath="url(#clip0_818_19557)">
|
||||||
|
<path d="M7.57496 7.5013C7.77088 6.94436 8.15759 6.47472 8.66659 6.17558C9.17559 5.87643 9.77404 5.76708 10.3559 5.8669C10.9378 5.96671 11.4656 6.26924 11.8459 6.72091C12.2261 7.17258 12.4342 7.74424 12.4333 8.33464C12.4333 10.0013 9.93329 10.8346 9.93329 10.8346M9.99996 14.168H10.0083M18.3333 10.0013C18.3333 14.6037 14.6023 18.3346 9.99996 18.3346C5.39759 18.3346 1.66663 14.6037 1.66663 10.0013C1.66663 5.39893 5.39759 1.66797 9.99996 1.66797C14.6023 1.66797 18.3333 5.39893 18.3333 10.0013Z" stroke="[--Text-Gray-500, #717680]" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_818_19557">
|
||||||
|
<rect width="20" height="20" fill="white" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate800] text-base font-normal font-['Inter'] leading-normal">{question}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Enhanced instructions for @ mentions */}
|
||||||
|
<div className="text-center text-[--Neutrals-NeutralSlate500] mt-8">
|
||||||
|
<div className="text-sm mb-2">Ask about your team, company data, or get insights.</div>
|
||||||
|
<div className="text-sm">Use <span className="bg-[--Neutrals-NeutralSlate100] px-2 py-1 rounded text-[--Neutrals-NeutralSlate800] font-mono">@</span> to mention team members.</div>
|
||||||
|
|
||||||
|
{/* Sample questions */}
|
||||||
|
<div className="mt-6 space-y-2">
|
||||||
|
<div className="text-sm font-medium text-[--Neutrals-NeutralSlate700] mb-3">Try asking:</div>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="bg-[--Neutrals-NeutralSlate50] p-3 rounded-lg text-left max-w-md mx-auto">
|
||||||
|
"How is the team performing overall?"
|
||||||
|
</div>
|
||||||
|
<div className="bg-[--Neutrals-NeutralSlate50] p-3 rounded-lg text-left max-w-md mx-auto">
|
||||||
|
"What are the main strengths of our organization?"
|
||||||
|
</div>
|
||||||
|
<div className="bg-[--Neutrals-NeutralSlate50] p-3 rounded-lg text-left max-w-md mx-auto">
|
||||||
|
"Tell me about @[employee name]'s recent feedback"
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{renderChatInput()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-[736px] flex-1 max-w-[736px] flex flex-col">
|
||||||
|
<div className="flex-1 overflow-y-auto py-6">
|
||||||
|
{state.messages.map((message) => (
|
||||||
|
<div key={message.id} className={`mb-4 flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||||
|
<div className={`max-w-[80%] p-4 rounded-2xl ${message.role === 'user'
|
||||||
|
? 'bg-[--Brand-Orange] text-white'
|
||||||
|
: 'bg-[--Neutrals-NeutralSlate100] text-[--Neutrals-NeutralSlate950] border border-[--Neutrals-NeutralSlate200]'
|
||||||
|
}`}>
|
||||||
|
<div className="text-sm leading-relaxed whitespace-pre-wrap">{message.content}</div>
|
||||||
|
{message.attachments && message.attachments.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 mt-3">
|
||||||
|
{message.attachments.map((file, index) => (
|
||||||
|
<div key={index} className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs ${message.role === 'user'
|
||||||
|
? 'bg-white/20 text-white/90'
|
||||||
|
: 'bg-[--Neutrals-NeutralSlate200] text-[--Neutrals-NeutralSlate700]'
|
||||||
|
}`}>
|
||||||
|
<div className="w-4 h-4">
|
||||||
|
{file.type.startsWith('image/') ? (
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10.8 14H4.62091C4.21704 14 4.0151 14 3.92159 13.9202C3.84045 13.8508 3.79739 13.7469 3.80577 13.6405C3.81542 13.5179 3.95821 13.3751 4.24379 13.0895L9.9124 7.42091C10.1764 7.15691 10.3084 7.02491 10.4606 6.97544C10.5946 6.93194 10.7388 6.93194 10.8728 6.97544C11.0249 7.02491 11.1569 7.15691 11.4209 7.42091L14 10V10.8M10.8 14C11.9201 14 12.4802 14 12.908 13.782C13.2843 13.5902 13.5902 13.2843 13.782 12.908C14 12.4802 14 11.9201 14 10.8M10.8 14H5.2C4.07989 14 3.51984 14 3.09202 13.782C2.71569 13.5902 2.40973 13.2843 2.21799 12.908C2 12.4802 2 11.9201 2 10.8V5.2C2 4.07989 2 3.51984 2.21799 3.09202C2.40973 2.71569 2.71569 2.40973 3.09202 2.21799C3.51984 2 4.07989 2 5.2 2H10.8C11.9201 2 12.4802 2 12.908 2.21799C2.71569 12.908 2.40973 2.21799 3.09202 2 3.51984 2 4.07989 2 5.2V10.8M7 5.66667C7 6.40305 6.40305 7 5.66667 7C4.93029 7 4.33333 6.40305 4.33333 5.66667C4.33333 4.93029 4.93029 4.33333 5.66667 4.33333C6.40305 4.33333 7 4.93029 7 5.66667Z" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9.33333 1.33333V2.8C9.33333 3.12001 9.33333 3.28002 9.40533 3.39467C9.46867 3.49422 9.57245 3.56756 9.672 3.63089C9.78665 3.70289 9.94666 3.70289 10.2667 3.70289H11.7333M10.6667 5.5H5.33333M10.6667 7.5H5.33333M6.66667 3.5H5.33333M9.33333 1.33333H5.86667C5.13029 1.33333 4.76209 1.33333 4.47852 1.47866C4.23137 1.60584 4.03918 1.79804 3.912 2.04518C3.76667 2.32876 3.76667 2.69695 3.76667 3.43333V12.5667C3.76667 13.303 3.76667 13.6712 3.912 13.9548C4.03918 14.2019 4.23137 14.3941 4.47852 14.5213C4.76209 14.6667 5.13029 14.6667 5.86667 14.6667H10.1333C10.8697 14.6667 11.2379 14.6667 11.5215 14.5213C11.7686 14.3941 11.9608 14.2019 12.088 13.9548C12.2333 13.6712 12.2333 13.303 12.2333 12.5667V5.33333L9.33333 1.33333Z" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="truncate max-w-[100px]">{file.name}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={`text-xs mt-2 ${message.role === 'user' ? 'text-white/70' : 'text-[--Neutrals-NeutralSlate500]'}`}>
|
||||||
|
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</div>
|
||||||
|
{message.mentions && message.mentions.length > 0 && (
|
||||||
|
<div className={`text-xs mt-1 ${message.role === 'user' ? 'text-white/60' : 'text-[--Neutrals-NeutralSlate400]'}`}>
|
||||||
|
Mentioned: {message.mentions.map(m => m.name).join(', ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{state.isLoading && (
|
||||||
|
<div className="flex justify-start mb-4">
|
||||||
|
<div className="bg-[--Neutrals-NeutralSlate100] text-[--Neutrals-NeutralSlate950] border border-[--Neutrals-NeutralSlate200] p-4 rounded-2xl">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-[--Brand-Orange]"></div>
|
||||||
|
<span className="text-sm">AI is analyzing...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
{renderChatInput()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderChatInput = () => {
|
||||||
|
return (
|
||||||
|
<div className="self-stretch pl-5 pr-3 pt-5 pb-3 relative bg-[--Neutrals-NeutralSlate50] rounded-3xl flex flex-col justify-start items-start gap-4 min-h-[80px]">
|
||||||
|
{renderUploadedFiles()}
|
||||||
|
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate500] text-base font-normal font-['Inter'] leading-normal relative pointer-events-none min-h-[24px] whitespace-pre-wrap">
|
||||||
|
<span className={`${currentInput ? 'text-[--Neutrals-NeutralSlate950]' : 'text-[--Neutrals-NeutralSlate500]'}`}>
|
||||||
|
{currentInput || "Ask anything, use @ to tag staff and ask questions."}
|
||||||
|
</span>
|
||||||
|
{/* Custom blinking cursor when focused and has text */}
|
||||||
|
{currentInput && isInputFocused && (
|
||||||
|
<span
|
||||||
|
className="inline-block w-0.5 h-6 bg-[--Neutrals-NeutralSlate800] ml-0.5"
|
||||||
|
style={{
|
||||||
|
animation: 'blink 1s infinite',
|
||||||
|
verticalAlign: 'text-top'
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
)}
|
||||||
|
{/* Custom blinking cursor when focused and no text */}
|
||||||
|
{!currentInput && isInputFocused && (
|
||||||
|
<span
|
||||||
|
className="absolute left-0 top-0 w-0.5 h-6 bg-[--Neutrals-NeutralSlate800]"
|
||||||
|
style={{
|
||||||
|
animation: 'blink 1s infinite'
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="self-stretch inline-flex justify-between items-center relative z-20">
|
||||||
|
<div className="flex justify-start items-center gap-4">
|
||||||
|
<div onClick={() => fileInputRef.current?.click()} className="cursor-pointer">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M17.6271 9.08442L10.1141 16.5974C8.40556 18.306 5.63546 18.306 3.92692 16.5974C2.21837 14.8889 2.21837 12.1188 3.92692 10.4102L11.4399 2.89724C12.579 1.75821 14.4257 1.75821 15.5647 2.89724C16.7037 4.03627 16.7037 5.883 15.5647 7.02203L8.34633 14.2404C7.77682 14.8099 6.85345 14.8099 6.28394 14.2404C5.71442 13.6709 5.71442 12.7475 6.28394 12.178L12.6184 5.84352" stroke="var(--color-gray-500)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div onClick={() => fileInputRef.current?.click()} className="cursor-pointer">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M13.5 17.5H5.77614C5.2713 17.5 5.01887 17.5 4.90199 17.4002C4.80056 17.3135 4.74674 17.1836 4.75721 17.0506C4.76927 16.8974 4.94776 16.7189 5.30474 16.3619L12.3905 9.27614C12.7205 8.94613 12.8855 8.78112 13.0758 8.7193C13.2432 8.66492 13.4235 8.66492 13.5908 8.7193C13.7811 8.78112 13.9461 8.94613 14.2761 9.27614L17.5 12.5V13.5M13.5 17.5C14.9001 17.5 15.6002 17.5 16.135 17.2275C16.6054 16.9878 16.9878 16.6054 17.2275 16.135C17.5 15.6002 17.5 14.9001 17.5 13.5M13.5 17.5H6.5C5.09987 17.5 4.3998 17.5 3.86502 17.2275C3.39462 16.9878 3.01217 16.6054 2.77248 16.135C2.5 15.6002 2.5 14.9001 2.5 13.5V6.5C2.5 5.09987 2.5 4.3998 2.77248 3.86502C3.01217 3.39462 3.39462 3.01217 3.86502 2.77248C4.3998 2.5 5.09987 2.5 6.5 2.5H13.5C14.9001 2.5 15.6002 2.5 16.135 2.77248C16.6054 3.01217 16.9878 3.39462 17.2275 3.86502C17.5 4.3998 17.5 5.09987 17.5 6.5V13.5M8.75 7.08333C8.75 8.00381 8.00381 8.75 7.08333 8.75C6.16286 8.75 5.41667 8.00381 5.41667 7.08333C5.41667 6.16286 6.16286 5.41667 7.08333 5.41667C8.00381 5.41667 8.75 6.16286 8.75 7.08333Z" stroke="var(--color-gray-500)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="cursor-pointer">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clipPath="url(#clip0_818_19694)">
|
||||||
|
<path d="M13.3334 6.66745V10.8341C13.3334 11.4972 13.5968 12.133 14.0657 12.6019C14.5345 13.0707 15.1704 13.3341 15.8334 13.3341C16.4965 13.3341 17.1324 13.0707 17.6012 12.6019C18.07 12.133 18.3334 11.4972 18.3334 10.8341V10.0008C18.3333 8.11998 17.6969 6.29452 16.5278 4.82123C15.3587 3.34794 13.7256 2.31347 11.894 1.88603C10.0624 1.45859 8.14003 1.66332 6.43955 2.46692C4.73906 3.27053 3.36042 4.62575 2.5278 6.31222C1.69519 7.99869 1.45756 9.91723 1.85356 11.7559C2.24956 13.5945 3.2559 15.2451 4.70895 16.4393C6.16199 17.6335 7.97628 18.3011 9.85681 18.3334C11.7373 18.3657 13.5735 17.761 15.0668 16.6175M13.3334 10.0008C13.3334 11.8417 11.841 13.3341 10.0001 13.3341C8.15914 13.3341 6.66676 11.8417 6.66676 10.0008C6.66676 8.15984 8.15914 6.66745 10.0001 6.66745C11.841 6.66745 13.3334 8.15984 13.3334 10.0008Z" stroke="var(--Neutrals-NeutralSlate500)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_818_19694">
|
||||||
|
<rect width="20" height="20" fill="white" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={handleSendMessage}
|
||||||
|
className={`p-2.5 rounded-[999px] flex justify-start items-center gap-2.5 overflow-hidden cursor-pointer ${currentInput.trim() || state.uploadedFiles.length > 0
|
||||||
|
? 'bg-[--Neutrals-NeutralSlate700]'
|
||||||
|
: 'bg-[--Neutrals-NeutralSlate400]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8 13.3346V2.66797M8 2.66797L4 6.66797M8 2.66797L12 6.66797" stroke="var(--Neutrals-NeutralSlate100)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Enhanced help text for keyboard navigation */}
|
||||||
|
<div className="absolute bottom-2 right-16 text-xs text-[--Neutrals-NeutralSlate400]">
|
||||||
|
{state.showEmployeeMenu ? '↑↓ Navigate • Enter/Tab Select • Esc Cancel' : 'Enter to send • Shift+Enter new line'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{renderEmployeeMenu()}
|
||||||
|
<textarea
|
||||||
|
ref={inputRef}
|
||||||
|
value={currentInput}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onFocus={() => setIsInputFocused(true)}
|
||||||
|
onBlur={() => setIsInputFocused(false)}
|
||||||
|
className="absolute inset-0 w-full resize-none outline-none bg-transparent text-transparent caret-transparent text-base font-normal font-['Inter'] leading-normal p-5 z-10 overflow-hidden"
|
||||||
|
placeholder=""
|
||||||
|
style={{
|
||||||
|
paddingTop: '20px', // Align with the display text
|
||||||
|
paddingLeft: '20px',
|
||||||
|
paddingRight: '12px',
|
||||||
|
paddingBottom: '60px', // Leave space for buttons
|
||||||
|
lineHeight: 'normal',
|
||||||
|
minHeight: '50px',
|
||||||
|
maxHeight: '150px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
className="hidden"
|
||||||
|
accept=".pdf,.doc,.docx,.txt,.png,.jpg,.jpeg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full inline-flex justify-start items-start overflow-hidden">
|
||||||
|
<div className="flex-1 self-stretch rounded-3xl shadow-[0px_0px_15px_0px_rgba(0,0,0,0.08)] flex justify-between items-start overflow-hidden">
|
||||||
|
<Sidebar companyName="Zitlac Media" />
|
||||||
|
<div className="flex-1 self-stretch py-6 bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-center items-center gap-2.5">
|
||||||
|
{renderChatInterface()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatNew;
|
||||||
@@ -111,7 +111,7 @@ const CompanyReportCard: React.FC<{ report: CompanyReport }> = ({ report }) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-[--text-secondary] mb-2">{need.department}</p>
|
<p className="text-sm text-[--text-secondary] mb-2">{need.department}</p>
|
||||||
<p className="text-sm text-[--text-secondary]">{need.reason}</p>
|
<p className="text-sm text-[--text-secondary]">{need.reasoning}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -129,7 +129,7 @@ const CompanyReportCard: React.FC<{ report: CompanyReport }> = ({ report }) => {
|
|||||||
<div className="p-4 bg-[--background-tertiary] rounded-lg">
|
<div className="p-4 bg-[--background-tertiary] rounded-lg">
|
||||||
<h4 className="font-medium text-[--text-primary] mb-2">Next Quarter Goals</h4>
|
<h4 className="font-medium text-[--text-primary] mb-2">Next Quarter Goals</h4>
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1">
|
||||||
{report.forwardOperatingPlan.nextQuarterGoals.map((goal, idx) => (
|
{report.forwardOperatingPlan.quarterlyGoals.map((goal, idx) => (
|
||||||
<li key={idx} className="text-sm text-[--text-secondary] flex items-start">
|
<li key={idx} className="text-sm text-[--text-secondary] flex items-start">
|
||||||
<span className="w-1.5 h-1.5 bg-blue-500 rounded-full mt-2 mr-2 flex-shrink-0"></span>
|
<span className="w-1.5 h-1.5 bg-blue-500 rounded-full mt-2 mr-2 flex-shrink-0"></span>
|
||||||
{goal}
|
{goal}
|
||||||
@@ -138,9 +138,9 @@ const CompanyReportCard: React.FC<{ report: CompanyReport }> = ({ report }) => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 bg-[--background-tertiary] rounded-lg">
|
<div className="p-4 bg-[--background-tertiary] rounded-lg">
|
||||||
<h4 className="font-medium text-[--text-primary] mb-2">Key Initiatives</h4>
|
<h4 className="font-medium text-[--text-primary] mb-2">Risk Mitigation</h4>
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1">
|
||||||
{report.forwardOperatingPlan.keyInitiatives.map((initiative, idx) => (
|
{report.forwardOperatingPlan.riskMitigation.map((initiative, idx) => (
|
||||||
<li key={idx} className="text-sm text-[--text-secondary] flex items-start">
|
<li key={idx} className="text-sm text-[--text-secondary] flex items-start">
|
||||||
<span className="w-1.5 h-1.5 bg-green-500 rounded-full mt-2 mr-2 flex-shrink-0"></span>
|
<span className="w-1.5 h-1.5 bg-green-500 rounded-full mt-2 mr-2 flex-shrink-0"></span>
|
||||||
{initiative}
|
{initiative}
|
||||||
@@ -153,8 +153,8 @@ const EmployeeQuestionnaireSteps: React.FC = () => {
|
|||||||
|
|
||||||
const result = await submitEmployeeAnswers(currentEmployee.id, answers);
|
const result = await submitEmployeeAnswers(currentEmployee.id, answers);
|
||||||
|
|
||||||
if (result.success) {
|
if (result) {
|
||||||
const message = result.reportGenerated
|
const message = result
|
||||||
? 'Questionnaire submitted successfully! Your AI-powered performance report has been generated.'
|
? 'Questionnaire submitted successfully! Your AI-powered performance report has been generated.'
|
||||||
: 'Questionnaire submitted successfully! Your report will be available shortly.';
|
: 'Questionnaire submitted successfully! Your report will be available shortly.';
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ const EmployeeQuestionnaireSteps: React.FC = () => {
|
|||||||
state: {
|
state: {
|
||||||
employeeId: currentEmployee.id,
|
employeeId: currentEmployee.id,
|
||||||
employeeName: currentEmployee.name,
|
employeeName: currentEmployee.name,
|
||||||
reportGenerated: result.reportGenerated,
|
reportGenerated: result,
|
||||||
message: message
|
message: message
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -250,7 +250,7 @@ const EmployeeQuestionnaireSteps: React.FC = () => {
|
|||||||
<div className="w-full max-w-2xl">
|
<div className="w-full max-w-2xl">
|
||||||
<EnhancedFigmaQuestion
|
<EnhancedFigmaQuestion
|
||||||
questionNumber={`Q${currentStep + 1}`}
|
questionNumber={`Q${currentStep + 1}`}
|
||||||
question={currentQuestion}
|
question={currentQuestion.prompt}
|
||||||
answer={answers[currentQuestion.id] || ''}
|
answer={answers[currentQuestion.id] || ''}
|
||||||
onAnswerChange={handleAnswerChange}
|
onAnswerChange={handleAnswerChange}
|
||||||
onBack={currentStep > 0 ? handleBack : undefined}
|
onBack={currentStep > 0 ? handleBack : undefined}
|
||||||
@@ -4,7 +4,7 @@ import { useOrg } from '../contexts/OrgContext';
|
|||||||
import { EnhancedFigmaQuestion, FigmaQuestionCard, EnhancedFigmaInput } from '../components/figma/EnhancedFigmaQuestion';
|
import { EnhancedFigmaQuestion, FigmaQuestionCard, EnhancedFigmaInput } from '../components/figma/EnhancedFigmaQuestion';
|
||||||
import { FigmaInput, FigmaSelect } from '../components/figma/FigmaInput';
|
import { FigmaInput, FigmaSelect } from '../components/figma/FigmaInput';
|
||||||
import { FigmaMultipleChoice } from '../components/figma/FigmaMultipleChoice';
|
import { FigmaMultipleChoice } from '../components/figma/FigmaMultipleChoice';
|
||||||
import { StoredImage } from '../services/imageStorageService';
|
import { StoredImage } from '../../services/imageStorageService';
|
||||||
|
|
||||||
interface OnboardingData {
|
interface OnboardingData {
|
||||||
// Step 0: Company Details
|
// Step 0: Company Details
|
||||||
@@ -75,8 +75,7 @@ const ReportDetail: React.FC<ReportDetailProps> = ({ report, type, employeeName,
|
|||||||
<h3 className="font-semibold text-[--text-primary]">{change.employeeName}</h3>
|
<h3 className="font-semibold text-[--text-primary]">{change.employeeName}</h3>
|
||||||
<p className="text-sm text-[--text-secondary]">{change.role} • {change.department}</p>
|
<p className="text-sm text-[--text-secondary]">{change.role} • {change.department}</p>
|
||||||
</div>
|
</div>
|
||||||
<span className={`px-3 py-1 text-xs rounded-full font-medium ${
|
<span className={`px-3 py-1 text-xs rounded-full font-medium ${change.changeType === 'departure' ? 'bg-red-100 text-red-800' :
|
||||||
change.changeType === 'departure' ? 'bg-red-100 text-red-800' :
|
|
||||||
change.changeType === 'promotion' ? 'bg-green-100 text-green-800' :
|
change.changeType === 'promotion' ? 'bg-green-100 text-green-800' :
|
||||||
'bg-blue-100 text-blue-800'
|
'bg-blue-100 text-blue-800'
|
||||||
}`}>
|
}`}>
|
||||||
@@ -102,8 +101,7 @@ const ReportDetail: React.FC<ReportDetailProps> = ({ report, type, employeeName,
|
|||||||
<div key={idx} className="p-4 bg-[--background-tertiary] rounded-lg">
|
<div key={idx} className="p-4 bg-[--background-tertiary] rounded-lg">
|
||||||
<div className="flex justify-between items-start mb-2">
|
<div className="flex justify-between items-start mb-2">
|
||||||
<h3 className="font-semibold text-[--text-primary]">{need.role}</h3>
|
<h3 className="font-semibold text-[--text-primary]">{need.role}</h3>
|
||||||
<span className={`px-2 py-1 text-xs rounded-full font-medium ${
|
<span className={`px-2 py-1 text-xs rounded-full font-medium ${need.urgency === 'high' ? 'bg-red-100 text-red-800' :
|
||||||
need.urgency === 'high' ? 'bg-red-100 text-red-800' :
|
|
||||||
need.urgency === 'medium' ? 'bg-yellow-100 text-yellow-800' :
|
need.urgency === 'medium' ? 'bg-yellow-100 text-yellow-800' :
|
||||||
'bg-green-100 text-green-800'
|
'bg-green-100 text-green-800'
|
||||||
}`}>
|
}`}>
|
||||||
@@ -111,7 +109,7 @@ const ReportDetail: React.FC<ReportDetailProps> = ({ report, type, employeeName,
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-[--text-secondary] mb-2">{need.department}</p>
|
<p className="text-sm text-[--text-secondary] mb-2">{need.department}</p>
|
||||||
<p className="text-sm text-[--text-secondary]">{need.reason}</p>
|
<p className="text-sm text-[--text-secondary]">{need.reasoning}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -129,7 +127,7 @@ const ReportDetail: React.FC<ReportDetailProps> = ({ report, type, employeeName,
|
|||||||
<div className="p-4 bg-[--background-tertiary] rounded-lg">
|
<div className="p-4 bg-[--background-tertiary] rounded-lg">
|
||||||
<h3 className="font-semibold text-[--text-primary] mb-3">Next Quarter Goals</h3>
|
<h3 className="font-semibold text-[--text-primary] mb-3">Next Quarter Goals</h3>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{companyReport.forwardOperatingPlan.nextQuarterGoals?.map((goal, idx) => (
|
{companyReport.forwardOperatingPlan.quarterlyGoals?.map((goal, idx) => (
|
||||||
<li key={idx} className="text-sm text-[--text-secondary] flex items-start">
|
<li key={idx} className="text-sm text-[--text-secondary] flex items-start">
|
||||||
<span className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></span>
|
<span className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></span>
|
||||||
{goal}
|
{goal}
|
||||||
@@ -140,7 +138,7 @@ const ReportDetail: React.FC<ReportDetailProps> = ({ report, type, employeeName,
|
|||||||
<div className="p-4 bg-[--background-tertiary] rounded-lg">
|
<div className="p-4 bg-[--background-tertiary] rounded-lg">
|
||||||
<h3 className="font-semibold text-[--text-primary] mb-3">Key Initiatives</h3>
|
<h3 className="font-semibold text-[--text-primary] mb-3">Key Initiatives</h3>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{companyReport.forwardOperatingPlan.keyInitiatives?.map((initiative, idx) => (
|
{companyReport.forwardOperatingPlan.riskMitigation?.map((initiative, idx) => (
|
||||||
<li key={idx} className="text-sm text-[--text-secondary] flex items-start">
|
<li key={idx} className="text-sm text-[--text-secondary] flex items-start">
|
||||||
<span className="w-2 h-2 bg-green-500 rounded-full mt-2 mr-3 flex-shrink-0"></span>
|
<span className="w-2 h-2 bg-green-500 rounded-full mt-2 mr-3 flex-shrink-0"></span>
|
||||||
{initiative}
|
{initiative}
|
||||||
@@ -165,7 +163,7 @@ const ReportDetail: React.FC<ReportDetailProps> = ({ report, type, employeeName,
|
|||||||
<div className="flex items-start space-x-3">
|
<div className="flex items-start space-x-3">
|
||||||
<span className="text-3xl">{strength.icon || '💪'}</span>
|
<span className="text-3xl">{strength.icon || '💪'}</span>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-[--text-primary] mb-1">{strength.area || strength}</h3>
|
<h3 className="font-semibold text-[--text-primary] mb-1">{strength.area || strength.description}</h3>
|
||||||
<p className="text-sm text-[--text-secondary]">{strength.description}</p>
|
<p className="text-sm text-[--text-secondary]">{strength.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -221,7 +219,7 @@ const ReportDetail: React.FC<ReportDetailProps> = ({ report, type, employeeName,
|
|||||||
<div className="sticky top-0 bg-[--background-primary] border-b border-[--border-color] p-6 flex justify-between items-center">
|
<div className="sticky top-0 bg-[--background-primary] border-b border-[--border-color] p-6 flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-[--text-primary]">{employeeName}'s Performance Report</h1>
|
<h1 className="text-2xl font-bold text-[--text-primary]">{employeeName}'s Performance Report</h1>
|
||||||
<p className="text-[--text-secondary]">{employeeReport.employee?.role} • {employeeReport.employee?.department}</p>
|
<p className="text-[--text-secondary]">{employeeReport.role} • {employeeReport.department}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<Button size="sm">Download as PDF</Button>
|
<Button size="sm">Download as PDF</Button>
|
||||||
@@ -351,7 +349,7 @@ const ReportDetail: React.FC<ReportDetailProps> = ({ report, type, employeeName,
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Opportunities */}
|
{/* Opportunities */}
|
||||||
{employeeReport.opportunities && employeeReport.opportunities.length > 0 && (
|
{employeeReport.opportunities && employeeReport.opportunities?.length > 0 && (
|
||||||
<Card>
|
<Card>
|
||||||
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
|
<h2 className="text-xl font-semibold text-[--text-primary] mb-4 flex items-center">
|
||||||
<span className="w-3 h-3 bg-blue-500 rounded-full mr-3"></span>
|
<span className="w-3 h-3 bg-blue-500 rounded-full mr-3"></span>
|
||||||
@@ -62,12 +62,10 @@ const SettingsNew: React.FC = () => {
|
|||||||
<div className="self-stretch inline-flex justify-start items-start gap-6">
|
<div className="self-stretch inline-flex justify-start items-start gap-6">
|
||||||
<div
|
<div
|
||||||
onClick={() => setActiveTab('general')}
|
onClick={() => setActiveTab('general')}
|
||||||
className={`w-32 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${
|
className={`w-32 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${activeTab === 'general' ? '' : 'opacity-60'
|
||||||
activeTab === 'general' ? '' : 'opacity-60'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className={`self-stretch text-center justify-center text-base font-['Inter'] leading-normal ${
|
<div className={`self-stretch text-center justify-center text-base font-['Inter'] leading-normal ${activeTab === 'general'
|
||||||
activeTab === 'general'
|
|
||||||
? 'text-Text-Gray-800 font-semibold'
|
? 'text-Text-Gray-800 font-semibold'
|
||||||
: 'text-Text-Gray-500 font-normal'
|
: 'text-Text-Gray-500 font-normal'
|
||||||
}`}>
|
}`}>
|
||||||
@@ -79,12 +77,10 @@ const SettingsNew: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={() => setActiveTab('billing')}
|
onClick={() => setActiveTab('billing')}
|
||||||
className={`inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${
|
className={`inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${activeTab === 'billing' ? '' : 'opacity-60'
|
||||||
activeTab === 'billing' ? '' : 'opacity-60'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className={`text-center justify-center text-base font-['Inter'] leading-normal ${
|
<div className={`text-center justify-center text-base font-['Inter'] leading-normal ${activeTab === 'billing'
|
||||||
activeTab === 'billing'
|
|
||||||
? 'text-Text-Gray-800 font-semibold'
|
? 'text-Text-Gray-800 font-semibold'
|
||||||
: 'text-Text-Gray-500 font-normal'
|
: 'text-Text-Gray-500 font-normal'
|
||||||
}`}>
|
}`}>
|
||||||
@@ -210,8 +206,7 @@ const SettingsNew: React.FC = () => {
|
|||||||
{/* System Preference */}
|
{/* System Preference */}
|
||||||
<div
|
<div
|
||||||
onClick={() => setSelectedTheme('system')}
|
onClick={() => setSelectedTheme('system')}
|
||||||
className={`max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${
|
className={`max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'system' ? 'opacity-100' : 'opacity-70'
|
||||||
selectedTheme === 'system' ? 'opacity-100' : 'opacity-70'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="inline-flex justify-start items-center">
|
<div className="inline-flex justify-start items-center">
|
||||||
@@ -224,13 +219,11 @@ const SettingsNew: React.FC = () => {
|
|||||||
{/* Light Mode */}
|
{/* Light Mode */}
|
||||||
<div
|
<div
|
||||||
onClick={() => setSelectedTheme('light')}
|
onClick={() => setSelectedTheme('light')}
|
||||||
className={`w-48 max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${
|
className={`w-48 max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'light' ? 'opacity-100' : 'opacity-70'
|
||||||
selectedTheme === 'light' ? 'opacity-100' : 'opacity-70'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="self-stretch h-28 relative bg-Text-White-00 rounded-lg overflow-hidden">
|
<div className="self-stretch h-28 relative bg-Text-White-00 rounded-lg overflow-hidden">
|
||||||
<div className={`w-48 h-28 left-0 top-0 absolute bg-Text-White-00 rounded-[10px] outline outline-1 outline-offset-[-1px] overflow-hidden ${
|
<div className={`w-48 h-28 left-0 top-0 absolute bg-Text-White-00 rounded-[10px] outline outline-1 outline-offset-[-1px] overflow-hidden ${selectedTheme === 'light' ? 'outline-Brand-Orange' : 'outline-Text-Gray-200'
|
||||||
selectedTheme === 'light' ? 'outline-Brand-Orange' : 'outline-Text-Gray-200'
|
|
||||||
}`}>
|
}`}>
|
||||||
<img className="w-48 h-28 left-0 top-0 absolute rounded-lg" src="https://via.placeholder.com/190x107/f8f9fa/6c757d?text=Light+Mode" />
|
<img className="w-48 h-28 left-0 top-0 absolute rounded-lg" src="https://via.placeholder.com/190x107/f8f9fa/6c757d?text=Light+Mode" />
|
||||||
</div>
|
</div>
|
||||||
@@ -241,13 +234,11 @@ const SettingsNew: React.FC = () => {
|
|||||||
{/* Dark Mode */}
|
{/* Dark Mode */}
|
||||||
<div
|
<div
|
||||||
onClick={() => setSelectedTheme('dark')}
|
onClick={() => setSelectedTheme('dark')}
|
||||||
className={`max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${
|
className={`max-w-60 inline-flex flex-col justify-start items-start gap-3 cursor-pointer ${selectedTheme === 'dark' ? 'opacity-100' : 'opacity-70'
|
||||||
selectedTheme === 'dark' ? 'opacity-100' : 'opacity-70'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="w-48 h-28 relative bg-Text-White-00 rounded-lg overflow-hidden">
|
<div className="w-48 h-28 relative bg-Text-White-00 rounded-lg overflow-hidden">
|
||||||
<div className={`w-48 h-28 left-0 top-0 absolute bg-Text-White-00 rounded-[10px] outline outline-1 outline-offset-[-1px] overflow-hidden ${
|
<div className={`w-48 h-28 left-0 top-0 absolute bg-Text-White-00 rounded-[10px] outline outline-1 outline-offset-[-1px] overflow-hidden ${selectedTheme === 'dark' ? 'outline-Brand-Orange' : 'outline-Text-Gray-200'
|
||||||
selectedTheme === 'dark' ? 'outline-Brand-Orange' : 'outline-Text-Gray-200'
|
|
||||||
}`}>
|
}`}>
|
||||||
<img className="w-48 h-28 left-0 top-0 absolute rounded-lg" src="https://via.placeholder.com/190x107/212529/ffffff?text=Dark+Mode" />
|
<img className="w-48 h-28 left-0 top-0 absolute rounded-lg" src="https://via.placeholder.com/190x107/212529/ffffff?text=Dark+Mode" />
|
||||||
</div>
|
</div>
|
||||||
@@ -50,8 +50,10 @@ export interface Report {
|
|||||||
opportunities: {
|
opportunities: {
|
||||||
roleAdjustment: string;
|
roleAdjustment: string;
|
||||||
accountabilitySupport: string;
|
accountabilitySupport: string;
|
||||||
};
|
description?: string;
|
||||||
|
}[];
|
||||||
risks: string[];
|
risks: string[];
|
||||||
|
recommendations: string[];
|
||||||
recommendation: {
|
recommendation: {
|
||||||
action: 'Keep' | 'Restructure' | 'Terminate';
|
action: 'Keep' | 'Restructure' | 'Terminate';
|
||||||
details: string[];
|
details: string[];
|
||||||
@@ -123,6 +125,7 @@ export interface CompanyReport {
|
|||||||
reasoning: string;
|
reasoning: string;
|
||||||
urgency?: 'high' | 'medium' | 'low'; // UI alias
|
urgency?: 'high' | 'medium' | 'low'; // UI alias
|
||||||
}[];
|
}[];
|
||||||
|
recommendations: string[];
|
||||||
// Operating plan (dual naming for UI compatibility)
|
// Operating plan (dual naming for UI compatibility)
|
||||||
operatingPlan: {
|
operatingPlan: {
|
||||||
nextQuarterGoals: string[];
|
nextQuarterGoals: string[];
|
||||||
157
src/utils/imageUtils.ts
Normal file
157
src/utils/imageUtils.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
/**
|
||||||
|
* Image processing utilities for resizing and encoding images
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ProcessedImage {
|
||||||
|
dataUrl: string;
|
||||||
|
blob: Blob;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
originalSize: number;
|
||||||
|
compressedSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize an image to a specific size and convert to base64
|
||||||
|
* @param file - The image file to process
|
||||||
|
* @param maxWidth - Maximum width (default: 128)
|
||||||
|
* @param maxHeight - Maximum height (default: 128)
|
||||||
|
* @param quality - JPEG quality (0-1, default: 0.8)
|
||||||
|
* @returns Promise with processed image data
|
||||||
|
*/
|
||||||
|
export const processImage = async (
|
||||||
|
file: File,
|
||||||
|
maxWidth: number = 128,
|
||||||
|
maxHeight: number = 128,
|
||||||
|
quality: number = 0.8
|
||||||
|
): Promise<ProcessedImage> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Validate file type
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
reject(new Error('File must be an image'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
// Calculate dimensions to maintain aspect ratio
|
||||||
|
let { width, height } = calculateDimensions(
|
||||||
|
img.width,
|
||||||
|
img.height,
|
||||||
|
maxWidth,
|
||||||
|
maxHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
|
||||||
|
// Draw and resize image
|
||||||
|
ctx!.imageSmoothingEnabled = true;
|
||||||
|
ctx!.imageSmoothingQuality = 'high';
|
||||||
|
ctx!.drawImage(img, 0, 0, width, height);
|
||||||
|
|
||||||
|
// Convert to blob and data URL
|
||||||
|
canvas.toBlob(
|
||||||
|
(blob) => {
|
||||||
|
if (!blob) {
|
||||||
|
reject(new Error('Failed to process image'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataUrl = canvas.toDataURL('image/jpeg', quality);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
dataUrl,
|
||||||
|
blob,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
originalSize: file.size,
|
||||||
|
compressedSize: blob.size,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
'image/jpeg',
|
||||||
|
quality
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
reject(new Error('Failed to load image'));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load the image
|
||||||
|
img.src = URL.createObjectURL(file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate dimensions to fit within max bounds while maintaining aspect ratio
|
||||||
|
*/
|
||||||
|
const calculateDimensions = (
|
||||||
|
originalWidth: number,
|
||||||
|
originalHeight: number,
|
||||||
|
maxWidth: number,
|
||||||
|
maxHeight: number
|
||||||
|
): { width: number; height: number } => {
|
||||||
|
const aspectRatio = originalWidth / originalHeight;
|
||||||
|
|
||||||
|
let width = maxWidth;
|
||||||
|
let height = maxHeight;
|
||||||
|
|
||||||
|
if (originalWidth > originalHeight) {
|
||||||
|
// Landscape
|
||||||
|
height = width / aspectRatio;
|
||||||
|
if (height > maxHeight) {
|
||||||
|
height = maxHeight;
|
||||||
|
width = height * aspectRatio;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Portrait or square
|
||||||
|
width = height * aspectRatio;
|
||||||
|
if (width > maxWidth) {
|
||||||
|
width = maxWidth;
|
||||||
|
height = width / aspectRatio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: Math.round(width),
|
||||||
|
height: Math.round(height),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate image file
|
||||||
|
*/
|
||||||
|
export const validateImageFile = (file: File): { valid: boolean; error?: string } => {
|
||||||
|
// Check file type
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
return { valid: false, error: 'File must be an image' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file size (max 10MB)
|
||||||
|
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
return { valid: false, error: 'Image must be smaller than 10MB' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check supported formats
|
||||||
|
const supportedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
|
||||||
|
if (!supportedTypes.includes(file.type)) {
|
||||||
|
return { valid: false, error: 'Supported formats: JPEG, PNG, GIF, WebP' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a unique filename
|
||||||
|
*/
|
||||||
|
export const generateUniqueFileName = (originalName: string, prefix: string = 'img'): string => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const random = Math.random().toString(36).substring(2, 8);
|
||||||
|
const extension = originalName.split('.').pop() || 'jpg';
|
||||||
|
return `${prefix}_${timestamp}_${random}.${extension}`;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user