SSE.html 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. <!DOCTYPE html>
  2. <html lang="zh">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>SSE 多轮对话演示</title>
  6. <style>
  7. body {
  8. background: #f6f8fa;
  9. font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", Arial, sans-serif;
  10. }
  11. #chatbox {
  12. width: 480px;
  13. margin: 40px auto;
  14. background: #fff;
  15. border-radius: 10px;
  16. box-shadow: 0 2px 12px #0001;
  17. padding: 24px 24px 12px 24px;
  18. }
  19. #history {
  20. min-height: 240px;
  21. max-height: 400px;
  22. overflow-y: auto;
  23. margin-bottom: 16px;
  24. padding-right: 8px;
  25. }
  26. .msg-row {
  27. display: flex;
  28. margin-bottom: 8px;
  29. }
  30. .msg-user {
  31. justify-content: flex-end;
  32. }
  33. .msg-ai {
  34. justify-content: flex-start;
  35. }
  36. .bubble {
  37. max-width: 70%;
  38. padding: 10px 16px;
  39. border-radius: 18px;
  40. font-size: 15px;
  41. line-height: 1.6;
  42. box-shadow: 0 1px 4px #0001;
  43. word-break: break-all;
  44. position: relative;
  45. }
  46. .bubble-user {
  47. background: linear-gradient(90deg, #6ec1e4 0%, #367be2 100%);
  48. color: #fff;
  49. border-bottom-right-radius: 4px;
  50. }
  51. .bubble-ai {
  52. background: #f0f1f5;
  53. color: #222;
  54. border-bottom-left-radius: 4px;
  55. }
  56. .bubble-label {
  57. font-size: 12px;
  58. color: #888;
  59. position: absolute;
  60. top: -18px;
  61. left: 0;
  62. white-space: nowrap;
  63. }
  64. .bubble-user .bubble-label {
  65. right: 0;
  66. left: auto;
  67. color: #6ec1e4;
  68. }
  69. .bubble-ai .bubble-label {
  70. left: 0;
  71. color: #888;
  72. }
  73. #input-area {
  74. display: flex;
  75. gap: 8px;
  76. margin-top: 8px;
  77. }
  78. #input {
  79. flex: 1;
  80. padding: 8px 12px;
  81. border-radius: 6px;
  82. border: 1px solid #d0d7de;
  83. font-size: 15px;
  84. }
  85. #output {
  86. display: none;
  87. }
  88. #username, #userid {
  89. width: 90px;
  90. margin-right: 8px;
  91. padding: 4px 8px;
  92. border-radius: 4px;
  93. border: 1px solid #d0d7de;
  94. }
  95. #send-btn {
  96. background: #367be2;
  97. color: #fff;
  98. border: none;
  99. border-radius: 6px;
  100. padding: 0 18px;
  101. font-size: 15px;
  102. cursor: pointer;
  103. transition: background 0.2s;
  104. }
  105. #send-btn:hover {
  106. background: #265bb5;
  107. }
  108. </style>
  109. </head>
  110. <body>
  111. <div id="chatbox">
  112. <div style="margin-bottom: 10px;">
  113. 用户名:<input id="username" value="web_user">
  114. 用户ID:<input id="userid" value="web_001">
  115. </div>
  116. <div id="history"></div>
  117. <div id="output"></div>
  118. <div id="input-area">
  119. <input id="input" type="text" placeholder="请输入你的问题...">
  120. <button id="send-btn" onclick="send()">发送</button>
  121. </div>
  122. </div>
  123. <script>
  124. // 获取页面元素
  125. const agentname = "AI Agent";
  126. let historyDiv = document.getElementById('history');
  127. let output = document.getElementById('output');
  128. let inputBox = document.getElementById('input');
  129. let usernameBox = document.getElementById('username');
  130. let useridBox = document.getElementById('userid');
  131. // 对话上下文数组,每条消息包含role和content
  132. let context = [];
  133. // 自动滚动到底部
  134. function scrollToBottom() {
  135. historyDiv.scrollTop = historyDiv.scrollHeight;
  136. }
  137. // 渲染历史消息(用户和AI气泡)
  138. function renderHistory() {
  139. historyDiv.innerHTML = '';
  140. const username = usernameBox.value;
  141. for (const msg of context) {
  142. if (msg.role === 'user') {
  143. historyDiv.innerHTML += `<div class='msg-row msg-user'><div class='bubble bubble-user'><span class='bubble-label'>${username}</span>${msg.content}</div></div>`;
  144. } else {
  145. historyDiv.innerHTML += `<div class='msg-row msg-ai'><div class='bubble bubble-ai'><span class='bubble-label'>${agentname}</span>${msg.content}</div></div>`;
  146. }
  147. }
  148. scrollToBottom();
  149. }
  150. // 发送消息主函数
  151. function send() {
  152. // const agentname = "AI Agent";
  153. const username = usernameBox.value;
  154. const user_id = useridBox.value;
  155. const user_message = inputBox.value.trim();
  156. if (!user_message) return;
  157. // 1. 添加用户消息到上下文
  158. context.push({role: 'user', content: user_message});
  159. // 2. 插入空AI气泡,后续AI回复会填充到这里
  160. context.push({role: 'assistant', content: ''});
  161. renderHistory();
  162. inputBox.value = '';
  163. inputBox.focus();
  164. // 3. 关闭上一个EventSource,防止多路并发
  165. if (window.es) { window.es.close(); window.es = null; }
  166. // 4. 拼接上下文为字符串,传给后端,便于多轮对话
  167. // 5. 构造SSE GET请求URL
  168. const url = `http://127.0.0.1:8000/agents/stream?username=${encodeURIComponent(username)}&user_id=${encodeURIComponent(user_id)}&user_message=${encodeURIComponent(user_message)}`;
  169. window.es = new EventSource(url);
  170. let aiMsg = '';
  171. let aiQueue = []; // 存储待打字的新片段
  172. let isTyping = false; // 标记当前是否在打字
  173. // 6. SSE消息到达时,入队并启动typeWriter动画
  174. window.es.onmessage = function(e) {
  175. const text = e.data;
  176. aiQueue.push(text); // 新片段入队
  177. if (!isTyping) typeWriter();
  178. };
  179. // 7. 打字机动画:每次只对新片段做动画,顺序拼接
  180. function typeWriter() {
  181. if (aiQueue.length === 0) {
  182. isTyping = false;
  183. return;
  184. }
  185. isTyping = true;
  186. const text = aiQueue.shift();
  187. let i = 0;
  188. function typeOneByOne() {
  189. if (i < text.length) {
  190. aiMsg += text[i]; // 逐字追加到AI回复
  191. context[context.length-1].content = aiMsg; // 更新最后一个assistant气泡
  192. renderHistory();
  193. i++;
  194. setTimeout(typeOneByOne, 50); // 打字速度
  195. } else {
  196. // 递归处理下一个片段
  197. setTimeout(typeWriter,50)
  198. // typeWriter();
  199. }
  200. }
  201. typeOneByOne();
  202. }
  203. // 8. SSE错误处理,关闭连接
  204. window.es.onerror = function() {
  205. window.es && window.es.close();
  206. window.es = null;
  207. };
  208. }
  209. // 支持回车快捷发送
  210. inputBox.addEventListener('keydown', function(e) {
  211. if (e.key === 'Enter') send();
  212. });
  213. </script>
  214. </body>
  215. </html>