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

Feature: Save Feedback to Postgres

Browse files
Files changed (1) hide show
  1. src/server.py +29 -175
src/server.py CHANGED
@@ -11,6 +11,7 @@ import re
11
  from fastapi import FastAPI, UploadFile, File
12
  from fastapi.responses import HTMLResponse
13
  from pydantic import BaseModel
 
14
  from src.core.engine import ModelEngine
15
  from src.core.memory import MemoryManager
16
  from src.core.saas_api import SaasAPI
@@ -47,185 +48,24 @@ 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
- @app.get("/", response_class=HTMLResponse)
51
- async def root():
52
- return """
53
  <!DOCTYPE html>
54
  <html lang="vi">
55
  <head>
56
- <meta charset="UTF-8">
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");
209
- }
210
- }
211
-
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(); }
224
- </script>
225
- </body>
226
- </html>
227
  """
228
 
 
 
 
 
229
  @app.post("/upload")
230
  async def upload_file(file: UploadFile = File(...)):
231
  file_ext = file.filename.split(".")[-1].lower()
@@ -243,10 +83,24 @@ async def upload_file(file: UploadFile = File(...)):
243
 
244
  @app.post("/feedback")
245
  async def save_feedback(req: FeedbackRequest):
246
- entry = { "prompt": req.prompt, "chosen": req.correction, "rejected": req.response, "timestamp": time.time() }
247
- with open("src/data/learning_queue.jsonl", "a", encoding="utf-8") as f:
248
- f.write(json.dumps(entry, ensure_ascii=False) + "\n")
249
- return {"status": "recorded"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
  @app.post("/chat")
252
  async def chat_endpoint(req: ChatRequest):
 
11
  from fastapi import FastAPI, UploadFile, File
12
  from fastapi.responses import HTMLResponse
13
  from pydantic import BaseModel
14
+ from sqlalchemy import text
15
  from src.core.engine import ModelEngine
16
  from src.core.memory import MemoryManager
17
  from src.core.saas_api import SaasAPI
 
48
  text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
49
  return text.replace("</think>", "").replace("<think>", "").strip()
50
 
51
+ # --- HTML CONTENT (Minified for brevity, same as before) ---
52
+ html_content = """
 
53
  <!DOCTYPE html>
54
  <html lang="vi">
55
  <head>
56
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Project A</title><script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script><link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"><style>:root{--primary:#0084ff;--bg:#ffffff;--chat-bg:#f0f2f5;--user-msg:#0084ff;--ai-msg:#e4e6eb}body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:var(--bg);display:flex;justify-content:center;height:100vh;margin:0}.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)}.header{padding:15px 20px;border-bottom:1px solid #eee;font-weight:700;font-size:18px;color:#333;display:flex;align-items:center;gap:10px}.status-dot{width:10px;height:10px;background:#31a24c;border-radius:50%}.messages{flex:1;padding:20px;overflow-y:auto;display:flex;flex-direction:column;gap:10px}.msg-row{display:flex;flex-direction:column;max-width:85%}.msg-row.user{align-self:flex-end;align-items:flex-end}.msg-row.ai{align-self:flex-start;align-items:flex-start}.msg{padding:10px 16px;border-radius:18px;font-size:15px;line-height:1.5}.msg.user{background:var(--user-msg);color:white;border-bottom-right-radius:4px}.msg.ai{background:var(--ai-msg);color:#050505;border-bottom-left-radius:4px}.attachment-area{display:flex;gap:10px;padding:10px 15px;background:#fff;overflow-x:auto;min-height:60px;display:none}.att-card{display:flex;align-items:center;gap:10px;background:#2b2d31;color:white;padding:8px 12px;border-radius:8px;font-size:13px;min-width:150px;box-shadow:0 2px 5px rgba(0,0,0,0.2)}.att-thumb{width:30px;height:30px;border-radius:4px;object-fit:cover;background:#444}.att-info{display:flex;flex-direction:column;flex:1;overflow:hidden}.att-name{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.att-size{font-size:10px;color:#aaa}.att-close{cursor:pointer;color:#aaa;padding:5px}.att-close:hover{color:white}.input-wrapper{padding:15px;border-top:1px solid #eee;background:#fff}.input-bar{display:flex;align-items:center;gap:10px;background:#f0f2f5;padding:8px 12px;border-radius:25px}.attach-btn{background:none;border:none;cursor:pointer;color:#65676b;font-size:20px}.attach-btn:hover{color:var(--primary)}input[type="text"]{flex:1;background:transparent;border:none;outline:none;font-size:15px}.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}#typing{font-size:12px;color:#888;margin-left:20px;margin-bottom:5px;display:none}.feedback-actions{display:flex;gap:10px;margin-top:5px;margin-left:5px;font-size:12px;color:#65676b;opacity:0;transition:opacity 0.2s}.msg-row.ai:hover .feedback-actions{opacity:1}.fb-btn{cursor:pointer}.fb-btn:hover{color:var(--primary)}.correction-box{display:none;margin-top:5px;width:100%}.correction-box input{width:100%;padding:5px;border:1px solid #ddd;border-radius:5px;font-size:12px}</style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  </head>
58
  <body>
59
+ <div class="chat-widget"><div class="header"><div class="status-dot"></div> Project A</div><div class="messages" id="messages"></div><div id="typing">Project A đang suy nghĩ...</div><div id="attachment-area" class="attachment-area"></div><div class="input-wrapper"><div class="input-bar"><input type="file" id="fileInput" accept="image/*" style="display:none;" onchange="handleFileSelect()"><button class="attach-btn" onclick="document.getElementById('fileInput').click()"><i class="fa-solid fa-paperclip"></i></button><input type="text" id="input" placeholder="Nhập tin nhắn..." onkeypress="handleEnter(event)"><button class="send-btn" onclick="sendMessage()"><i class="fa-solid fa-paper-plane"></i></button></div></div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  <script>
61
+ const API_URL="";let currentFile=null;function handleFileSelect(){const e=document.getElementById("fileInput").files[0];e&&(currentFile=e,(new FileReader).onload=function(t){showAttachmentCard(e.name,t.target.result)},(new FileReader).readAsDataURL(e))}function showAttachmentCard(e,t){const n=document.getElementById("attachment-area");n.style.display="flex",n.innerHTML=`<div class="att-card"><img src="${t}" class="att-thumb"><div class="att-info"><div class="att-name">${e}</div><div class="att-size">Ready to upload</div></div><div class="att-close" onclick="clearFile()">✕</div></div>`}function clearFile(){currentFile=null,document.getElementById("fileInput").value="",document.getElementById("attachment-area").style.display="none",document.getElementById("attachment-area").innerHTML=""}async function sendMessage(){const e=document.getElementById("input"),t=e.value.trim();if(!t&&!currentFile)return;t&&addMessage(t,"user"),currentFile&&addMessage(`📎 Đã gửi ảnh: ${currentFile.name}`,"user"),e.value="",clearFile(),document.getElementById("typing").style.display="block";try{if(currentFile){const e=new FormData;e.append("file",currentFile),await fetch(`${API_URL}/upload`,{method:"POST",body:e}),clearFile()}const n=t||"Hãy mô tả ảnh tôi vừa gửi.",a=await fetch(`${API_URL}/chat`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({user_id:1,store_id:1,message:n})}),s=await a.json();document.getElementById("typing").style.display="none",addAiMessage(s.response,n)}catch(e){document.getElementById("typing").style.display="none",addMessage("Lỗi: "+e.message,"ai")}}function addMessage(e,t){const n=document.createElement("div");n.className=`msg-row ${t}`;const a=document.createElement("div");a.className=`msg ${t}`,a.innerHTML=marked.parse(e),n.appendChild(a),document.getElementById("messages").appendChild(n),document.getElementById("messages").scrollTop=document.getElementById("messages").scrollHeight}function addAiMessage(e,t){const n=document.createElement("div");n.className="msg-row ai";const a=document.createElement("div");a.className="msg ai",a.innerHTML=marked.parse(e);const s=document.createElement("div");s.className="feedback-actions",s.innerHTML=`<span class="fb-btn" onclick="sendFeedback(this, 'up')">👍</span><span class="fb-btn" onclick="showCorrection(this)">👎</span>`;const i=document.createElement("div");i.className="correction-box",i.innerHTML=`<input placeholder="Góp ý sửa lỗi..." onkeypress="if(event.key==='Enter') submitCorrection('${t}', '${e.replace(/'/g,"\'").replace(/\n/g," ")}', this)">`,n.appendChild(a),n.appendChild(s),n.appendChild(i),document.getElementById("messages").appendChild(n),document.getElementById("messages").scrollTop=document.getElementById("messages").scrollHeight}function showCorrection(e){e.parentElement.nextElementSibling.style.display="block"}function sendFeedback(e,t){e.parentElement.innerHTML="<span style='color:green'>✓ Đã ghi nhận</span>"}async function submitCorrection(e,t,n){const a=n.value;n.parentElement.innerHTML="<span style='color:green; font-size:12px'>✓ Cảm ơn bạn!</span>",await fetch(`${API_URL}/feedback`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt:e,response:t,feedback:"down",correction:a})})}function handleEnter(e){"Enter"===e.key&&sendMessage()}
62
+ </script></body></html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  """
64
 
65
+ @app.get("/", response_class=HTMLResponse)
66
+ async def root():
67
+ return html_content
68
+
69
  @app.post("/upload")
70
  async def upload_file(file: UploadFile = File(...)):
71
  file_ext = file.filename.split(".")[-1].lower()
 
83
 
84
  @app.post("/feedback")
85
  async def save_feedback(req: FeedbackRequest):
86
+ # --- UPDATED: Save to Neon DB ---
87
+ try:
88
+ with memory.get_conn() as conn:
89
+ conn.execute(text("""
90
+ INSERT INTO feedback_logs (user_id, prompt, ai_response, user_correction, feedback_type)
91
+ VALUES (:uid, :p, :r, :c, :f)
92
+ """), {
93
+ "uid": 1, # Default user for now
94
+ "p": req.prompt,
95
+ "r": req.rejected, # 'rejected' matches 'response' in request
96
+ "c": req.correction,
97
+ "f": req.feedback
98
+ })
99
+ conn.commit()
100
+ return {"status": "recorded_in_db"}
101
+ except Exception as e:
102
+ print(f"Feedback Error: {e}")
103
+ return {"status": "error", "message": str(e)}
104
 
105
  @app.post("/chat")
106
  async def chat_endpoint(req: ChatRequest):