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 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({ sessionId: session._id, sender: 'user', content: userContent });
 
 
 
 
 
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({ sessionId: session._id, sender: 'user', content: userContent });
 
 
 
 
 
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
- plusBtn.innerHTML = '<i class="fas fa-check text-green-500"></i>';
 
 
 
 
 
 
 
 
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
- appendMessage('user', message || "[SIGNAL]");
 
 
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"><div class="prose max-w-none">${marked.parse(text)}</div></div>
 
 
 
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
  }