sonthaiha commited on
Commit
f73c3de
·
verified ·
1 Parent(s): a0b166b

Fix: Context Amnesia & UI Cards

Browse files
Files changed (2) hide show
  1. src/core/memory.py +37 -23
  2. src/server.py +91 -141
src/core/memory.py CHANGED
@@ -14,33 +14,29 @@ class MemoryManager:
14
 
15
  def get_conn(self): return self.engine.connect()
16
 
17
- def get_user_workspaces(self, user_id):
18
- if not self.engine: return [{"id": 1, "name": "Offline Store"}]
19
- try:
20
- with self.get_conn() as conn:
21
- rows = conn.execute(text("SELECT id, name, type FROM workspaces WHERE user_id = :uid"), {"uid": str(user_id)}).fetchall()
22
- if not rows: return [{"id": 1, "name": "Default Store"}]
23
- return [{"id": r[0], "name": r[1]} for r in rows]
24
- except: return [{"id": 1, "name": "Default Store"}]
25
-
26
- def _get_or_create_session(self, conn, user_id, workspace_id):
27
- row = conn.execute(text("SELECT id FROM chat_sessions WHERE user_id = :uid ORDER BY last_active DESC LIMIT 1"), {"uid": str(user_id)}).fetchone()
28
- if row: return row[0]
29
- res = conn.execute(text("INSERT INTO chat_sessions (user_id, workspace_id, title) VALUES (:uid, :wid, 'New Chat') RETURNING id"), {"uid": str(user_id), "wid": str(workspace_id)}).fetchone()
30
- return res[0]
31
-
32
  def save_attachment(self, user_id, workspace_id, filename, filetype, analysis):
33
  try:
34
  with self.get_conn() as conn:
35
- sid = self._get_or_create_session(conn, user_id, workspace_id)
 
 
 
 
 
 
 
36
  conn.execute(text("INSERT INTO chat_attachments (session_id, file_name, file_type, analysis_summary) VALUES (:sid, :f, :t, :a)"), {"sid": sid, "f": filename, "t": filetype, "a": analysis})
 
37
  conn.commit()
38
- except: pass
39
 
40
  def add_message(self, user_id, workspace_id, role, content):
41
  try:
42
  with self.get_conn() as conn:
43
- sid = self._get_or_create_session(conn, user_id, workspace_id)
 
 
 
44
  conn.execute(text("INSERT INTO chat_messages (session_id, role, content) VALUES (:sid, :role, :content)"), {"sid": sid, "role": role, "content": str(content)})
45
  conn.commit()
46
  except: pass
@@ -48,19 +44,37 @@ class MemoryManager:
48
  def get_context_string(self, user_id, limit=6):
49
  try:
50
  with self.get_conn() as conn:
 
51
  rows = conn.execute(text("SELECT m.role, m.content FROM chat_messages m JOIN chat_sessions s ON m.session_id = s.id WHERE s.user_id = :uid ORDER BY m.created_at DESC LIMIT :lim"), {"uid": str(user_id), "lim": limit}).fetchall()
52
  history = "\n".join([f"{r[0]}: {r[1]}" for r in reversed(rows)])
53
 
54
- att_rows = conn.execute(text("SELECT a.file_name, a.analysis_summary FROM chat_attachments a JOIN chat_sessions s ON a.session_id = s.id WHERE s.user_id = :uid ORDER BY s.last_active DESC LIMIT 3"), {"uid": str(user_id)}).fetchall()
55
- vision = ""
 
 
 
56
  if att_rows:
57
- vision = "\n[VISUAL CONTEXT]:\n" + "\n".join([f"- {r[0]}: {r[1]}" for r in att_rows])
58
- return vision + history
59
- except: return ""
 
 
 
 
 
 
 
 
 
 
60
 
61
  def save_workflow(self, workspace_id, name, json_data):
 
62
  with self.get_conn() as conn:
63
  conn.execute(text("INSERT INTO scenarios (workspace_id, name, description, steps, status, created_at) VALUES (:wid, :name, 'AI Generated', :steps, 'active', :time)"),
64
  {"wid": workspace_id, "name": name, "steps": json.dumps(json_data), "time": datetime.now().isoformat()})
