zhlajiex
commited on
Commit
·
072fffe
1
Parent(s):
1418755
Feat: Show attached image in chat and provide local preview
Browse files- backend/controllers/ai.js +15 -7
- backend/public/chat.html +22 -7
- backend/routes/ai.js +1 -1
- backend/server.js +2 -2
backend/controllers/ai.js
CHANGED
|
@@ -81,9 +81,11 @@ exports.chat = asyncHandler(async (req, res, next) => {
|
|
| 81 |
// 2. Handle File (Sync Handshake to prevent OS Termination)
|
| 82 |
let attachmentContext = '';
|
| 83 |
let vaultLink = '';
|
|
|
|
| 84 |
if (req.file) {
|
| 85 |
console.log('Vault: Initiating Priority Upload...');
|
| 86 |
attachmentContext = await processFile(req.file.path);
|
|
|
|
| 87 |
try {
|
| 88 |
const vaultData = await uploadToVault(req.file.path, req.file.originalname);
|
| 89 |
if (vaultData && vaultData.success) {
|
|
@@ -92,12 +94,8 @@ exports.chat = asyncHandler(async (req, res, next) => {
|
|
| 92 |
}
|
| 93 |
} catch (e) {
|
| 94 |
console.error("Vault_Priority_Error:", e.message);
|
| 95 |
-
} finally {
|
| 96 |
-
if (fs.existsSync(req.file.path)) {
|
| 97 |
-
fs.unlinkSync(req.file.path);
|
| 98 |
-
console.log(`[SYSTEM] Temp file purged: ${req.file.path}`);
|
| 99 |
-
}
|
| 100 |
}
|
|
|
|
| 101 |
}
|
| 102 |
|
| 103 |
// 3. Build History
|
|
@@ -185,7 +183,12 @@ exports.chat = asyncHandler(async (req, res, next) => {
|
|
| 185 |
|
| 186 |
// Save and end
|
| 187 |
const userContent = message || "[SIGNAL]";
|
| 188 |
-
await Message.create({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
await Message.create({ sessionId: session._id, sender: 'ai', content: fullAIResponse, modelUsed: model });
|
| 190 |
|
| 191 |
user.usage.requestsToday += 1;
|
|
@@ -275,7 +278,12 @@ exports.chat = asyncHandler(async (req, res, next) => {
|
|
| 275 |
try {
|
| 276 |
// Persist Messages after stream ends
|
| 277 |
const userContent = message || (req.file ? `[Attached Image: ${req.file.originalname}]` : "[SIGNAL]");
|
| 278 |
-
await Message.create({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
await Message.create({ sessionId: session._id, sender: 'ai', content: fullAIResponse, modelUsed: model });
|
| 280 |
|
| 281 |
user.usage.requestsToday += 1;
|
|
|
|
| 81 |
// 2. Handle File (Sync Handshake to prevent OS Termination)
|
| 82 |
let attachmentContext = '';
|
| 83 |
let vaultLink = '';
|
| 84 |
+
let attachmentUrl = '';
|
| 85 |
if (req.file) {
|
| 86 |
console.log('Vault: Initiating Priority Upload...');
|
| 87 |
attachmentContext = await processFile(req.file.path);
|
| 88 |
+
attachmentUrl = `/uploads/${req.file.filename}`;
|
| 89 |
try {
|
| 90 |
const vaultData = await uploadToVault(req.file.path, req.file.originalname);
|
| 91 |
if (vaultData && vaultData.success) {
|
|
|
|
| 94 |
}
|
| 95 |
} catch (e) {
|
| 96 |
console.error("Vault_Priority_Error:", e.message);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
}
|
| 98 |
+
// We keep the file in public/uploads so the frontend can display it
|
| 99 |
}
|
| 100 |
|
| 101 |
// 3. Build History
|
|
|
|
| 183 |
|
| 184 |
// Save and end
|
| 185 |
const userContent = message || "[SIGNAL]";
|
| 186 |
+
await Message.create({
|
| 187 |
+
sessionId: session._id,
|
| 188 |
+
sender: 'user',
|
| 189 |
+
content: userContent,
|
| 190 |
+
attachmentUrl: attachmentUrl
|
| 191 |
+
});
|
| 192 |
await Message.create({ sessionId: session._id, sender: 'ai', content: fullAIResponse, modelUsed: model });
|
| 193 |
|
| 194 |
user.usage.requestsToday += 1;
|
|
|
|
| 278 |
try {
|
| 279 |
// Persist Messages after stream ends
|
| 280 |
const userContent = message || (req.file ? `[Attached Image: ${req.file.originalname}]` : "[SIGNAL]");
|
| 281 |
+
await Message.create({
|
| 282 |
+
sessionId: session._id,
|
| 283 |
+
sender: 'user',
|
| 284 |
+
content: userContent,
|
| 285 |
+
attachmentUrl: attachmentUrl
|
| 286 |
+
});
|
| 287 |
await Message.create({ sessionId: session._id, sender: 'ai', content: fullAIResponse, modelUsed: model });
|
| 288 |
|
| 289 |
user.usage.requestsToday += 1;
|
backend/public/chat.html
CHANGED
|
@@ -210,12 +210,21 @@
|
|
| 210 |
|
| 211 |
let currentSessionId = null, selectedFile = null, isProcessing = false;
|
| 212 |
const chatWindow = document.getElementById('chat-window'), input = document.getElementById('user-input');
|
|
|
|
| 213 |
|
| 214 |
function handleFile(input) {
|
| 215 |
if (input.files && input.files[0]) {
|
| 216 |
selectedFile = input.files[0];
|
| 217 |
const plusBtn = document.querySelector('button[onclick*="file-input"]');
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
console.log(`[FILE_LOADED] ${selectedFile.name}`);
|
| 220 |
}
|
| 221 |
}
|
|
@@ -290,7 +299,7 @@
|
|
| 290 |
currentSessionId = id; chatWindow.innerHTML = ''; toggleMenu();
|
| 291 |
const res = await fetch(`${API_BASE}/api/ai/sessions/${id}/messages`, { headers: { 'Authorization': `Bearer ${token}` } });
|
| 292 |
const data = await res.json();
|
| 293 |
-
if (data.success) data.data.forEach(m => appendMessage(m.sender === 'user' ? 'user' : 'ai', m.content, m.modelUsed));
|
| 294 |
loadHistory();
|
| 295 |
}
|
| 296 |
|
|
@@ -303,7 +312,9 @@
|
|
| 303 |
const fd = new FormData(); fd.append('message', message); fd.append('model', activeModel);
|
| 304 |
if (currentSessionId) fd.append('sessionId', currentSessionId);
|
| 305 |
if (selectedFile) fd.append('file', selectedFile);
|
| 306 |
-
|
|
|
|
|
|
|
| 307 |
input.value = ''; input.style.height = 'auto';
|
| 308 |
try {
|
| 309 |
const res = await fetch(`${API_BASE}/api/ai/chat`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: fd });
|
|
@@ -336,13 +347,14 @@
|
|
| 336 |
} finally {
|
| 337 |
isProcessing = false;
|
| 338 |
selectedFile = null;
|
|
|
|
| 339 |
document.getElementById('file-input').value = '';
|
| 340 |
document.querySelector('button[onclick*="file-input"]').innerHTML = '<i class="fas fa-plus"></i>';
|
| 341 |
document.getElementById('send-btn').innerHTML = '<i class="fas fa-arrow-up text-sm"></i>';
|
| 342 |
-
}
|
| 343 |
}
|
| 344 |
|
| 345 |
-
function appendMessage(role, text, model = '') {
|
| 346 |
const div = document.createElement('div');
|
| 347 |
div.className = `msg-node ${role}`;
|
| 348 |
div.innerHTML = `
|
|
@@ -350,7 +362,10 @@
|
|
| 350 |
<div class="flex items-center gap-3"><div class="status-dot"></div> ${role === 'user' ? 'Architect In' : 'Titan Out'}</div>
|
| 351 |
<div class="hud-id">${model || 'MAIN CORE'}</div>
|
| 352 |
</div>
|
| 353 |
-
<div class="bubble"
|
|
|
|
|
|
|
|
|
|
| 354 |
<div class="msg-toolkit">
|
| 355 |
<div class="tool-btn" onclick="copyText(this)"><i class="far fa-copy"></i> Copy</div>
|
| 356 |
${role === 'ai' ? `<div class="tool-btn" onclick="window.location.reload()"><i class="fas fa-redo"></i> Redo</div>` : ''}
|
|
@@ -403,4 +418,4 @@
|
|
| 403 |
function logout() { localStorage.removeItem('token'); window.location.href = '/auth'; }
|
| 404 |
</script>
|
| 405 |
</body>
|
| 406 |
-
</html>
|
|
|
|
| 210 |
|
| 211 |
let currentSessionId = null, selectedFile = null, isProcessing = false;
|
| 212 |
const chatWindow = document.getElementById('chat-window'), input = document.getElementById('user-input');
|
| 213 |
+
let localFilePreview = null;
|
| 214 |
|
| 215 |
function handleFile(input) {
|
| 216 |
if (input.files && input.files[0]) {
|
| 217 |
selectedFile = input.files[0];
|
| 218 |
const plusBtn = document.querySelector('button[onclick*="file-input"]');
|
| 219 |
+
|
| 220 |
+
// Show local image preview instead of checkmark
|
| 221 |
+
if (selectedFile.type.startsWith('image/')) {
|
| 222 |
+
localFilePreview = URL.createObjectURL(selectedFile);
|
| 223 |
+
plusBtn.innerHTML = `<img src="${localFilePreview}" class="w-8 h-8 rounded-lg object-cover border border-white/20">`;
|
| 224 |
+
} else {
|
| 225 |
+
plusBtn.innerHTML = '<i class="fas fa-check text-green-500"></i>';
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
console.log(`[FILE_LOADED] ${selectedFile.name}`);
|
| 229 |
}
|
| 230 |
}
|
|
|
|
| 299 |
currentSessionId = id; chatWindow.innerHTML = ''; toggleMenu();
|
| 300 |
const res = await fetch(`${API_BASE}/api/ai/sessions/${id}/messages`, { headers: { 'Authorization': `Bearer ${token}` } });
|
| 301 |
const data = await res.json();
|
| 302 |
+
if (data.success) data.data.forEach(m => appendMessage(m.sender === 'user' ? 'user' : 'ai', m.content, m.modelUsed, m.attachmentUrl));
|
| 303 |
loadHistory();
|
| 304 |
}
|
| 305 |
|
|
|
|
| 312 |
const fd = new FormData(); fd.append('message', message); fd.append('model', activeModel);
|
| 313 |
if (currentSessionId) fd.append('sessionId', currentSessionId);
|
| 314 |
if (selectedFile) fd.append('file', selectedFile);
|
| 315 |
+
|
| 316 |
+
appendMessage('user', message || "[SIGNAL]", '', localFilePreview);
|
| 317 |
+
|
| 318 |
input.value = ''; input.style.height = 'auto';
|
| 319 |
try {
|
| 320 |
const res = await fetch(`${API_BASE}/api/ai/chat`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: fd });
|
|
|
|
| 347 |
} finally {
|
| 348 |
isProcessing = false;
|
| 349 |
selectedFile = null;
|
| 350 |
+
localFilePreview = null;
|
| 351 |
document.getElementById('file-input').value = '';
|
| 352 |
document.querySelector('button[onclick*="file-input"]').innerHTML = '<i class="fas fa-plus"></i>';
|
| 353 |
document.getElementById('send-btn').innerHTML = '<i class="fas fa-arrow-up text-sm"></i>';
|
| 354 |
+
}
|
| 355 |
}
|
| 356 |
|
| 357 |
+
function appendMessage(role, text, model = '', attachmentUrl = '') {
|
| 358 |
const div = document.createElement('div');
|
| 359 |
div.className = `msg-node ${role}`;
|
| 360 |
div.innerHTML = `
|
|
|
|
| 362 |
<div class="flex items-center gap-3"><div class="status-dot"></div> ${role === 'user' ? 'Architect In' : 'Titan Out'}</div>
|
| 363 |
<div class="hud-id">${model || 'MAIN CORE'}</div>
|
| 364 |
</div>
|
| 365 |
+
<div class="bubble">
|
| 366 |
+
${attachmentUrl ? `<img src="${attachmentUrl}" class="max-w-full rounded-2xl mb-4 border border-white/10 shadow-2xl cursor-zoom-in" onclick="window.open(this.src)">` : ''}
|
| 367 |
+
<div class="prose max-w-none">${marked.parse(text)}</div>
|
| 368 |
+
</div>
|
| 369 |
<div class="msg-toolkit">
|
| 370 |
<div class="tool-btn" onclick="copyText(this)"><i class="far fa-copy"></i> Copy</div>
|
| 371 |
${role === 'ai' ? `<div class="tool-btn" onclick="window.location.reload()"><i class="fas fa-redo"></i> Redo</div>` : ''}
|
|
|
|
| 418 |
function logout() { localStorage.removeItem('token'); window.location.href = '/auth'; }
|
| 419 |
</script>
|
| 420 |
</body>
|
| 421 |
+
</html>
|
backend/routes/ai.js
CHANGED
|
@@ -10,7 +10,7 @@ const router = express.Router();
|
|
| 10 |
// Setup Multer for file uploads
|
| 11 |
const storage = multer.diskStorage({
|
| 12 |
destination: function (req, file, cb) {
|
| 13 |
-
const uploadPath = path.join(__dirname, '../uploads');
|
| 14 |
if (!fs.existsSync(uploadPath)) fs.mkdirSync(uploadPath, { recursive: true });
|
| 15 |
cb(null, uploadPath)
|
| 16 |
},
|
|
|
|
| 10 |
// Setup Multer for file uploads
|
| 11 |
const storage = multer.diskStorage({
|
| 12 |
destination: function (req, file, cb) {
|
| 13 |
+
const uploadPath = path.join(__dirname, '../public/uploads');
|
| 14 |
if (!fs.existsSync(uploadPath)) fs.mkdirSync(uploadPath, { recursive: true });
|
| 15 |
cb(null, uploadPath)
|
| 16 |
},
|
backend/server.js
CHANGED
|
@@ -25,9 +25,9 @@ restoreFromCloud();
|
|
| 25 |
// Periodic Cloud Sync (Every 30 minutes)
|
| 26 |
setInterval(syncToCloud, 30 * 60 * 1000);
|
| 27 |
|
| 28 |
-
// Ensure Uploads Directory exists
|
| 29 |
const fs = require('fs');
|
| 30 |
-
const uploadsDir = path.join(__dirname, 'uploads');
|
| 31 |
if (!fs.existsSync(uploadsDir)){
|
| 32 |
fs.mkdirSync(uploadsDir, { recursive: true });
|
| 33 |
}
|
|
|
|
| 25 |
// Periodic Cloud Sync (Every 30 minutes)
|
| 26 |
setInterval(syncToCloud, 30 * 60 * 1000);
|
| 27 |
|
| 28 |
+
// Ensure Uploads Directory exists inside public
|
| 29 |
const fs = require('fs');
|
| 30 |
+
const uploadsDir = path.join(__dirname, 'public', 'uploads');
|
| 31 |
if (!fs.existsSync(uploadsDir)){
|
| 32 |
fs.mkdirSync(uploadsDir, { recursive: true });
|
| 33 |
}
|