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

Chat

- {selectedEmployees.length > 0 && ( -
- Analyzing: -
- {selectedEmployees.slice(0, 3).map((emp, index) => ( -
- {emp.name} -
- ))} - {selectedEmployees.length > 3 && ( -
- +{selectedEmployees.length - 3} more -
- )} -
-
- )} -
-
- - {/* Messages Area */} -
- {hasMessages ? ( -
- -
-
- ) : ( - children - )} -
- - {/* Input Area */} -
-
-
- - - {/* Send Button */} - -
-
-
-
-
- ); -}; - -export default ChatLayout; \ No newline at end of file diff --git a/components/chat/ChatSidebar.tsx b/components/chat/ChatSidebar.tsx deleted file mode 100644 index 91bfae7..0000000 --- a/components/chat/ChatSidebar.tsx +++ /dev/null @@ -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 = ({ icon, label, isActive, onClick }) => ( -
- {icon} -
- {label} -
-
-); - -interface ChatSidebarProps { - currentPage?: string; - onNavigate?: (page: string) => void; -} - -const ChatSidebar: React.FC = ({ currentPage = 'chat', onNavigate }) => { - const { org } = useOrg(); - - const handleNavigation = (page: string) => { - if (onNavigate) { - onNavigate(page); - } - }; - - return ( -
- {/* Company Selection */} -
-
-
-
-
-
-
- {/* Company Icon SVG */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- {org?.name || 'Zitlac Media'} -
-
-
-
- - - -
-
- - {/* Navigation Menu */} -
-
- - - - } - label="Company Wiki" - isActive={currentPage === 'wiki'} - onClick={() => handleNavigation('wiki')} - /> - - - - } - label="Submissions" - isActive={currentPage === 'submissions'} - onClick={() => handleNavigation('submissions')} - /> - - - - - - - - - - - } - label="Reports" - isActive={currentPage === 'reports'} - onClick={() => handleNavigation('reports')} - /> - - - - } - label="Chat" - isActive={currentPage === 'chat'} - onClick={() => handleNavigation('chat')} - /> - - - - - - - - - - - } - label="Help" - isActive={currentPage === 'help'} - onClick={() => handleNavigation('help')} - /> -
-
-
- - {/* Bottom Section with Settings and Company Report Builder */} -
- - - - - - - - - - - - } - label="Settings" - isActive={currentPage === 'settings'} - onClick={() => handleNavigation('settings')} - /> - - {/* Company Report Builder Card */} -
-
-
-
-
-
-
-
- Build {org?.name || '[Company]'}'s Report -
-
- Share this form with your team members to capture valuable info about your company to train Auditly. -
-
-
-
-
-
-
Invite
-
-
- - - -
-
-
-
- - - -
-
-
Copy
-
-
-
-
-
-
-
- ); -}; - -export default ChatSidebar; \ No newline at end of file diff --git a/functions/index.js b/functions/index.js index 7d262bf..1d4ee0c 100644 --- a/functions/index.js +++ b/functions/index.js @@ -912,7 +912,7 @@ exports.chat = onRequest({ cors: true }, async (req, res) => { 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) { return res.status(400).json({ error: "Message is required" }); @@ -932,9 +932,44 @@ Current Context: ${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. `.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({ model: "gpt-4o", messages: [ @@ -944,21 +979,25 @@ Provide helpful, actionable insights while maintaining professional confidential }, { role: "user", - content: message + content: userContent } ], temperature: 0.7, - max_tokens: 500, + max_tokens: 1000, // Increased for more detailed responses when analyzing images }); response = completion.choices[0].message.content; } else { // 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 = [ - "That's an interesting point about performance metrics. Based on the data, I'd recommend focusing on...", - "I can see from the employee report that there are opportunities for growth in...", - "The company analysis suggests that this area needs attention. Here's what I would suggest...", - "Based on the performance data, this employee shows strong potential in...", + `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...${attachmentText}`, + `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...${attachmentText}`, ]; response = responses[Math.floor(Math.random() * responses.length)]; diff --git a/index.css b/index.css index 740ab6f..d42e032 100644 --- a/index.css +++ b/index.css @@ -1,5 +1,19 @@ @import "tailwindcss"; +/* Blinking cursor animation for chat input */ +@keyframes blink { + + 0%, + 50% { + opacity: 1; + } + + 51%, + 100% { + opacity: 0; + } +} + :root { diff --git a/index.tsx b/index.tsx index 1a099f8..8c06fba 100644 --- a/index.tsx +++ b/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; +import App from './src/App'; import './index.css'; const rootElement = document.getElementById('root'); diff --git a/pages/ChatNew.tsx b/pages/ChatNew.tsx deleted file mode 100644 index 58a8b69..0000000 --- a/pages/ChatNew.tsx +++ /dev/null @@ -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(null); - const fileInputRef = useRef(null); - - const [state, setState] = useState({ - 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) => { - 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) => { - 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 ( -
- {filteredEmployees.slice(0, 3).map((employee, index) => ( -
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' : '' - }`} - > -
- {employee.name} -
-
- ))} -
- ); - }; - - const renderUploadedFiles = () => { - if (state.uploadedFiles.length === 0) return null; - - return ( -
- {state.uploadedFiles.map((file, index) => ( -
-
-
-
-
- - - -
-
-
{file.name}
-
-
removeFile(index)} className="cursor-pointer"> - - - -
-
-
- ))} -
- ); - }; - - const renderChatInterface = () => { - if (state.messages.length === 0) { - return ( -
-
-
What would you like to understand?
-
- {categories.map((category) => ( -
setSelectedCategory(category)} - className={`px-3 py-1.5 rounded-lg shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)] shadow-[inset_0px_-2px_0px_0px_rgba(10,13,18,0.05)] shadow-[inset_0px_0px_0px_1px_rgba(10,13,18,0.18)] flex justify-center items-center gap-1 overflow-hidden cursor-pointer ${selectedCategory === category ? 'bg-white' : '' - }`} - > -
-
{category}
-
-
- ))} -
-
-
- {questionStarters.map((question, index) => ( -
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" - > -
- - - - - - - - - - -
-
{question}
-
- ))} -
-
-
- {renderChatInput()} -
- ); - } - - return ( -
-
- {state.messages.map((message) => ( -
-
- {message.content} -
-
- ))} - {state.isLoading && ( -
-
-
-
-
-
-
-
-
- )} -
- {renderChatInput()} -
- ); - }; - - const renderChatInput = () => { - return ( -
- {renderUploadedFiles()} -
- {currentInput || "Ask anything, use @ to tag staff and ask questions."} -
-
-
-
fileInputRef.current?.click()} className="cursor-pointer"> - - - -
-
fileInputRef.current?.click()} className="cursor-pointer"> - - - -
-
- - - - - - - - - - -
-
-
0 - ? 'bg-Main-BG-Gray-800' - : 'bg-Text-Gray-300' - }`} - > -
- - - -
-
-
- {renderEmployeeMenu()} -