65
  conn.commit()
66
  return 1
 
 
 
 
14
 
15
  def get_conn(self): return self.engine.connect()
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  def save_attachment(self, user_id, workspace_id, filename, filetype, analysis):
18
  try:
19
  with self.get_conn() as conn:
20
+ # Get or Create Session
21
+ row = conn.execute(text("SELECT id FROM chat_sessions WHERE user_id = :uid ORDER BY last_active DESC LIMIT 1"), {"uid": str(user_id)}).fetchone()
22
+ if row:
23
+ sid = row[0]
24
+ else:
25
+ res = conn.execute(text("INSERT INTO chat_sessions (user_id, workspace_id, title) VALUES (:uid, :wid, 'New Chat') RETURNING id"), {"uid": str(user_id), "wid": str(workspace_id)}).fetchone()
26
+ sid = res[0]
27
+
28
  conn.execute(text("INSERT INTO chat_attachments (session_id, file_name, file_type, analysis_summary) VALUES (:sid, :f, :t, :a)"), {"sid": sid, "f": filename, "t": filetype, "a": analysis})
29
+ conn.execute(text("UPDATE chat_sessions SET last_active = CURRENT_TIMESTAMP WHERE id = :sid"), {"sid": sid})
30
  conn.commit()
31
+ except Exception as e: print(f"DB Error: {e}")
32
 
33
  def add_message(self, user_id, workspace_id, role, content):
34
  try:
35
  with self.get_conn() as conn:
36
+ # Simple lookup for demo
37
+ sid_res = conn.execute(text("SELECT id FROM chat_sessions WHERE user_id = :uid ORDER BY last_active DESC LIMIT 1"), {"uid": str(user_id)}).fetchone()
38
+ sid = sid_res[0] if sid_res else 1
39
+
40
  conn.execute(text("INSERT INTO chat_messages (session_id, role, content) VALUES (:sid, :role, :content)"), {"sid": sid, "role": role, "content": str(content)})
41
  conn.commit()
42
  except: pass
 
44
  def get_context_string(self, user_id, limit=6):
45
  try:
46
  with self.get_conn() as conn:
47
+ # 1. Get Messages
48
  rows = conn.execute(text("SELECT m.role, m.content FROM chat_messages m JOIN chat_sessions s ON m.session_id = s.id WHERE s.user_id = :uid ORDER BY m.created_at DESC LIMIT :lim"), {"uid": str(user_id), "lim": limit}).fetchall()
49
  history = "\n".join([f"{r[0]}: {r[1]}" for r in reversed(rows)])
50
 
51
+ # 2. Get Attachments (SMARTER LOGIC)
52
+ # We fetch the *Latest* one separately to mark it as CURRENT
53
+ att_rows = conn.execute(text("SELECT a.file_name, a.analysis_summary, a.created_at FROM chat_attachments a JOIN chat_sessions s ON a.session_id = s.id WHERE s.user_id = :uid ORDER BY a.created_at DESC LIMIT 3"), {"uid": str(user_id)}).fetchall()
54
+
55
+ vision_context = ""
56
  if att_rows:
57
+ # The first one is the latest
58
+ latest = att_rows[0]
59
+ vision_context += f"\n[CURRENTLY LOOKING AT IMAGE]:\nFile: {latest[0]}\nDescription: {latest[1]}\n"
60
+
61
+ # The rest are history
62
+ if len(att_rows) > 1:
63
+ vision_context += "\n[PREVIOUS IMAGES (Context only)]:\n"
64
+ for r in att_rows[1:]:
65
+ vision_context += f"- {r[0]}: {r[1]}\n"
66
+
67
+ return vision_context + "\n" + history
68
+ except Exception as e:
69
+ return f"Error: {e}"
70
 
71
  def save_workflow(self, workspace_id, name, json_data):
72
+ # (Same as before)
73
  with self.get_conn() as conn:
