1
0

app.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. document.addEventListener('DOMContentLoaded', () => {
  2. const chatContainer = document.getElementById('chat-container');
  3. const userInput = document.getElementById('user-input');
  4. const sendBtn = document.getElementById('send-btn');
  5. const projectUpload = document.getElementById('project-upload');
  6. const fileNameDisplay = document.getElementById('file-name');
  7. // 新增元素
  8. const sessionList = document.getElementById('session-list');
  9. const newSessionBtn = document.getElementById('new-session-btn');
  10. const currentSessionTitle = document.getElementById('current-session-title');
  11. const userLevelSelect = document.getElementById('user-level');
  12. const historyList = document.getElementById('history-list');
  13. let currentSessionId = null;
  14. // 自动滚动到底部
  15. function scrollToBottom() {
  16. chatContainer.scrollTop = chatContainer.scrollHeight;
  17. }
  18. // 添加消息到聊天界面
  19. function addMessage(text, isUser = false, toolCalls = null) {
  20. const msgDiv = document.createElement('div');
  21. msgDiv.className = `message ${isUser ? 'user-message' : 'bot-message'}`;
  22. // 如果有工具调用信息,先渲染工具调用块
  23. if (toolCalls && toolCalls.length > 0) {
  24. const toolsContainer = document.createElement('div');
  25. toolsContainer.className = 'tool-calls-container';
  26. toolCalls.forEach(tc => {
  27. const tcDiv = document.createElement('div');
  28. tcDiv.className = 'tool-call-block';
  29. // 尝试格式化参数和结果
  30. let formattedArgs = tc.arguments;
  31. try {
  32. formattedArgs = JSON.stringify(JSON.parse(tc.arguments), null, 2);
  33. } catch (e) {}
  34. let formattedResult = tc.result;
  35. try {
  36. formattedResult = JSON.stringify(JSON.parse(tc.result), null, 2);
  37. } catch (e) {}
  38. tcDiv.innerHTML = `
  39. <div class="tool-call-header">
  40. <span class="tool-icon">🛠️</span>
  41. <span class="tool-name">调用工具: <strong>${tc.name}</strong></span>
  42. </div>
  43. <div class="tool-call-details">
  44. <div class="tool-args">
  45. <div class="tool-label">输入参数:</div>
  46. <pre><code>${formattedArgs}</code></pre>
  47. </div>
  48. <div class="tool-result">
  49. <div class="tool-label">执行结果:</div>
  50. <pre><code>${formattedResult || '无返回结果'}</code></pre>
  51. </div>
  52. </div>
  53. `;
  54. toolsContainer.appendChild(tcDiv);
  55. });
  56. msgDiv.appendChild(toolsContainer);
  57. }
  58. // 简单处理 markdown 换行
  59. const textDiv = document.createElement('div');
  60. textDiv.className = 'message-text';
  61. const formattedText = text.replace(/\n/g, '<br>');
  62. textDiv.innerHTML = formattedText;
  63. msgDiv.appendChild(textDiv);
  64. chatContainer.appendChild(msgDiv);
  65. scrollToBottom();
  66. }
  67. // 显示加载状态
  68. function showLoading() {
  69. const loadingDiv = document.createElement('div');
  70. loadingDiv.className = 'message loading';
  71. loadingDiv.id = 'loading-msg';
  72. loadingDiv.textContent = '助手正在思考...';
  73. chatContainer.appendChild(loadingDiv);
  74. scrollToBottom();
  75. }
  76. // 移除加载状态
  77. function removeLoading() {
  78. const loadingDiv = document.getElementById('loading-msg');
  79. if (loadingDiv) {
  80. loadingDiv.remove();
  81. }
  82. }
  83. // 加载会话列表
  84. async function loadSessions() {
  85. try {
  86. const response = await fetch('/api/sessions');
  87. const data = await response.json();
  88. sessionList.innerHTML = '';
  89. data.sessions.forEach(session => {
  90. const div = document.createElement('div');
  91. div.className = `session-item ${session.id === currentSessionId ? 'active' : ''}`;
  92. const titleSpan = document.createElement('span');
  93. titleSpan.textContent = session.title;
  94. titleSpan.className = 'session-title';
  95. const deleteBtn = document.createElement('button');
  96. deleteBtn.innerHTML = '🗑️';
  97. deleteBtn.className = 'delete-session-btn';
  98. deleteBtn.title = '删除会话';
  99. deleteBtn.onclick = (e) => {
  100. e.stopPropagation(); // 阻止触发切换会话
  101. deleteSession(session.id, div);
  102. };
  103. div.appendChild(titleSpan);
  104. div.appendChild(deleteBtn);
  105. div.onclick = () => switchSession(session.id, session.title);
  106. sessionList.appendChild(div);
  107. });
  108. } catch (error) {
  109. console.error('加载会话列表失败:', error);
  110. }
  111. }
  112. // 删除会话
  113. async function deleteSession(sessionId, element) {
  114. if (!confirm('确定要删除这个会话吗?删除后无法恢复。')) return;
  115. try {
  116. const response = await fetch(`/api/sessions/${sessionId}`, {
  117. method: 'DELETE'
  118. });
  119. if (response.ok) {
  120. // 添加淡出动画
  121. element.style.opacity = '0';
  122. setTimeout(() => {
  123. element.remove();
  124. // 如果删除的是当前会话,新建一个会话
  125. if (sessionId === currentSessionId) {
  126. createNewSession();
  127. }
  128. }, 300);
  129. } else {
  130. alert('删除失败');
  131. }
  132. } catch (error) {
  133. console.error('删除会话失败:', error);
  134. alert('网络错误,删除失败');
  135. }
  136. }
  137. // 切换会话
  138. async function switchSession(sessionId, title) {
  139. currentSessionId = sessionId;
  140. currentSessionTitle.textContent = title;
  141. chatContainer.innerHTML = '';
  142. // 更新侧边栏高亮
  143. document.querySelectorAll('.session-item').forEach(item => {
  144. item.classList.remove('active');
  145. if (item.textContent === title) {
  146. item.classList.add('active');
  147. }
  148. });
  149. try {
  150. const response = await fetch(`/api/sessions/${sessionId}`);
  151. const data = await response.json();
  152. if (data.messages && data.messages.length > 0) {
  153. data.messages.forEach(msg => {
  154. addMessage(msg.text, msg.isUser, msg.tool_calls);
  155. });
  156. } else {
  157. addMessage('你好!我是你的软件开发学习助手。我可以根据你的水平出题,或者帮你测试代码。请问有什么我可以帮你的?');
  158. }
  159. } catch (error) {
  160. console.error('加载会话历史失败:', error);
  161. addMessage('加载历史记录失败');
  162. }
  163. }
  164. // 新建会话 (仅前端状态)
  165. function createNewSession() {
  166. currentSessionId = null;
  167. currentSessionTitle.textContent = '👨‍💻 SoftwareDevHelper';
  168. chatContainer.innerHTML = '';
  169. document.querySelectorAll('.session-item').forEach(item => item.classList.remove('active'));
  170. addMessage('你好!我是你的软件开发学习助手。我可以根据你的水平出题,提供开发建议,并测试你的代码。你想从哪里开始?');
  171. }
  172. // 发送文本消息
  173. async function sendMessage() {
  174. const text = userInput.value.trim();
  175. if (!text) return;
  176. userInput.value = '';
  177. addMessage(text, true);
  178. showLoading();
  179. sendBtn.disabled = true;
  180. try {
  181. const response = await fetch('/api/chat', {
  182. method: 'POST',
  183. headers: {
  184. 'Content-Type': 'application/json'
  185. },
  186. body: JSON.stringify({
  187. message: text,
  188. session_id: currentSessionId || ''
  189. })
  190. });
  191. const data = await response.json();
  192. removeLoading();
  193. if (response.ok) {
  194. addMessage(data.response, false, data.tool_calls);
  195. // 如果是新会话,更新当前 session_id 并刷新列表
  196. if (!currentSessionId) {
  197. currentSessionId = data.session_id;
  198. currentSessionTitle.textContent = text.substring(0, 15) + (text.length > 15 ? '...' : '');
  199. }
  200. loadSessions();
  201. loadUserMemory(); // 聊天后可能更新了记忆
  202. } else {
  203. addMessage(`错误: ${data.detail || '请求失败'}`);
  204. }
  205. } catch (error) {
  206. removeLoading();
  207. addMessage(`网络错误: ${error.message}`);
  208. } finally {
  209. sendBtn.disabled = false;
  210. userInput.focus();
  211. }
  212. }
  213. // 处理文件上传
  214. async function handleFileUpload(event) {
  215. const file = event.target.files[0];
  216. if (!file) return;
  217. fileNameDisplay.textContent = file.name;
  218. const formData = new FormData();
  219. formData.append('file', file);
  220. formData.append('session_id', currentSessionId || '');
  221. addMessage(`[上传项目] ${file.name}`, true);
  222. showLoading();
  223. projectUpload.value = '';
  224. try {
  225. const response = await fetch('/api/upload_project', {
  226. method: 'POST',
  227. body: formData
  228. });
  229. const data = await response.json();
  230. removeLoading();
  231. if (response.ok) {
  232. addMessage(data.response, false, data.tool_calls);
  233. if (!currentSessionId) {
  234. currentSessionId = data.session_id;
  235. currentSessionTitle.textContent = "上传项目测试";
  236. }
  237. loadSessions();
  238. loadUserMemory();
  239. } else {
  240. addMessage(`上传失败: ${data.detail || '未知错误'}`);
  241. }
  242. } catch (error) {
  243. removeLoading();
  244. addMessage(`上传出错: ${error.message}`);
  245. } finally {
  246. fileNameDisplay.textContent = '未选择文件';
  247. }
  248. }
  249. // 加载用户记忆
  250. async function loadUserMemory() {
  251. try {
  252. const response = await fetch('/api/user_memory');
  253. const data = await response.json();
  254. userLevelSelect.value = data.level || 'beginner';
  255. historyList.innerHTML = '';
  256. if (data.history && data.history.length > 0) {
  257. data.history.forEach(record => {
  258. const li = document.createElement('li');
  259. li.textContent = record;
  260. historyList.appendChild(li);
  261. });
  262. } else {
  263. const li = document.createElement('li');
  264. li.textContent = '暂无做题记录';
  265. li.style.color = '#999';
  266. historyList.appendChild(li);
  267. }
  268. } catch (error) {
  269. console.error('加载用户记忆失败:', error);
  270. }
  271. }
  272. // 更新用户水平
  273. async function updateUserLevel() {
  274. const newLevel = userLevelSelect.value;
  275. try {
  276. await fetch('/api/user_memory/level', {
  277. method: 'POST',
  278. headers: {
  279. 'Content-Type': 'application/json'
  280. },
  281. body: JSON.stringify({ level: newLevel })
  282. });
  283. } catch (error) {
  284. console.error('更新用户水平失败:', error);
  285. }
  286. }
  287. // 清空用户记忆记录
  288. async function resetUserMemory() {
  289. if (confirm('确定要清空所有的做题记录吗?这会将你的水平重置为入门(Beginner)。')) {
  290. try {
  291. await fetch('/api/user_memory', {
  292. method: 'DELETE'
  293. });
  294. loadUserMemory();
  295. addMessage('您的做题记录已清空,水平已重置。', false);
  296. } catch (error) {
  297. console.error('清空记录失败:', error);
  298. }
  299. }
  300. }
  301. // 事件绑定
  302. sendBtn.addEventListener('click', sendMessage);
  303. userInput.addEventListener('keypress', (e) => {
  304. if (e.key === 'Enter' && !e.shiftKey) {
  305. e.preventDefault();
  306. sendMessage();
  307. }
  308. });
  309. projectUpload.addEventListener('change', handleFileUpload);
  310. newSessionBtn.addEventListener('click', createNewSession);
  311. userLevelSelect.addEventListener('change', updateUserLevel);
  312. const resetMemoryBtn = document.getElementById('reset-memory-btn');
  313. if (resetMemoryBtn) {
  314. resetMemoryBtn.addEventListener('click', resetUserMemory);
  315. }
  316. // 初始化
  317. loadSessions();
  318. loadUserMemory();
  319. createNewSession();
  320. });