123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- <!DOCTYPE html>
- <html lang="zh">
- <head>
- <meta charset="UTF-8">
- <title>SSE 多轮对话演示</title>
- <style>
- body {
- background: #f6f8fa;
- font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", Arial, sans-serif;
- }
- #chatbox {
- width: 480px;
- margin: 40px auto;
- background: #fff;
- border-radius: 10px;
- box-shadow: 0 2px 12px #0001;
- padding: 24px 24px 12px 24px;
- }
- #history {
- min-height: 240px;
- max-height: 400px;
- overflow-y: auto;
- margin-bottom: 16px;
- padding-right: 8px;
- }
- .msg-row {
- display: flex;
- margin-bottom: 8px;
- }
- .msg-user {
- justify-content: flex-end;
- }
- .msg-ai {
- justify-content: flex-start;
- }
- .bubble {
- max-width: 70%;
- padding: 10px 16px;
- border-radius: 18px;
- font-size: 15px;
- line-height: 1.6;
- box-shadow: 0 1px 4px #0001;
- word-break: break-all;
- position: relative;
- }
- .bubble-user {
- background: linear-gradient(90deg, #6ec1e4 0%, #367be2 100%);
- color: #fff;
- border-bottom-right-radius: 4px;
- }
- .bubble-ai {
- background: #f0f1f5;
- color: #222;
- border-bottom-left-radius: 4px;
- }
- .bubble-label {
- font-size: 12px;
- color: #888;
- position: absolute;
- top: -18px;
- left: 0;
- white-space: nowrap;
- }
- .bubble-user .bubble-label {
- right: 0;
- left: auto;
- color: #6ec1e4;
- }
- .bubble-ai .bubble-label {
- left: 0;
- color: #888;
- }
- #input-area {
- display: flex;
- gap: 8px;
- margin-top: 8px;
- }
- #input {
- flex: 1;
- padding: 8px 12px;
- border-radius: 6px;
- border: 1px solid #d0d7de;
- font-size: 15px;
- }
- #output {
- display: none;
- }
- #username, #userid {
- width: 90px;
- margin-right: 8px;
- padding: 4px 8px;
- border-radius: 4px;
- border: 1px solid #d0d7de;
- }
- #send-btn {
- background: #367be2;
- color: #fff;
- border: none;
- border-radius: 6px;
- padding: 0 18px;
- font-size: 15px;
- cursor: pointer;
- transition: background 0.2s;
- }
- #send-btn:hover {
- background: #265bb5;
- }
- </style>
- </head>
- <body>
- <div id="chatbox">
- <div style="margin-bottom: 10px;">
- 用户名:<input id="username" value="web_user">
- 用户ID:<input id="userid" value="web_001">
- </div>
- <div id="history"></div>
- <div id="output"></div>
- <div id="input-area">
- <input id="input" type="text" placeholder="请输入你的问题...">
- <button id="send-btn" onclick="send()">发送</button>
- </div>
- </div>
- <script>
- // 获取页面元素
- const agentname = "AI Agent";
- let historyDiv = document.getElementById('history');
- let output = document.getElementById('output');
- let inputBox = document.getElementById('input');
- let usernameBox = document.getElementById('username');
- let useridBox = document.getElementById('userid');
- // 对话上下文数组,每条消息包含role和content
- let context = [];
- // 自动滚动到底部
- function scrollToBottom() {
- historyDiv.scrollTop = historyDiv.scrollHeight;
- }
- // 渲染历史消息(用户和AI气泡)
- function renderHistory() {
- historyDiv.innerHTML = '';
- const username = usernameBox.value;
- for (const msg of context) {
- if (msg.role === 'user') {
- historyDiv.innerHTML += `<div class='msg-row msg-user'><div class='bubble bubble-user'><span class='bubble-label'>${username}</span>${msg.content}</div></div>`;
- } else {
- historyDiv.innerHTML += `<div class='msg-row msg-ai'><div class='bubble bubble-ai'><span class='bubble-label'>${agentname}</span>${msg.content}</div></div>`;
- }
- }
- scrollToBottom();
- }
- // 发送消息主函数
- function send() {
- // const agentname = "AI Agent";
- const username = usernameBox.value;
- const user_id = useridBox.value;
- const user_message = inputBox.value.trim();
- if (!user_message) return;
- // 1. 添加用户消息到上下文
- context.push({role: 'user', content: user_message});
- // 2. 插入空AI气泡,后续AI回复会填充到这里
- context.push({role: 'assistant', content: ''});
- renderHistory();
- inputBox.value = '';
- inputBox.focus();
- // 3. 关闭上一个EventSource,防止多路并发
- if (window.es) { window.es.close(); window.es = null; }
- // 4. 拼接上下文为字符串,传给后端,便于多轮对话
-
- // 5. 构造SSE GET请求URL
- const url = `http://127.0.0.1:8000/agents/stream?username=${encodeURIComponent(username)}&user_id=${encodeURIComponent(user_id)}&user_message=${encodeURIComponent(user_message)}`;
- window.es = new EventSource(url);
- let aiMsg = '';
- let aiQueue = []; // 存储待打字的新片段
- let isTyping = false; // 标记当前是否在打字
- // 6. SSE消息到达时,入队并启动typeWriter动画
- window.es.onmessage = function(e) {
- const text = e.data;
- aiQueue.push(text); // 新片段入队
- if (!isTyping) typeWriter();
-
- };
- // 7. 打字机动画:每次只对新片段做动画,顺序拼接
- function typeWriter() {
- if (aiQueue.length === 0) {
- isTyping = false;
- return;
- }
- isTyping = true;
- const text = aiQueue.shift();
- let i = 0;
- function typeOneByOne() {
- if (i < text.length) {
- aiMsg += text[i]; // 逐字追加到AI回复
- context[context.length-1].content = aiMsg; // 更新最后一个assistant气泡
- renderHistory();
- i++;
- setTimeout(typeOneByOne, 50); // 打字速度
- } else {
- // 递归处理下一个片段
- setTimeout(typeWriter,50)
- // typeWriter();
- }
- }
- typeOneByOne();
- }
- // 8. SSE错误处理,关闭连接
- window.es.onerror = function() {
- window.es && window.es.close();
- window.es = null;
- };
- }
- // 支持回车快捷发送
- inputBox.addEventListener('keydown', function(e) {
- if (e.key === 'Enter') send();
- });
- </script>
- </body>
- </html>
|