74
  conn.execute(text("INSERT INTO scenarios (workspace_id, name, description, steps, status, created_at) VALUES (:wid, :name, 'AI Generated', :steps, 'active', :time)"),
75
  {"wid": workspace_id, "name": name, "steps": json.dumps(json_data), "time": datetime.now().isoformat()})
76
  conn.commit()
77
  return 1
78
+
79
+ # Required for server.py imports
80
+ def get_user_workspaces(self, uid): return []
src/server.py CHANGED
@@ -47,8 +47,6 @@ def clean_output(text):
47
  text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
48
  return text.replace("</think>", "").replace("<think>", "").strip()
49
 
50
- # --- ROUTES ---
51
-
52
  @app.get("/", response_class=HTMLResponse)
53
  async def root():
54
  return """
@@ -59,142 +57,152 @@ async def root():
59
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
60
  <title>Project A</title>
61
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
 
62
  <style>
63
- :root { --primary: #0084ff; --bg: #ffffff; --chat-bg: #f0f2f5; --user-msg: #0084ff; --ai-msg: #e4e6eb; --text-ai: #050505; }
64
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: var(--bg); display: flex; justify-content: center; height: 100vh; margin: 0; }
65
-
66
- .chat-widget { width: 100%; max-width: 600px; height: 100vh; background: #fff; display: flex; flex-direction: column; box-shadow: 0 0 20px rgba(0,0,0,0.1); }
67
 
68
- /* Header */
69
  .header { padding: 15px 20px; border-bottom: 1px solid #eee; font-weight: 700; font-size: 18px; color: #333; display: flex; align-items: center; gap: 10px; }
70
  .status-dot { width: 10px; height: 10px; background: #31a24c; border-radius: 50%; }
71
 
72
- /* Messages */
73
- .messages { flex: 1; padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; background: #fff; }
74
  .msg-row { display: flex; flex-direction: column; max-width: 85%; }
75
  .msg-row.user { align-self: flex-end; align-items: flex-end; }
76
  .msg-row.ai { align-self: flex-start; align-items: flex-start; }
77
-
78
- .msg { padding: 10px 16px; border-radius: 18px; font-size: 15px; line-height: 1.5; word-wrap: break-word; }
79
  .msg.user { background: var(--user-msg); color: white; border-bottom-right-radius: 4px; }
80
- .msg.ai { background: var(--ai-msg); color: var(--text-ai); border-bottom-left-radius: 4px; }
81
- .msg p { margin: 0; }
82
-
83
- /* Feedback */
84
- .feedback-actions { display: flex; gap: 10px; margin-top: 5px; margin-left: 5px; font-size: 12px; color: #65676b; opacity: 0; transition: opacity 0.2s; }
85
- .msg-row.ai:hover .feedback-actions { opacity: 1; }
86
- .fb-btn { cursor: pointer; } .fb-btn:hover { color: var(--primary); }
87
- .correction-box { display: none; margin-top: 5px; width: 100%; }
88
- .correction-box input { width: 100%; padding: 5px; border: 1px solid #ddd; border-radius: 5px; font-size: 12px; }
89
 
90
- /* Input Area */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  .input-wrapper { padding: 15px; border-top: 1px solid #eee; background: #fff; }
92
- .file-preview { display: none; padding: 8px 12px; background: #e7f3ff; border-radius: 8px; margin-bottom: 10px; font-size: 13px; color: #0084ff; align-items: center; justify-content: space-between; }
93
- .remove-file { cursor: pointer; font-weight: bold; }
94
-
95
  .input-bar { display: flex; align-items: center; gap: 10px; background: #f0f2f5; padding: 8px 12px; border-radius: 25px; }
96
-
97
- .attach-btn { background: none; border: none; cursor: pointer; color: #65676b; padding: 5px; display: flex; align-items: center; }
98
  .attach-btn:hover { color: var(--primary); }
99
- .attach-btn svg { width: 24px; height: 24px; fill: currentColor; }
100
-
101
  input[type="text"] { flex: 1; background: transparent; border: none; outline: none; font-size: 15px; }
 
102
 
103
- .send-btn { background: var(--primary); color: white; border: none; width: 36px; height: 36px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: transform 0.1s; }
104
- .send-btn:active { transform: scale(0.95); }
105
- .send-btn svg { width: 18px; height: 18px; fill: white; margin-left: 2px; }
106
-
107
  #typing { font-size: 12px; color: #888; margin-left: 20px; margin-bottom: 5px; display: none; }
108
  </style>
109
  </head>
110
  <body>
111
 
112
  <div class="chat-widget">
113
- <div class="header">
114
- <div class="status-dot"></div> Project A
115
- </div>
116
 
117
- <div class="messages" id="messages">
118
- <!-- Chat starts empty -->
119
- </div>
120
-
121
- <div id="typing">Project A đang soạn tin...</div>
122
 
123
  <div class="input-wrapper">
124
- <div id="file-preview" class="file-preview">
125
- <span id="filename">image.png</span>
126
- <span class="remove-file" onclick="clearFile()">✕</span>
127
- </div>
128
-
129
  <div class="input-bar">
130
- <input type="file" id="fileInput" accept="image/*" style="display: none;" onchange="handleFileUpload()">
131
- <button class="attach-btn" onclick="document.getElementById('fileInput').click()" title="Đính kèm ảnh">
132
- <svg viewBox="0 0 24 24"><path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5a2.5 2.5 0 0 1 5 0v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5a2.5 2.5 0 0 0 5 0V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z"/></svg>
133
  </button>
134
-
135
  <input type="text" id="input" placeholder="Nhập tin nhắn..." onkeypress="handleEnter(event)">
136
-
137
- <button class="send-btn" onclick="sendMessage()">
138
- <svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path></svg>
139
- </button>
140
  </div>
141
  </div>
142
  </div>
143
 
144
  <script>
145
- const API_URL = ""; // Relative path for HF Space
 
146
 
147
- async function handleFileUpload() {
148
  const file = document.getElementById('fileInput').files[0];
149
  if (!file) return;
 
150
 
151
- const preview = document.getElementById("file-preview");
152
- const nameSpan = document.getElementById("filename");
153
-
154
- preview.style.display = "flex";
155
- nameSpan.innerText = "⏳ Đang tải: " + file.name;
156
-
157
- const formData = new FormData();
158
- formData.append("file", file);
159
 
160
- try {
161
- const res = await fetch(`${API_URL}/upload`, { method: "POST", body: formData });
162
- const data = await res.json();
163
- if (data.status === "success") {
164
- nameSpan.innerText = "📎 " + file.name;
165
- } else {
166
- nameSpan.innerText = "❌ Lỗi tải file";
167
- }
168
- } catch (e) {
169
- nameSpan.innerText = "❌ Lỗi kết nối";
170
- }
 
 
171
  }
172
 
173
  function clearFile() {
174
- document.getElementById("fileInput").value = "";
175
- document.getElementById("file-preview").style.display = "none";
 
 
176
  }
177
 
178
  async function sendMessage() {
179
  const input = document.getElementById("input");
180
  const text = input.value.trim();
181
- if (!text) return;
 
 
 
 
 
 
 
 
 
182
 
183
- addMessage(text, "user");
184
  input.value = "";
185
- clearFile(); // Visual clear, server remembers context
186
-
187
  document.getElementById("typing").style.display = "block";
188
 
189
  try {
 
 
 
 
 
 
 
 
 
 
 
 
190
  const res = await fetch(`${API_URL}/chat`, {
191
  method: "POST",
192
  headers: { "Content-Type": "application/json" },
193
- body: JSON.stringify({ user_id: 1, store_id: 1, message: text })
194
  });
 
195
  const data = await res.json();
196
  document.getElementById("typing").style.display = "none";
197
- addAiMessage(data.response, text);
 
198
  } catch (e) {
199
  document.getElementById("typing").style.display = "none";
200
  addMessage("Lỗi: " + e.message, "ai");
@@ -204,68 +212,12 @@ async def root():
204
  function addMessage(text, role) {
205
  const row = document.createElement("div");
206
  row.className = `msg-row ${role}`;
207
-
208
  const bubble = document.createElement("div");
209
  bubble.className = `msg ${role}`;
210
  bubble.innerHTML = marked.parse(text);
211
-
212
- row.appendChild(bubble);
213
- document.getElementById("messages").appendChild(row);
214
- scrollToBottom();
215
- }
216
-
217
- function addAiMessage(text, prompt) {
218
- const row = document.createElement("div");
219
- row.className = "msg-row ai";
220
-
221
- const bubble = document.createElement("div");
222
- bubble.className = "msg ai";
223
- bubble.innerHTML = marked.parse(text);
224
-
225
- const actions = document.createElement("div");
226
- actions.className = "feedback-actions";
227
- actions.innerHTML = `
228
- <span class="fb-btn" onclick="sendFeedback(this, 'up')">👍</span>
229
- <span class="fb-btn" onclick="showCorrection(this)">👎</span>
230
- `;
231
-
232
- const correction = document.createElement("div");
233
- correction.className = "correction-box";
234
- correction.innerHTML = `
235
- <input placeholder="Góp ý sửa lỗi..." onkeypress="if(event.key==='Enter') submitCorrection('${prompt}', '${text.replace(/'/g, "\'")}', this)">
236
- `;
237
-
238
  row.appendChild(bubble);
239
- row.appendChild(actions);
240
- row.appendChild(correction);
241
-
242
  document.getElementById("messages").appendChild(row);
243
- scrollToBottom();
244
- }
245
-
246
- function showCorrection(btn) {
247
- btn.parentElement.nextElementSibling.style.display = 'block';
248
- }
249
-
250
- function sendFeedback(btn, type) {
251
- btn.parentElement.innerHTML = "<span style='color:green'>✓ Đã ghi nhận</span>";
252
- // Call API if needed
253
- }
254
-
255
- async function submitCorrection(prompt, response, input) {
256
- const feedback = input.value;
257
- input.parentElement.innerHTML = "<span style='color:green; font-size:12px'>✓ Cảm ơn bạn!</span>";
258
-
259
- await fetch(`${API_URL}/feedback`, {
260
- method: "POST",
261
- headers: { "Content-Type": "application/json" },
262
- body: JSON.stringify({ prompt, response, feedback: "down", correction: feedback })
263
- });
264
- }
265
-
266
- function scrollToBottom() {
267
- const m = document.getElementById("messages");
268
- m.scrollTop = m.scrollHeight;
269
  }
270
 
271
  function handleEnter(e) { if (e.key === "Enter") sendMessage(); }
@@ -286,7 +238,6 @@ async def upload_file(file: UploadFile = File(...)):
286
  if file_ext in ['jpg', 'png', 'jpeg', 'webp']:
287
  analysis = vision.analyze_media(save_path)
288
 
289
- # Save to User 1 (Default)
290
  memory.save_attachment(1, 1, file.filename, file_ext, analysis)
291
  return {"status": "success", "vision_analysis": analysis}
292
 
@@ -305,7 +256,6 @@ async def chat_endpoint(req: ChatRequest):
305
  decision = manager.analyze_task(req.message, history)
306
  cat = decision.get("category", "GENERAL")
307
 
308
- # Vision Override
309
  if "ảnh" in req.message.lower() or "hình" in req.message.lower():
310
  cat = "GENERAL"
311
 
 
47
  text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
48
  return text.replace("</think>", "").replace("<think>", "").strip()
49
 
 
 
50
  @app.get("/", response_class=HTMLResponse)
51
  async def root():
52
  return """
 
