diff --git a/public/nexus-features.js b/public/nexus-features.js
index 12aef8e..dc487fa 100644
--- a/public/nexus-features.js
+++ b/public/nexus-features.js
@@ -1,611 +1,107 @@
-// NexusChat Enhanced Features
class NexusFeatures {
- constructor() {
- this.emojiPicker = null;
- this.filePreview = null;
- this.messageReactions = new Map();
- this.mentions = new Set();
- this.init();
- }
-
- init() {
- this.setupFileUpload();
- this.setupEmojiPicker();
- this.setupMessageReactions();
- this.setupMentions();
- this.setupMessageSearch();
- this.setupVoiceMessages();
- this.setupMessageThreading();
- }
-
- setupFileUpload() {
- const attachBtn = document.getElementById('attachBtn');
- const fileInput = document.getElementById('fileInput');
-
- if (attachBtn && fileInput) {
- attachBtn.addEventListener('click', () => {
- fileInput.click();
- });
-
- fileInput.addEventListener('change', (e) => {
- const file = e.target.files[0];
- if (file) {
- this.handleFileUpload(file);
- }
- });
- }
- }
-
- handleFileUpload(file) {
- // Create file preview
- const filePreview = this.createFilePreview(file);
- const messagesContainer = document.getElementById('chatMessages');
- messagesContainer.appendChild(filePreview);
-
- // Simulate upload progress
- this.simulateUploadProgress(filePreview, file);
-
- // Scroll to bottom
- this.scrollToBottom();
- }
-
- createFilePreview(file) {
- const preview = document.createElement('div');
- preview.className = 'file-preview nexus-message';
-
- const fileIcon = this.getFileIcon(file.type);
- const fileSize = this.formatFileSize(file.size);
-
- preview.innerHTML = `
-
-
-
-
-
-
-
-
${fileIcon}
-
-
${file.name}
-
${fileSize}
-
-
-
-
-
- `;
-
- return preview;
- }
-
- getFileIcon(type) {
- if (type.startsWith('image/')) return '
';
- if (type.includes('pdf')) return '
';
- if (type.includes('word') || type.includes('document')) return '
';
- if (type.includes('text')) return '
';
- return '
';
- }
-
- formatFileSize(bytes) {
- if (bytes === 0) return '0 Bytes';
- const k = 1024;
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
- }
-
- simulateUploadProgress(preview, file) {
- const progressFill = preview.querySelector('.progress-fill');
- const uploadStatus = preview.querySelector('.upload-status');
-
- let progress = 0;
- const interval = setInterval(() => {
- progress += Math.random() * 20;
- if (progress >= 100) {
- progress = 100;
- clearInterval(interval);
- uploadStatus.textContent = 'Upload complete!';
- progressFill.style.background = 'var(--discord-success)';
- }
- progressFill.style.width = progress + '%';
- }, 200);
- }
-
- setupEmojiPicker() {
- const emojiBtn = document.getElementById('emojiBtn');
-
- if (emojiBtn) {
- emojiBtn.addEventListener('click', () => {
- this.toggleEmojiPicker();
- });
- }
- }
-
- toggleEmojiPicker() {
- if (this.emojiPicker) {
- this.emojiPicker.remove();
- this.emojiPicker = null;
- return;
- }
-
- this.emojiPicker = document.createElement('div');
- this.emojiPicker.className = 'nexus-emoji-picker';
- this.emojiPicker.innerHTML = `
-
-
-
- Recent
- 😀
- 👥
- 🎉
-
-
- ${this.getEmojiGrid('recent')}
-
-
- `;
-
- const inputGroup = document.querySelector('.discord-input-group');
- inputGroup.style.position = 'relative';
- inputGroup.appendChild(this.emojiPicker);
-
- // Add category switching
- this.emojiPicker.addEventListener('click', (e) => {
- if (e.target.classList.contains('emoji-category')) {
- const category = e.target.dataset.category;
- this.switchEmojiCategory(category);
- } else if (e.target.classList.contains('emoji-item')) {
- this.insertEmoji(e.target.textContent);
- this.emojiPicker.remove();
- this.emojiPicker = null;
- }
- });
- }
-
- switchEmojiCategory(category) {
- const categories = this.emojiPicker.querySelectorAll('.emoji-category');
- categories.forEach(cat => cat.classList.remove('active'));
- this.emojiPicker.querySelector(`[data-category="${category}"]`).classList.add('active');
-
- const emojiGrid = this.emojiPicker.querySelector('#emojiGrid');
- emojiGrid.innerHTML = this.getEmojiGrid(category);
- }
-
- getEmojiGrid(category) {
- const emojiCategories = {
- recent: ['😀', '😂', '❤️', '👍', '👎', '🎉', '🔥', '💯'],
- smileys: ['😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣', '😊', '😇', '🙂', '🙃', '😉', '😌', '😍', '🥰'],
- people: ['👤', '👥', '👨', '👩', '👦', '👧', '👶', '👴', '👵', '👨💼', '👩💼', '👨🎓', '👩🎓'],
- objects: ['🎉', '🎊', '🎈', '🎁', '🏆', '🥇', '🥈', '🥉', '🏅', '🎖️', '⭐', '🌟', '💫', '✨', '🔥', '💯']
- };
-
- const emojis = emojiCategories[category] || emojiCategories.smileys;
- return emojis.map(emoji =>
- `
${emoji} `
- ).join('');
- }
-
- insertEmoji(emoji) {
- const messageInput = document.getElementById('messageInput');
- const start = messageInput.selectionStart;
- const end = messageInput.selectionEnd;
- const text = messageInput.value;
-
- messageInput.value = text.substring(0, start) + emoji + text.substring(end);
- messageInput.focus();
- messageInput.setSelectionRange(start + emoji.length, start + emoji.length);
-
- messageInput.dispatchEvent(new Event('input'));
- }
-
- setupMessageReactions() {
- document.addEventListener('click', (e) => {
- if (e.target.classList.contains('message-reaction')) {
- this.handleReaction(e.target);
- }
- });
- }
-
- handleReaction(reactionElement) {
- const messageId = reactionElement.closest('.discord-message').dataset.messageId;
- const emoji = reactionElement.textContent;
-
- if (!this.messageReactions.has(messageId)) {
- this.messageReactions.set(messageId, new Map());
- }
-
- const reactions = this.messageReactions.get(messageId);
- const currentCount = reactions.get(emoji) || 0;
- reactions.set(emoji, currentCount + 1);
-
- this.updateReactionDisplay(reactionElement, currentCount + 1);
- }
-
- updateReactionDisplay(reactionElement, count) {
- const countSpan = reactionElement.querySelector('.reaction-count');
- if (countSpan) {
- countSpan.textContent = count;
- } else {
- reactionElement.innerHTML = `${reactionElement.textContent}
${count} `;
- }
- reactionElement.classList.add('active');
- }
-
- setupMentions() {
- const messageInput = document.getElementById('messageInput');
-
- messageInput.addEventListener('input', (e) => {
- const text = e.target.value;
- const cursorPos = e.target.selectionStart;
-
- // Check for @ mention
- const beforeCursor = text.substring(0, cursorPos);
- const mentionMatch = beforeCursor.match(/@(\w*)$/);
-
- if (mentionMatch) {
- this.showMentionSuggestions(mentionMatch[1]);
- } else {
- this.hideMentionSuggestions();
- }
- });
- }
-
- showMentionSuggestions(query) {
- // Get current users from the participants list
- const participants = Array.from(document.querySelectorAll('#participantsList .discord-username'))
- .map(el => el.textContent)
- .filter(name => name.toLowerCase().includes(query.toLowerCase()));
-
- if (participants.length === 0) return;
-
- let mentionBox = document.getElementById('mentionSuggestions');
- if (!mentionBox) {
- mentionBox = document.createElement('div');
- mentionBox.id = 'mentionSuggestions';
- mentionBox.className = 'mention-suggestions';
- document.querySelector('.discord-input-area').appendChild(mentionBox);
- }
-
- mentionBox.innerHTML = participants.map(name =>
- `
@${name}
`
- ).join('');
-
- mentionBox.style.display = 'block';
-
- // Add click handlers
- mentionBox.addEventListener('click', (e) => {
- if (e.target.classList.contains('mention-item')) {
- this.insertMention(e.target.dataset.name);
- }
- });
- }
-
- insertMention(name) {
- const messageInput = document.getElementById('messageInput');
- const text = messageInput.value;
- const cursorPos = messageInput.selectionStart;
-
- const beforeCursor = text.substring(0, cursorPos);
- const afterCursor = text.substring(cursorPos);
-
- const newText = beforeCursor.replace(/@\w*$/, `@${name} `) + afterCursor;
- messageInput.value = newText;
- messageInput.focus();
-
- this.hideMentionSuggestions();
- }
-
- hideMentionSuggestions() {
- const mentionBox = document.getElementById('mentionSuggestions');
- if (mentionBox) {
- mentionBox.style.display = 'none';
- }
- }
-
- setupMessageSearch() {
- // Add search functionality
- const searchHTML = `
-
- `;
-
- // Add search to sidebar
- const sidebar = document.querySelector('.discord-sidebar .card-body');
- if (sidebar) {
- sidebar.insertAdjacentHTML('afterbegin', searchHTML);
- }
- }
-
- setupVoiceMessages() {
- // Add voice message functionality
- const voiceBtn = document.createElement('button');
- voiceBtn.className = 'btn btn-outline-secondary me-2';
- voiceBtn.innerHTML = '
';
- voiceBtn.title = 'Voice message';
-
- const inputGroup = document.querySelector('.discord-input-group');
- inputGroup.insertBefore(voiceBtn, inputGroup.querySelector('#emojiBtn'));
-
- voiceBtn.addEventListener('click', () => {
- this.toggleVoiceRecording();
- });
- }
-
- toggleVoiceRecording() {
- // Placeholder for voice recording functionality
- console.log('Voice recording toggle - would implement WebRTC here');
- }
-
- setupMessageThreading() {
- // Add threading functionality
- document.addEventListener('contextmenu', (e) => {
- if (e.target.closest('.discord-message')) {
- e.preventDefault();
- this.showMessageContextMenu(e);
- }
- });
- }
-
- showMessageContextMenu(e) {
- const contextMenu = document.createElement('div');
- contextMenu.className = 'message-context-menu';
- contextMenu.innerHTML = `
-
- Reply
-
-
- React
-
-
- Start Thread
-
- `;
-
- contextMenu.style.position = 'fixed';
- contextMenu.style.left = e.pageX + 'px';
- contextMenu.style.top = e.pageY + 'px';
- contextMenu.style.zIndex = '1000';
-
- document.body.appendChild(contextMenu);
-
- // Remove on click outside
- setTimeout(() => {
- document.addEventListener('click', () => {
- contextMenu.remove();
- }, { once: true });
- }, 0);
- }
-
- scrollToBottom() {
- const chatMessages = document.querySelector('#chatMessages');
- chatMessages.scrollTop = chatMessages.scrollHeight;
- }
+ constructor() {
+ this.searchQuery = '';
+ this.init();
+ }
+
+ init() {
+ this.setupComposerTools();
+ this.setupSearch();
+ this.setupMessageObserver();
+ this.restoreDraft();
+ }
+
+ setupComposerTools() {
+ const inputArea = document.querySelector('.discord-input-area');
+ const messageInput = document.getElementById('messageInput');
+ if (!inputArea || !messageInput) return;
+
+ const toolbar = document.createElement('div');
+ toolbar.className = 'composer-toolbar';
+ toolbar.innerHTML = `
+
Enter sends • Shift+Enter adds line • /ai for quick bot prompt
+
0 / 8000
+ `;
+
+ inputArea.insertBefore(toolbar, inputArea.firstChild);
+
+ messageInput.addEventListener('input', () => {
+ const countElement = document.getElementById('composerCharCount');
+ if (!countElement) return;
+
+ countElement.textContent = `${messageInput.value.length} / 8000`;
+ countElement.classList.toggle('composer-char-count-warning', messageInput.value.length > 7000);
+ localStorage.setItem(this.getDraftKey(), messageInput.value);
+ });
+
+ document.addEventListener('chat:message-sent', () => {
+ localStorage.removeItem(this.getDraftKey());
+ const countElement = document.getElementById('composerCharCount');
+ if (!countElement) return;
+ countElement.textContent = '0 / 8000';
+ countElement.classList.remove('composer-char-count-warning');
+ });
+ }
+
+ setupSearch() {
+ const sidebarBody = document.querySelector('.discord-sidebar .card-body');
+ if (!sidebarBody) return;
+
+ const search = document.createElement('div');
+ search.className = 'message-search-container';
+ search.innerHTML = `
+
+
+
+
+ `;
+
+ sidebarBody.insertBefore(search, sidebarBody.firstChild);
+
+ const input = search.querySelector('#messageSearch');
+ input.addEventListener('input', event => {
+ this.searchQuery = event.target.value.trim().toLowerCase();
+ this.filterMessages();
+ });
+ }
+
+ setupMessageObserver() {
+ const chatMessages = document.getElementById('chatMessages');
+ if (!chatMessages) return;
+
+ const observer = new MutationObserver(() => {
+ this.filterMessages();
+ });
+
+ observer.observe(chatMessages, { childList: true, subtree: true });
+ }
+
+ filterMessages() {
+ const messages = document.querySelectorAll('#chatMessages .discord-message');
+ messages.forEach(message => {
+ const text = message.querySelector('.discord-message-text')?.textContent?.toLowerCase() || '';
+ const visible = !this.searchQuery || text.includes(this.searchQuery);
+ message.classList.toggle('message-hidden', !visible);
+ });
+ }
+
+ restoreDraft() {
+ const messageInput = document.getElementById('messageInput');
+ if (!messageInput) return;
+
+ const draft = localStorage.getItem(this.getDraftKey());
+ if (!draft) return;
+
+ messageInput.value = draft;
+ messageInput.dispatchEvent(new Event('input'));
+ }
+
+ getDraftKey() {
+ const roomName = document.getElementById('roomName')?.textContent || 'general';
+ return `chatify:draft:${roomName}`;
+ }
}
-// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
- window.nexusFeatures = new NexusFeatures();
+ window.nexusFeatures = new NexusFeatures();
});
-
-// Add enhanced CSS
-const enhancedCSS = `
-.nexus-emoji-picker {
- position: absolute;
- bottom: 100%;
- left: 0;
- right: 0;
- background: var(--discord-bg-secondary);
- border: 1px solid var(--discord-border);
- border-radius: var(--discord-radius-lg);
- box-shadow: var(--discord-shadow-lg);
- z-index: 1000;
- margin-bottom: 0.5rem;
- max-width: 400px;
-}
-
-.emoji-picker-content {
- padding: 1rem;
-}
-
-.emoji-picker-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 1rem;
- padding-bottom: 0.5rem;
- border-bottom: 1px solid var(--discord-border);
-}
-
-.emoji-categories {
- display: flex;
- gap: 0.5rem;
- margin-bottom: 1rem;
-}
-
-.emoji-category {
- background: transparent;
- border: 1px solid var(--discord-border);
- border-radius: var(--discord-radius);
- padding: 0.5rem;
- color: var(--discord-text-secondary);
- cursor: pointer;
- transition: var(--discord-transition);
-}
-
-.emoji-category.active,
-.emoji-category:hover {
- background: var(--discord-accent);
- color: white;
-}
-
-.emoji-grid {
- display: grid;
- grid-template-columns: repeat(8, 1fr);
- gap: 0.5rem;
- max-height: 200px;
- overflow-y: auto;
-}
-
-.emoji-item {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 32px;
- height: 32px;
- cursor: pointer;
- border-radius: var(--discord-radius);
- transition: var(--discord-transition);
- font-size: 1.2rem;
-}
-
-.emoji-item:hover {
- background: rgba(255, 255, 255, 0.1);
- transform: scale(1.1);
-}
-
-.file-preview {
- margin-bottom: 1rem;
- animation: slideInUp 0.3s ease;
-}
-
-.file-upload-container {
- background: rgba(255, 255, 255, 0.05);
- border-radius: var(--discord-radius);
- padding: 1rem;
- margin-top: 0.5rem;
-}
-
-.file-info {
- display: flex;
- align-items: center;
- gap: 1rem;
- margin-bottom: 1rem;
-}
-
-.file-icon {
- font-size: 2rem;
- color: var(--discord-accent);
-}
-
-.file-details {
- flex: 1;
-}
-
-.file-name {
- font-weight: 600;
- color: var(--discord-text);
-}
-
-.file-size {
- font-size: 0.8rem;
- color: var(--discord-text-secondary);
-}
-
-.upload-progress {
- margin-top: 0.5rem;
-}
-
-.progress-bar {
- width: 100%;
- height: 4px;
- background: rgba(255, 255, 255, 0.2);
- border-radius: 2px;
- overflow: hidden;
- margin-bottom: 0.5rem;
-}
-
-.progress-fill {
- height: 100%;
- background: var(--discord-accent);
- border-radius: 2px;
- transition: width 0.3s ease;
-}
-
-.upload-status {
- font-size: 0.8rem;
- color: var(--discord-text-secondary);
-}
-
-.mention-suggestions {
- position: absolute;
- bottom: 100%;
- left: 0;
- right: 0;
- background: var(--discord-bg-secondary);
- border: 1px solid var(--discord-border);
- border-radius: var(--discord-radius);
- box-shadow: var(--discord-shadow);
- z-index: 1000;
- margin-bottom: 0.5rem;
- max-height: 200px;
- overflow-y: auto;
-}
-
-.mention-item {
- padding: 0.5rem 1rem;
- cursor: pointer;
- transition: var(--discord-transition);
- color: var(--discord-text);
-}
-
-.mention-item:hover {
- background: rgba(255, 255, 255, 0.1);
-}
-
-.message-context-menu {
- background: var(--discord-bg-secondary);
- border: 1px solid var(--discord-border);
- border-radius: var(--discord-radius);
- box-shadow: var(--discord-shadow-lg);
- padding: 0.5rem 0;
- min-width: 150px;
-}
-
-.context-item {
- padding: 0.5rem 1rem;
- cursor: pointer;
- transition: var(--discord-transition);
- color: var(--discord-text);
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.context-item:hover {
- background: rgba(255, 255, 255, 0.1);
-}
-
-@keyframes slideInUp {
- from {
- opacity: 0;
- transform: translateY(20px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-`;
-
-// Add the enhanced CSS
-const style = document.createElement('style');
-style.textContent = enhancedCSS;
-document.head.appendChild(style);
diff --git a/public/styles.css b/public/styles.css
index 4f46f04..f24a10e 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -1231,4 +1231,187 @@ body.discord-theme {
.glow-danger {
box-shadow: 0 0 20px rgba(237, 66, 69, 0.3);
-}
\ No newline at end of file
+}
+/* Enhanced chat features */
+.chat-file {
+ margin-top: 0.65rem;
+ border: 1px solid var(--discord-border);
+ border-radius: var(--discord-radius);
+ background: rgba(255, 255, 255, 0.04);
+ padding: 0.75rem;
+}
+
+.chat-file-header {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.chat-file-header a {
+ color: #8ea1ff;
+ text-decoration: none;
+ font-weight: 600;
+}
+
+.chat-file-header a:hover {
+ text-decoration: underline;
+}
+
+.chat-file-image {
+ width: 100%;
+ max-width: 320px;
+ border-radius: var(--discord-radius);
+ border: 1px solid var(--discord-border);
+}
+
+.chat-file-meta {
+ font-size: 0.8rem;
+ color: var(--discord-text-secondary);
+}
+
+.composer-toolbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.5rem 1.5rem 0;
+ color: var(--discord-text-muted);
+ font-size: 0.8rem;
+}
+
+.composer-hint {
+ display: flex;
+ align-items: center;
+ gap: 0.35rem;
+}
+
+.composer-char-count {
+ font-weight: 600;
+}
+
+.composer-char-count-warning {
+ color: var(--discord-warning);
+}
+
+.message-search-container {
+ padding: 0.75rem;
+ border-bottom: 1px solid var(--discord-border);
+}
+
+.search-input-group {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ background: var(--discord-bg);
+ border: 1px solid var(--discord-border);
+ border-radius: var(--discord-radius);
+ padding: 0.45rem 0.65rem;
+ color: var(--discord-text-muted);
+}
+
+.search-input-group input {
+ width: 100%;
+ border: none;
+ outline: none;
+ background: transparent;
+ color: var(--discord-text);
+ font-size: 0.9rem;
+}
+
+.message-hidden {
+ display: none;
+}
+
+/* ChatGPT + Apple inspired refinements */
+.apple-chat-theme {
+ background: radial-gradient(circle at top, #1f2937, #0b1220 65%);
+}
+
+.glass-navbar {
+ backdrop-filter: blur(14px);
+ background: rgba(18, 24, 37, 0.75);
+}
+
+.chat-top-controls {
+ gap: 0.5rem;
+}
+
+#assistantMode {
+ max-width: 190px;
+ background: rgba(255, 255, 255, 0.1);
+ color: #f1f5f9;
+ border: 1px solid rgba(255, 255, 255, 0.25);
+}
+
+#assistantMode option {
+ color: #111827;
+}
+
+.discord-message {
+ background: rgba(255, 255, 255, 0.01);
+ border-radius: 14px;
+ padding: 0.75rem;
+}
+
+.assistant-message {
+ border: 1px solid rgba(52, 211, 153, 0.4);
+ background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(30, 64, 175, 0.15));
+}
+
+.system-message {
+ border: 1px solid rgba(148, 163, 184, 0.3);
+ background: rgba(148, 163, 184, 0.1);
+}
+
+.poll-card {
+ margin-top: 0.65rem;
+ border: 1px solid var(--discord-border);
+ border-radius: var(--discord-radius);
+ background: rgba(15, 23, 42, 0.7);
+ padding: 0.75rem;
+}
+
+.poll-question {
+ font-weight: 600;
+ margin-bottom: 0.5rem;
+}
+
+.poll-options {
+ display: grid;
+ gap: 0.4rem;
+}
+
+.poll-option {
+ border: 1px solid rgba(148, 163, 184, 0.35);
+ background: rgba(148, 163, 184, 0.12);
+ color: var(--discord-text);
+ border-radius: 10px;
+ padding: 0.45rem 0.6rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ transition: var(--discord-transition);
+}
+
+.poll-option:hover {
+ border-color: #60a5fa;
+ background: rgba(59, 130, 246, 0.18);
+}
+
+.poll-option-votes {
+ color: var(--discord-text-secondary);
+ font-size: 0.82rem;
+}
+
+.poll-total {
+ margin-top: 0.5rem;
+ color: var(--discord-text-muted);
+ font-size: 0.82rem;
+}
+
+@media (max-width: 900px) {
+ #assistantMode {
+ max-width: 145px;
+ font-size: 0.78rem;
+ }
+}
diff --git a/server.js b/server.js
index 5ba0e8d..fffdd9d 100644
--- a/server.js
+++ b/server.js
@@ -1,186 +1,402 @@
-const express = require('express');
-const path = require('path');
-const http = require('http');
-const socketio = require('socket.io');
-const { userJoin, getCurrentUser, userLeave, getRoomUsers, updateUserStatus } = require('./utils/users');
-const formatMessage = require('./utils/messages');
-const MessageHistory = require('./utils/messageHistory');
-
-const app = express();
-const server = http.createServer(app);
-const io = socketio(server);
-
-const port = process.env.PORT || 3000;
-const botName = 'ChatifyBot';
-
-// Initialize message history
-const messageHistory = new MessageHistory();
-
-// Middleware to parse JSON and urlencoded data
-app.use(express.json());
-app.use(express.urlencoded({ extended: true }));
-
-// Serve static files from the 'public' directory
-app.use(express.static('public'));
-
-// Serve static files from the 'views' directory
-app.use(express.static('views'));
-
-// Health check endpoint
-app.get('/health', (req, res) => {
- res.status(200).json({
- status: 'healthy',
- timestamp: new Date().toISOString(),
- uptime: process.uptime(),
- memory: process.memoryUsage(),
- version: require('./package.json').version
- });
-});
-
-// Serve index.html for the root route
-app.get('/', (req, res) => {
- res.sendFile(path.join(__dirname, 'public', 'index.html'));
-});
-
-// Handle chat room join
-app.post('/join', (req, res) => {
- const { name, roomCode } = req.body;
-
- if (name && roomCode) {
- res.redirect(`/transfer-screen.html?redirect=${encodeURIComponent(`/chat.html?name=${encodeURIComponent(name)}&room=${encodeURIComponent(roomCode)}`)}`);
- } else {
- res.redirect('/?error=invalid');
- }
-});
- // Run when client connects
- io.on('connection', socket => {
- console.log(`User connected: ${socket.id}`);
-
- socket.on('joinRoom', ({ username, room }) => {
- const user = userJoin(socket.id, username, room);
- socket.join(user.room);
-
- // Send message history to the user
- const history = messageHistory.getMessages(user.room, 50);
- socket.emit('messageHistory', history);
-
- // Welcome current user
- const welcomeMessage = formatMessage(botName, `Welcome to ${user.room}, ${user.username}! 🎉`);
- socket.emit('message', welcomeMessage);
- messageHistory.addMessage(user.room, welcomeMessage);
-
- // Broadcast when a user connects
- const joinMessage = formatMessage(botName, `${user.username} joined the server`);
- socket.broadcast
- .to(user.room)
- .emit('message', joinMessage);
- messageHistory.addMessage(user.room, joinMessage);
-
- // Send users and room info
- io.to(user.room).emit('roomUsers', {
- room: user.room,
- users: getRoomUsers(user.room)
- });
-
- // Typing indicators
- socket.on('typing', () => {
- const user = getCurrentUser(socket.id);
- if (user) {
- socket.broadcast.to(user.room).emit('typing', user.username);
- }
- });
-
- socket.on('stopTyping', () => {
- const user = getCurrentUser(socket.id);
- if (user) {
- socket.broadcast.to(user.room).emit('stopTyping', user.username);
- }
- });
- });
-
- // Listen for chatMessage
- socket.on('chatMessage', msg => {
- const user = getCurrentUser(socket.id);
- if (user) {
- const message = formatMessage(user.username, msg);
- io.to(user.room).emit('message', message);
- messageHistory.addMessage(user.room, message);
- }
- });
-
- // Runs when client disconnects
- socket.on('disconnect', (reason) => {
- console.log(`User disconnected: ${socket.id}, reason: ${reason}`);
- const user = userLeave(socket.id);
-
- if (user) {
- const leaveMessage = formatMessage(botName, `${user.username} left the server`);
- io.to(user.room).emit('message', leaveMessage);
- messageHistory.addMessage(user.room, leaveMessage);
-
- // Send users and room info
- io.to(user.room).emit('roomUsers', {
- room: user.room,
- users: getRoomUsers(user.room)
- });
- }
- });
-
- // Handle reconnection
- socket.on('reconnect', () => {
- console.log(`User reconnected: ${socket.id}`);
- // Update user status to online
- const user = getCurrentUser(socket.id);
- if (user) {
- updateUserStatus(socket.id, 'online');
- io.to(user.room).emit('roomUsers', {
- room: user.room,
- users: getRoomUsers(user.room)
- });
- }
- });
-
- socket.on('updateStatus', (status) => {
- const user = updateUserStatus(socket.id, status);
- if (user) {
- io.to(user.room).emit('roomUsers', {
- room: user.room,
- users: getRoomUsers(user.room)
- });
- }
- });
- });
-
-// Error handling
-process.on('uncaughtException', (err) => {
- console.error('Uncaught Exception:', err);
- process.exit(1);
-});
-
-process.on('unhandledRejection', (reason, promise) => {
- console.error('Unhandled Rejection at:', promise, 'reason:', reason);
- process.exit(1);
-});
-
-// Graceful shutdown
-process.on('SIGTERM', () => {
- console.log('SIGTERM received, shutting down gracefully');
- server.close(() => {
- console.log('Process terminated');
- process.exit(0);
- });
-});
-
-process.on('SIGINT', () => {
- console.log('SIGINT received, shutting down gracefully');
- server.close(() => {
- console.log('Process terminated');
- process.exit(0);
- });
-});
-
-// Start the server
-server.listen(port, () => {
- console.log(`Chatify server running on port ${port}`);
- console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
-});
+const express = require('express');
+const path = require('path');
+const http = require('http');
+const socketio = require('socket.io');
+const { userJoin, getCurrentUser, userLeave, getRoomUsers, updateUserStatus } = require('./utils/users');
+const formatMessage = require('./utils/messages');
+const MessageHistory = require('./utils/messageHistory');
+
+const app = express();
+const server = http.createServer(app);
+const io = socketio(server, {
+ maxHttpBufferSize: 6 * 1024 * 1024
+});
+
+const port = process.env.PORT || 3000;
+const botName = 'Chatify Assistant';
+const MAX_MESSAGE_LENGTH = 8000;
+const MAX_FILE_SIZE = 5 * 1024 * 1024;
+const GEMINI_API_KEY = process.env.GEMINI_API_KEY || 'AIzaSyDdb9CwRD5CpkC28n91zrE9WwMoFQTgwoY';
+const GEMINI_MODELS = ['gemini-3.0-flash', 'gemini-2.5-flash', 'gemini-2.0-flash'];
+
+const messageHistory = new MessageHistory();
+const roomPolls = new Map(); // room -> Map(pollId => poll)
+
+app.use(express.json());
+app.use(express.urlencoded({ extended: true }));
+app.use(express.static('public'));
+app.use(express.static('views'));
+
+app.get('/health', (req, res) => {
+ res.status(200).json({
+ status: 'healthy',
+ timestamp: new Date().toISOString(),
+ uptime: process.uptime(),
+ memory: process.memoryUsage(),
+ version: require('./package.json').version
+ });
+});
+
+app.get('/', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'index.html'));
+});
+
+app.post('/join', (req, res) => {
+ const name = (req.body.name || '').trim();
+ const roomCode = (req.body.roomCode || '').trim();
+
+ if (name && roomCode) {
+ res.redirect(`/transfer-screen.html?redirect=${encodeURIComponent(`/chat.html?name=${encodeURIComponent(name)}&room=${encodeURIComponent(roomCode)}`)}`);
+ return;
+ }
+
+ res.redirect('/?error=invalid');
+});
+
+function sanitizeText(value) {
+ return value
+ .toString()
+ .replace(/\r\n/g, '\n')
+ .replace(/\u0000/g, '')
+ .slice(0, MAX_MESSAGE_LENGTH)
+ .trim();
+}
+
+function sendRoomUsers(room) {
+ io.to(room).emit('roomUsers', {
+ room,
+ users: getRoomUsers(room)
+ });
+}
+
+function ensureRoomPolls(room) {
+ if (!roomPolls.has(room)) {
+ roomPolls.set(room, new Map());
+ }
+ return roomPolls.get(room);
+}
+
+function getFallbackReply(prompt) {
+ const normalized = prompt.toLowerCase();
+
+ if (normalized.includes('help')) {
+ return 'I can help summarize ideas, draft replies, create action plans, and assist with decisions. Try asking for a summary, roadmap, or polished response.';
+ }
+
+ if (normalized.includes('summary') || normalized.includes('summarize')) {
+ return 'Summary: your room supports long-form messages, file sharing, polls, and assistant prompts for faster decision-making.';
+ }
+
+ if (normalized.includes('roadmap') || normalized.includes('plan')) {
+ return 'Suggested plan: 1) Define goals 2) Open a poll 3) Share relevant files 4) Assign owners 5) Track updates in-thread.';
+ }
+
+ return 'Thanks! I can turn that into a cleaner draft, checklist, or decision poll if you want.';
+}
+
+async function generateAssistantReply({ prompt, mode, room, username }) {
+ const contextMessages = messageHistory
+ .getMessages(room, 12)
+ .filter(message => message.type === 'text')
+ .slice(-8)
+ .map(message => `${message.username}: ${message.text}`)
+ .join('\n');
+
+ const systemInstruction = `You are Chatify Assistant inside a group chat app. Keep answers useful, concise, and specific.\nMode: ${mode}.\nIf asked for plans/summaries, provide structured bullet points.\nUser: ${username}.`;
+
+ const userPrompt = `Conversation context:\n${contextMessages || '(no recent context)'}\n\nCurrent user prompt:\n${prompt}`;
+
+ const payload = {
+ contents: [{ role: 'user', parts: [{ text: userPrompt }] }],
+ systemInstruction: { role: 'system', parts: [{ text: systemInstruction }] },
+ generationConfig: {
+ temperature: mode === 'creative' ? 1.0 : mode === 'concise' ? 0.4 : 0.7,
+ topP: 0.9,
+ maxOutputTokens: 450
+ }
+ };
+
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), 12000);
+
+ try {
+ for (const model of GEMINI_MODELS) {
+ const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${GEMINI_API_KEY}`;
+ const response = await fetch(endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(payload),
+ signal: controller.signal
+ });
+
+ if (!response.ok) {
+ continue;
+ }
+
+ const data = await response.json();
+ const text = data?.candidates?.[0]?.content?.parts?.map(part => part.text).join('\n').trim();
+ if (text) {
+ clearTimeout(timeout);
+ return text.slice(0, MAX_MESSAGE_LENGTH);
+ }
+ }
+
+ return getFallbackReply(prompt);
+ } catch (error) {
+ console.error('Assistant generation failed:', error.message);
+ return getFallbackReply(prompt);
+ } finally {
+ clearTimeout(timeout);
+ }
+}
+
+async function emitAssistantReply({ socket, user, prompt, mode }) {
+ const safeMode = sanitizeText(mode || 'balanced').slice(0, 20) || 'balanced';
+ const safePrompt = sanitizeText(prompt || '');
+ if (!safePrompt) return;
+
+ const responseText = await generateAssistantReply({
+ prompt: safePrompt,
+ mode: safeMode,
+ room: user.room,
+ username: user.username
+ });
+
+ const botMessage = {
+ ...formatMessage(botName, responseText),
+ type: 'text',
+ meta: { role: 'assistant', mode: safeMode }
+ };
+
+ io.to(user.room).emit('message', botMessage);
+ messageHistory.addMessage(user.room, botMessage);
+}
+
+io.on('connection', socket => {
+ console.log(`User connected: ${socket.id}`);
+
+ socket.on('joinRoom', ({ username, room }) => {
+ const safeUsername = sanitizeText(username || 'Guest').slice(0, 30);
+ const safeRoom = sanitizeText(room || 'general').slice(0, 40);
+
+ const user = userJoin(socket.id, safeUsername || 'Guest', safeRoom || 'general');
+ socket.join(user.room);
+
+ const history = messageHistory.getMessages(user.room, 120);
+ socket.emit('messageHistory', history);
+
+ const welcomeMessage = {
+ ...formatMessage(botName, `Welcome to ${user.room}, ${user.username}. You can share files, create polls, and use the assistant tools from the top action bar.`),
+ type: 'text',
+ meta: { role: 'assistant' }
+ };
+ socket.emit('message', welcomeMessage);
+
+ const joinMessage = {
+ ...formatMessage(botName, `${user.username} joined the room`),
+ type: 'text',
+ meta: { role: 'system' }
+ };
+ socket.broadcast.to(user.room).emit('message', joinMessage);
+ messageHistory.addMessage(user.room, joinMessage);
+
+ sendRoomUsers(user.room);
+ });
+
+ socket.on('chatMessage', async rawMessage => {
+ const user = getCurrentUser(socket.id);
+ if (!user) return;
+
+ const cleanedText = sanitizeText(rawMessage);
+ if (!cleanedText) return;
+
+ const message = {
+ ...formatMessage(user.username, cleanedText),
+ type: 'text'
+ };
+
+ io.to(user.room).emit('message', message);
+ messageHistory.addMessage(user.room, message);
+
+ if (cleanedText.startsWith('/ai ')) {
+ const prompt = cleanedText.slice(4).trim();
+ await emitAssistantReply({ socket, user, prompt, mode: 'balanced' });
+ }
+ });
+
+ socket.on('assistantPrompt', async payload => {
+ const user = getCurrentUser(socket.id);
+ if (!user || !payload) return;
+
+ const prompt = sanitizeText(payload.prompt || '');
+ const mode = sanitizeText(payload.mode || 'balanced').slice(0, 20);
+ if (!prompt) return;
+
+ await emitAssistantReply({ socket, user, prompt, mode });
+ });
+
+ socket.on('createPoll', payload => {
+ const user = getCurrentUser(socket.id);
+ if (!user || !payload) return;
+
+ const question = sanitizeText(payload.question || '').slice(0, 240);
+ const options = Array.isArray(payload.options)
+ ? payload.options.map(option => sanitizeText(option).slice(0, 90)).filter(Boolean).slice(0, 6)
+ : [];
+
+ if (!question || options.length < 2) {
+ socket.emit('pollError', 'A poll needs a question and at least 2 options.');
+ return;
+ }
+
+ const pollId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
+ const poll = {
+ id: pollId,
+ room: user.room,
+ createdBy: user.username,
+ question,
+ options: options.map((option, index) => ({ index, option, votes: 0 })),
+ voters: {}
+ };
+
+ const roomPollMap = ensureRoomPolls(user.room);
+ roomPollMap.set(pollId, poll);
+
+ const pollMessage = {
+ ...formatMessage(user.username, `Poll: ${question}`),
+ type: 'poll',
+ poll
+ };
+
+ io.to(user.room).emit('message', pollMessage);
+ messageHistory.addMessage(user.room, pollMessage);
+ });
+
+ socket.on('votePoll', payload => {
+ const user = getCurrentUser(socket.id);
+ if (!user || !payload) return;
+
+ const pollId = sanitizeText(payload.pollId || '').slice(0, 60);
+ const optionIndex = Number(payload.optionIndex);
+ const roomPollMap = ensureRoomPolls(user.room);
+ const poll = roomPollMap.get(pollId);
+
+ if (!poll || Number.isNaN(optionIndex) || !poll.options[optionIndex]) {
+ socket.emit('pollError', 'Poll vote was invalid.');
+ return;
+ }
+
+ const previousVote = poll.voters[socket.id];
+ if (previousVote !== undefined && poll.options[previousVote]) {
+ poll.options[previousVote].votes = Math.max(0, poll.options[previousVote].votes - 1);
+ }
+
+ poll.voters[socket.id] = optionIndex;
+ poll.options[optionIndex].votes += 1;
+
+ const pollMessage = {
+ ...formatMessage(botName, `${user.username} voted on: ${poll.question}`),
+ type: 'poll-update',
+ poll
+ };
+
+ io.to(user.room).emit('pollUpdate', pollMessage);
+ });
+
+ socket.on('fileUpload', payload => {
+ const user = getCurrentUser(socket.id);
+ if (!user || !payload) return;
+
+ const name = sanitizeText(payload.name || 'attachment').slice(0, 120);
+ const type = sanitizeText(payload.type || 'application/octet-stream').slice(0, 100);
+ const size = Number(payload.size) || 0;
+ const data = typeof payload.data === 'string' ? payload.data : '';
+
+ if (!name || !data.startsWith('data:') || data.length < 30) {
+ socket.emit('uploadError', 'Invalid file payload.');
+ return;
+ }
+
+ if (size > MAX_FILE_SIZE) {
+ socket.emit('uploadError', `File is too large. Max size is ${Math.round(MAX_FILE_SIZE / (1024 * 1024))}MB.`);
+ return;
+ }
+
+ const fileMessage = {
+ ...formatMessage(user.username, `${user.username} shared a file: ${name}`),
+ type: 'file',
+ file: { name, type, size, data }
+ };
+
+ io.to(user.room).emit('message', fileMessage);
+ messageHistory.addMessage(user.room, fileMessage);
+ });
+
+ socket.on('typing', () => {
+ const user = getCurrentUser(socket.id);
+ if (user) {
+ socket.broadcast.to(user.room).emit('typing', user.username);
+ }
+ });
+
+ socket.on('stopTyping', () => {
+ const user = getCurrentUser(socket.id);
+ if (user) {
+ socket.broadcast.to(user.room).emit('stopTyping', user.username);
+ }
+ });
+
+ socket.on('updateStatus', status => {
+ const user = updateUserStatus(socket.id, status);
+ if (user) sendRoomUsers(user.room);
+ });
+
+ socket.on('disconnect', reason => {
+ console.log(`User disconnected: ${socket.id}, reason: ${reason}`);
+ const user = userLeave(socket.id);
+ if (!user) return;
+
+ const leaveMessage = {
+ ...formatMessage(botName, `${user.username} left the room`),
+ type: 'text',
+ meta: { role: 'system' }
+ };
+
+ io.to(user.room).emit('message', leaveMessage);
+ messageHistory.addMessage(user.room, leaveMessage);
+ sendRoomUsers(user.room);
+ });
+});
+
+process.on('uncaughtException', err => {
+ console.error('Uncaught Exception:', err);
+ process.exit(1);
+});
+
+process.on('unhandledRejection', (reason, promise) => {
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
+ process.exit(1);
+});
+
+process.on('SIGTERM', () => {
+ console.log('SIGTERM received, shutting down gracefully');
+ server.close(() => {
+ console.log('Process terminated');
+ process.exit(0);
+ });
+});
+
+process.on('SIGINT', () => {
+ console.log('SIGINT received, shutting down gracefully');
+ server.close(() => {
+ console.log('Process terminated');
+ process.exit(0);
+ });
+});
+
+server.listen(port, () => {
+ console.log(`Chatify server running on port ${port}`);
+ console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
+});
diff --git a/utils/users.js b/utils/users.js
index ecd3f14..fa401fc 100644
--- a/utils/users.js
+++ b/utils/users.js
@@ -1,37 +1,54 @@
-const users = [];
-
-function userJoin(id, username, room) {
- const user = { id, username, room };
- users.push(user);
- return user;
-}
-
-function getCurrentUser(id) {
- return users.find(user => user.id === id);
-}
-
-function userLeave(id) {
- const index = users.findIndex(user => user.id === id);
- if (index !== -1) {
- return users.splice(index, 1)[0];
- }
-}
- function getRoomUsers(room) {
- return users.filter(user => user.room === room);
- }
-
- function updateUserStatus(id, status) {
- const user = users.find(user => user.id === id);
- if (user) {
- user.status = status;
- }
- return user;
- }
-
- module.exports = {
- userJoin,
- getCurrentUser,
- userLeave,
- getRoomUsers,
- updateUserStatus
- };
+const users = [];
+
+function userJoin(id, username, room) {
+ const existingIndex = users.findIndex(user => user.id === id);
+ if (existingIndex !== -1) {
+ users.splice(existingIndex, 1);
+ }
+
+ const cleanUsername = (username || 'Guest').toString().trim().slice(0, 30);
+ const cleanRoom = (room || 'general').toString().trim().slice(0, 40);
+ const user = {
+ id,
+ username: cleanUsername || 'Guest',
+ room: cleanRoom || 'general',
+ status: 'online'
+ };
+
+ users.push(user);
+ return user;
+}
+
+function getCurrentUser(id) {
+ return users.find(user => user.id === id);
+}
+
+function userLeave(id) {
+ const index = users.findIndex(user => user.id === id);
+ if (index !== -1) {
+ return users.splice(index, 1)[0];
+ }
+
+ return undefined;
+}
+
+function getRoomUsers(room) {
+ return users.filter(user => user.room === room);
+}
+
+function updateUserStatus(id, status) {
+ const user = users.find(candidate => candidate.id === id);
+ if (user) {
+ user.status = status;
+ }
+
+ return user;
+}
+
+module.exports = {
+ userJoin,
+ getCurrentUser,
+ userLeave,
+ getRoomUsers,
+ updateUserStatus
+};
diff --git a/views/chat.html b/views/chat.html
index d0fa3a4..1711510 100644
--- a/views/chat.html
+++ b/views/chat.html
@@ -9,10 +9,9 @@
-
-
-
+
+
@@ -22,12 +21,24 @@
general
-
+
+
+ Assistant: Balanced
+ Assistant: Creative
+ Assistant: Concise
+
+
+
+ Quick Actions
+
+
+
-
- 0
-
+ 0
@@ -35,11 +46,7 @@
-
-
- Leave Server
-
-
+ Leave
@@ -52,35 +59,23 @@
-
-
-
+
@@ -90,26 +85,18 @@
-
-