57
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
58
  <title>Project A</title>
59
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
60
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
61
  <style>
62
+ :root { --primary: #0084ff; --bg: #ffffff; --chat-bg: #f0f2f5; --user-msg: #0084ff; --ai-msg: #e4e6eb; }
63
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: var(--bg); display: flex; justify-content: center; height: 100vh; margin: 0; }
 
 
64
 
65
+ .chat-widget { width: 100%; max-width: 700px; height: 100vh; background: #fff; display: flex; flex-direction: column; box-shadow: 0 0 20px rgba(0,0,0,0.1); }
66
  .header { padding: 15px 20px; border-bottom: 1px solid #eee; font-weight: 700; font-size: 18px; color: #333; display: flex; align-items: center; gap: 10px; }
67
  .status-dot { width: 10px; height: 10px; background: #31a24c; border-radius: 50%; }
68
 
69
+ .messages { flex: 1; padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; }
 
70
  .msg-row { display: flex; flex-direction: column; max-width: 85%; }
71
  .msg-row.user { align-self: flex-end; align-items: flex-end; }
72
  .msg-row.ai { align-self: flex-start; align-items: flex-start; }
73
+ .msg { padding: 10px 16px; border-radius: 18px; font-size: 15px; line-height: 1.5; }
 
74
  .msg.user { background: var(--user-msg); color: white; border-bottom-right-radius: 4px; }
75
+ .msg.ai { background: var(--ai-msg); color: #050505; border-bottom-left-radius: 4px; }
 
 
 
 
 
 
 
 
76
 
77
+ /* --- NEW ATTACHMENT CARDS --- */
78
+ .attachment-area { display: flex; gap: 10px; padding: 10px 15px; background: #fff; overflow-x: auto; min-height: 60px; display: none; }
79
+
80
+ .att-card {
81
+ display: flex; align-items: center; gap: 10px;
82
+ background: #2b2d31; color: white;
83
+ padding: 8px 12px; border-radius: 8px;
84
+ font-size: 13px; min-width: 150px;
85
+ box-shadow: 0 2px 5px rgba(0,0,0,0.2);
86
+ }
87
+
88
+ .att-thumb { width: 30px; height: 30px; border-radius: 4px; object-fit: cover; background: #444; }
89
+ .att-info { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
90
+ .att-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
91
+ .att-size { font-size: 10px; color: #aaa; }
92
+ .att-close { cursor: pointer; color: #aaa; padding: 5px; }
93
+ .att-close:hover { color: white; }
94
+
95
+ /* Input Bar */
96
  .input-wrapper { padding: 15px; border-top: 1px solid #eee; background: #fff; }
 
 
 
97
  .input-bar { display: flex; align-items: center; gap: 10px; background: #f0f2f5; padding: 8px 12px; border-radius: 25px; }
98
+ .attach-btn { background: none; border: none; cursor: pointer; color: #65676b; font-size: 20px; }
 
99
  .attach-btn:hover { color: var(--primary); }
 
 
100
  input[type="text"] { flex: 1; background: transparent; border: none; outline: none; font-size: 15px; }
101
+ .send-btn { background: var(--primary); color: white; border: none; width: 36px; height: 36px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; }
102
 
 
 
 
 
103
  #typing { font-size: 12px; color: #888; margin-left: 20px; margin-bottom: 5px; display: none; }
104
  </style>
105
  </head>
106
  <body>
107
 
108
  <div class="chat-widget">
109
+ <div class="header"><div class="status-dot"></div> Project A</div>
110
+ <div class="messages" id="messages"></div>
111
+ <div id="typing">Project A đang suy nghĩ...</div>
112
 
113
+ <!-- ATTACHMENT PREVIEW AREA -->
114
+ <div id="attachment-area" class="attachment-area"></div>
 
 
 
115
 
116
  <div class="input-wrapper">
 
 
 
 
 
117
  <div class="input-bar">
118
+ <input type="file" id="fileInput" accept="image/*" style="display: none;" onchange="handleFileSelect()">
119
+ <button class="attach-btn" onclick="document.getElementById('fileInput').click()">
120
+ <i class="fa-solid fa-paperclip"></i>
121
  </button>
 
122
  <input type="text" id="input" placeholder="Nhập tin nhắn..." onkeypress="handleEnter(event)">
123
+ <button class="send-btn" onclick="sendMessage()"><i class="fa-solid fa-paper-plane"></i></button>
 
 
 
124
  </div>
125
  </div>
126
  </div>
127
 
128
  <script>
129
+ const API_URL = "";
130
+ let currentFile = null;
131
 
132
+ function handleFileSelect() {
133
  const file = document.getElementById('fileInput').files[0];
134
  if (!file) return;
135
+ currentFile = file;
136
 
137
+ const reader = new FileReader();
138
+ reader.onload = function(e) {
139
+ showAttachmentCard(file.name, e.target.result);
140
+ };
141
+ reader.readAsDataURL(file);
142
+ }
 
 
143
 
144
+ function showAttachmentCard(name, src) {
145
+ const area = document.getElementById('attachment-area');
146
+ area.style.display = 'flex';
147
+ area.innerHTML = `
148
+ <div class="att-card">
149
+ <img src="${src}" class="att-thumb">
150
+ <div class="att-info">
151
+ <div class="att-name">${name}</div>
152
+ <div class="att-size">Ready to upload</div>
153
+ </div>
154
+ <div class="att-close" onclick="clearFile()">✕</div>
155
+ </div>
156
+ `;
157
  }
158
 
159
  function clearFile() {
160
+ currentFile = null;
161
+ document.getElementById('fileInput').value = "";
162
+ document.getElementById('attachment-area').style.display = 'none';
163
+ document.getElementById('attachment-area').innerHTML = "";
164
  }
165
 
166
  async function sendMessage() {
167
  const input = document.getElementById("input");
168
  const text = input.value.trim();
169
+
170
+ // Don't send if empty AND no file
171
+ if (!text && !currentFile) return;
172
+
173
+ // 1. UI Update
174
+ if (text) addMessage(text, "user");
175
+ if (currentFile) {
176
+ // Show a mini "uploaded" message for the file
177
+ addMessage(`📎 Đã gửi ảnh: ${currentFile.name}`, "user");
178
+ }
179
 
 
180
  input.value = "";
 
 
181
  document.getElementById("typing").style.display = "block";
182
 
183
  try {
184
+ // 2. Upload File First (If exists)
185
+ if (currentFile) {
186
+ const formData = new FormData();
187
+ formData.append("file", currentFile);
188
+ await fetch(`${API_URL}/upload`, { method: "POST", body: formData });
189
+ clearFile(); // Clear from UI after upload
190
+ }
191
+
192
+ // 3. Send Text
193
+ // If text was empty but file was sent, prompt AI to describe it
194
+ const msgToSend = text || "Hãy mô tả ảnh tôi vừa gửi.";
195
+
196
  const res = await fetch(`${API_URL}/chat`, {
197
  method: "POST",
198
  headers: { "Content-Type": "application/json" },
199
+ body: JSON.stringify({ user_id: 1, store_id: 1, message: msgToSend })
200
  });
201
+
202
  const data = await res.json();
203
  document.getElementById("typing").style.display = "none";
204
+ addMessage(data.response, "ai");
205
+
206
  } catch (e) {
207
  document.getElementById("typing").style.display = "none";
208
  addMessage("Lỗi: " + e.message, "ai");
 
212
  function addMessage(text, role) {
213
  const row = document.createElement("div");
214
  row.className = `msg-row ${role}`;
 
215
  const bubble = document.createElement("div");
216
  bubble.className = `msg ${role}`;
217
  bubble.innerHTML = marked.parse(text);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  row.appendChild(bubble);
 
 
 
219
  document.getElementById("messages").appendChild(row);
220
+ document.getElementById("messages").scrollTop = document.getElementById("messages").scrollHeight;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  }
222
 
223
  function handleEnter(e) { if (e.key === "Enter") sendMessage(); }
 
238
  if file_ext in ['jpg', 'png', 'jpeg', 'webp']:
239
  analysis = vision.analyze_media(save_path)
240
 
 
241
  memory.save_attachment(1, 1, file.filename, file_ext, analysis)
242
  return {"status": "success", "vision_analysis": analysis}
243
 
 
256
  decision = manager.analyze_task(req.message, history)
257
  cat = decision.get("category", "GENERAL")
258
 
 
259
  if "ảnh" in req.message.lower() or "hình" in req.message.lower():
260
  cat = "GENERAL"
261