test-chat.html 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>WMS智能助手</title>
  7. </head>
  8. <body>
  9. <div class="warehouse-chat">
  10. <!-- 侧边栏 -->
  11. <div class="sidebar">
  12. <div class="logo">
  13. <div class="logo-icon">
  14. <span style="font-size: 20px;">🤖</span>
  15. </div>
  16. </div>
  17. <div class="nav-buttons">
  18. <button class="nav-btn active">
  19. <span class="nav-icon">💬</span>
  20. </button>
  21. <button class="nav-btn">
  22. <span class="nav-icon">📊</span>
  23. </button>
  24. <button class="nav-btn">
  25. <span class="nav-icon">🔄</span>
  26. </button>
  27. </div>
  28. </div>
  29. <!-- 主聊天区域 -->
  30. <div class="chat-container">
  31. <!-- 欢迎界面 -->
  32. <div id="welcome-screen" class="welcome-screen">
  33. <div class="welcome-content">
  34. <div class="welcome-icon">
  35. <span style="font-size: 64px;">🤖</span>
  36. </div>
  37. <h1 class="welcome-title">我是WMS智能助手,很高兴见到你!</h1>
  38. <p class="welcome-subtitle">我可以帮您处理订单加急标记、创建入库预约等任务,请把你的任务交给我~</p>
  39. <!-- 推荐功能 -->
  40. <div class="recommendations">
  41. <div class="recommendation-section">
  42. <h3>📦 订单加急</h3>
  43. <p>例如:"订单SOZ25090400035加急处理"</p>
  44. </div>
  45. <div class="recommendation-section">
  46. <h3>🚚 创建入库预约</h3>
  47. <p>例如:"梁涵9月20日上午送100箱货到九干仓,单号T2025072101115676"</p>
  48. </div>
  49. <div class="examples">
  50. <div class="example" onclick="sendExample('订单SOZ25090400035加急处理')">订单SOZ25090400035加急处理</div>
  51. <div class="example" onclick="sendExample('梁涵9月20日上午送100箱货到九干仓,单号T2025072101115676')">梁涵9月20日上午送100箱货到九干仓,单号T2025072101115676</div>
  52. <div class="example" onclick="sendExample('傲竹,订单BSIM2510100010004预计10月13日上午到货')">傲竹,订单BSIM2510100010004预计10月13日上午到货</div>
  53. </div>
  54. </div>
  55. </div>
  56. </div>
  57. <!-- 聊天消息 -->
  58. <div id="messages-container" class="messages-container" style="display: none;">
  59. <div id="messages"></div>
  60. <!-- 加载状态 -->
  61. <div id="loading-message" class="message assistant" style="display: none;">
  62. <div class="message-content">
  63. <div class="assistant-message">
  64. <div class="assistant-icon">
  65. <span style="font-size: 16px;">🤖</span>
  66. </div>
  67. <div class="typing-indicator">
  68. <span class="dot"></span>
  69. <span class="dot"></span>
  70. <span class="dot"></span>
  71. </div>
  72. </div>
  73. </div>
  74. </div>
  75. </div>
  76. <!-- 输入区域 -->
  77. <div class="input-container">
  78. <div class="input-box">
  79. <div class="input-wrapper">
  80. <input
  81. id="message-input"
  82. type="text"
  83. placeholder="给WMS智能助手发送消息"
  84. class="message-input"
  85. onkeypress="handleKeyPress(event)"
  86. />
  87. <div class="input-actions">
  88. <button class="action-btn" title="深度思考">
  89. 🧠
  90. </button>
  91. <button class="action-btn" title="联网搜索">
  92. 🌐
  93. </button>
  94. <button
  95. id="send-btn"
  96. class="send-btn"
  97. onclick="sendMessage()"
  98. >
  99. </button>
  100. </div>
  101. </div>
  102. </div>
  103. <p class="disclaimer">内容由 AI 生成,请仔细核验</p>
  104. </div>
  105. </div>
  106. </div>
  107. <script>
  108. // 全局变量
  109. const API_BASE_URL = 'http://127.0.0.1:8080';
  110. let messages = [];
  111. let isLoading = false;
  112. // DOM元素
  113. let welcomeScreen;
  114. let messagesContainer;
  115. let messagesDiv;
  116. let messageInput;
  117. let sendBtn;
  118. let loadingMessage;
  119. let currentAssistantElement = null;
  120. let currentAssistantText = '';
  121. // 初始化
  122. document.addEventListener('DOMContentLoaded', function() {
  123. welcomeScreen = document.getElementById('welcome-screen');
  124. messagesContainer = document.getElementById('messages-container');
  125. messagesDiv = document.getElementById('messages');
  126. messageInput = document.getElementById('message-input');
  127. sendBtn = document.getElementById('send-btn');
  128. loadingMessage = document.getElementById('loading-message');
  129. // 自动聚焦输入框
  130. messageInput.focus();
  131. console.log('聊天界面已初始化');
  132. });
  133. // 处理键盘事件
  134. function handleKeyPress(event) {
  135. if (event.key === 'Enter' && !event.shiftKey) {
  136. event.preventDefault();
  137. sendMessage();
  138. }
  139. }
  140. // 发送示例消息
  141. function sendExample(text) {
  142. messageInput.value = text;
  143. sendMessage();
  144. }
  145. // 发送消息
  146. async function sendMessage() {
  147. const message = messageInput.value.trim();
  148. if (!message || isLoading) return;
  149. // 隐藏欢迎界面,显示消息容器
  150. if (messages.length === 0) {
  151. welcomeScreen.style.display = 'none';
  152. messagesContainer.style.display = 'block';
  153. }
  154. // 禁用输入和按钮
  155. setInputState(false);
  156. // 添加用户消息
  157. addUserMessage(message);
  158. // 显示加载状态
  159. showLoading();
  160. // 清空输入框
  161. messageInput.value = '';
  162. try {
  163. const response = await fetch(`${API_BASE_URL}/api/agent/chat/stream`, {
  164. method: 'POST',
  165. headers: {
  166. 'Content-Type': 'application/json',
  167. },
  168. body: JSON.stringify({
  169. message: message
  170. })
  171. });
  172. await handleStreamResponse(response);
  173. } catch (error) {
  174. console.error('请求失败:', error);
  175. // 隐藏加载状态
  176. hideLoading();
  177. // 添加错误消息
  178. addBotMessage('❌ 网络错误,请稍后重试。');
  179. } finally {
  180. // 重新启用输入和按钮
  181. setInputState(true);
  182. messageInput.focus();
  183. }
  184. }
  185. // 添加用户消息
  186. function addUserMessage(message) {
  187. const messageDiv = document.createElement('div');
  188. messageDiv.className = 'message user';
  189. messageDiv.innerHTML = `
  190. <div class="message-content">
  191. <div class="user-message">
  192. ${escapeHtml(message)}
  193. </div>
  194. </div>
  195. `;
  196. messagesDiv.appendChild(messageDiv);
  197. messages.push({ type: 'user', content: message, timestamp: new Date() });
  198. scrollToBottom();
  199. }
  200. // 添加机器人消息
  201. function addBotMessage(message) {
  202. const messageDiv = document.createElement('div');
  203. messageDiv.className = 'message assistant';
  204. messageDiv.innerHTML = `
  205. <div class="message-content">
  206. <div class="assistant-message">
  207. <div class="assistant-icon">
  208. <span style="font-size: 16px;">🤖</span>
  209. </div>
  210. <div class="assistant-text">${formatMessage(message)}</div>
  211. </div>
  212. </div>
  213. `;
  214. messagesDiv.appendChild(messageDiv);
  215. messages.push({ type: 'assistant', content: message, timestamp: new Date() });
  216. scrollToBottom();
  217. }
  218. function createStreamingBotMessage() {
  219. const messageDiv = document.createElement('div');
  220. messageDiv.className = 'message assistant';
  221. messageDiv.innerHTML = `
  222. <div class="message-content">
  223. <div class="assistant-message">
  224. <div class="assistant-icon">
  225. <span style="font-size: 16px;">🤖</span>
  226. </div>
  227. <div class="assistant-text"></div>
  228. </div>
  229. </div>
  230. `;
  231. messagesDiv.appendChild(messageDiv);
  232. messages.push({ type: 'assistant', content: '', timestamp: new Date() });
  233. scrollToBottom();
  234. currentAssistantElement = messageDiv.querySelector('.assistant-text');
  235. currentAssistantText = '';
  236. }
  237. function updateStreamingBotMessage(text) {
  238. if (!currentAssistantElement) {
  239. createStreamingBotMessage();
  240. }
  241. currentAssistantText += text;
  242. currentAssistantElement.innerHTML = formatMessage(currentAssistantText);
  243. const lastMessage = messages[messages.length - 1];
  244. if (lastMessage && lastMessage.type === 'assistant') {
  245. lastMessage.content = currentAssistantText;
  246. }
  247. scrollToBottom();
  248. }
  249. async function handleStreamResponse(response) {
  250. if (!response.ok) {
  251. hideLoading();
  252. addBotMessage('❌ 服务器返回错误,请稍后重试。');
  253. return;
  254. }
  255. const reader = response.body?.getReader();
  256. if (!reader) {
  257. hideLoading();
  258. addBotMessage('❌ 当前浏览器不支持流式响应。');
  259. return;
  260. }
  261. createStreamingBotMessage();
  262. const decoder = new TextDecoder('utf-8');
  263. let buffer = '';
  264. let streamFinished = false;
  265. try {
  266. while (!streamFinished) {
  267. const { value, done } = await reader.read();
  268. if (done) {
  269. break;
  270. }
  271. buffer += decoder.decode(value, { stream: true });
  272. let boundary = findSseBoundary(buffer);
  273. while (boundary.index !== -1) {
  274. const rawEvent = buffer.slice(0, boundary.index).replace(/\r/g, '').trim();
  275. buffer = buffer.slice(boundary.index + boundary.length);
  276. streamFinished = processSseEvent(rawEvent) || streamFinished;
  277. boundary = findSseBoundary(buffer);
  278. }
  279. }
  280. if (buffer.trim()) {
  281. streamFinished = processSseEvent(buffer.replace(/\r/g, '').trim()) || streamFinished;
  282. }
  283. } catch (error) {
  284. console.error('处理流式响应失败:', error);
  285. updateStreamingBotMessage('\n❌ 响应解析失败,请稍后重试。');
  286. } finally {
  287. hideLoading();
  288. currentAssistantElement = null;
  289. currentAssistantText = '';
  290. }
  291. }
  292. function findSseBoundary(text) {
  293. const lfIndex = text.indexOf('\n\n');
  294. const crlfIndex = text.indexOf('\r\n\r\n');
  295. if (lfIndex === -1 && crlfIndex === -1) {
  296. return { index: -1, length: 0 };
  297. }
  298. if (lfIndex === -1) {
  299. return { index: crlfIndex, length: 4 };
  300. }
  301. if (crlfIndex === -1) {
  302. return { index: lfIndex, length: 2 };
  303. }
  304. return lfIndex < crlfIndex
  305. ? { index: lfIndex, length: 2 }
  306. : { index: crlfIndex, length: 4 };
  307. }
  308. function processSseEvent(rawEvent) {
  309. if (!rawEvent) return false;
  310. const lines = rawEvent.split('\n');
  311. let finished = false;
  312. for (const line of lines) {
  313. const trimmedLine = line.trim();
  314. if (!trimmedLine.startsWith('data:')) continue;
  315. const dataStr = trimmedLine.slice(5).trim();
  316. if (!dataStr) continue;
  317. if (dataStr === '[DONE]') {
  318. finished = true;
  319. break;
  320. }
  321. let payload;
  322. try {
  323. payload = JSON.parse(dataStr);
  324. } catch (error) {
  325. console.warn('JSON解析失败,使用原始数据:', dataStr);
  326. payload = { type: 'text', text: dataStr };
  327. }
  328. if (payload.type === 'text' && payload.text) {
  329. updateStreamingBotMessage(payload.text);
  330. } else if (payload.type === 'error') {
  331. updateStreamingBotMessage(`\n❌ ${payload.text || '发生未知错误'}`);
  332. }
  333. }
  334. return finished;
  335. }
  336. // 显示加载状态
  337. function showLoading() {
  338. isLoading = true;
  339. loadingMessage.style.display = 'block';
  340. scrollToBottom();
  341. }
  342. // 隐藏加载状态
  343. function hideLoading() {
  344. isLoading = false;
  345. loadingMessage.style.display = 'none';
  346. }
  347. // 设置输入状态
  348. function setInputState(enabled) {
  349. messageInput.disabled = !enabled;
  350. sendBtn.disabled = !enabled;
  351. if (enabled) {
  352. sendBtn.innerHTML = '↑';
  353. } else {
  354. sendBtn.innerHTML = '<div class="loading"></div>';
  355. }
  356. }
  357. // 滚动到底部
  358. function scrollToBottom() {
  359. messagesContainer.scrollTop = messagesContainer.scrollHeight;
  360. }
  361. // HTML转义
  362. function escapeHtml(text) {
  363. const div = document.createElement('div');
  364. div.textContent = text;
  365. return div.innerHTML;
  366. }
  367. // 格式化消息
  368. function formatMessage(content) {
  369. // 先进行HTML转义,再处理换行
  370. return content.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>');
  371. }
  372. </script>
  373. <style>
  374. * {
  375. margin: 0;
  376. padding: 0;
  377. box-sizing: border-box;
  378. }
  379. body {
  380. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
  381. background: #f8f9fa;
  382. }
  383. .warehouse-chat {
  384. display: flex;
  385. height: 100vh;
  386. background: #f8f9fa;
  387. }
  388. .sidebar {
  389. width: 60px;
  390. background: #ffffff;
  391. border-right: 1px solid #e5e7eb;
  392. display: flex;
  393. flex-direction: column;
  394. align-items: center;
  395. padding: 16px 0;
  396. }
  397. .logo {
  398. margin-bottom: 24px;
  399. }
  400. .logo-icon {
  401. width: 32px;
  402. height: 32px;
  403. background: linear-gradient(135deg, #f8f8fc, #d6d1df);
  404. border-radius: 8px;
  405. display: flex;
  406. align-items: center;
  407. justify-content: center;
  408. font-size: 16px;
  409. }
  410. .nav-buttons {
  411. display: flex;
  412. flex-direction: column;
  413. gap: 8px;
  414. }
  415. .nav-btn {
  416. width: 44px;
  417. height: 44px;
  418. border: none;
  419. background: transparent;
  420. border-radius: 8px;
  421. cursor: pointer;
  422. display: flex;
  423. align-items: center;
  424. justify-content: center;
  425. transition: all 0.2s;
  426. }
  427. .nav-btn:hover {
  428. background: #f3f4f6;
  429. }
  430. .nav-btn.active {
  431. background: #eef2ff;
  432. color: #4f46e5;
  433. }
  434. .nav-icon {
  435. font-size: 18px;
  436. }
  437. .chat-container {
  438. flex: 1;
  439. display: flex;
  440. flex-direction: column;
  441. max-width: 800px;
  442. margin: 0 auto;
  443. width: 100%;
  444. }
  445. .welcome-screen {
  446. flex: 1;
  447. display: flex;
  448. align-items: center;
  449. justify-content: center;
  450. padding: 40px;
  451. }
  452. .welcome-content {
  453. text-align: center;
  454. max-width: 600px;
  455. }
  456. .welcome-icon {
  457. font-size: 64px;
  458. margin-bottom: 24px;
  459. }
  460. .welcome-title {
  461. font-size: 28px;
  462. font-weight: 600;
  463. color: #1f2937;
  464. margin-bottom: 16px;
  465. line-height: 1.3;
  466. }
  467. .welcome-subtitle {
  468. font-size: 16px;
  469. color: #6b7280;
  470. line-height: 1.5;
  471. margin: 0 0 32px 0;
  472. }
  473. .recommendations {
  474. text-align: left;
  475. background: white;
  476. border-radius: 16px;
  477. padding: 24px;
  478. box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
  479. margin-top: 24px;
  480. }
  481. .recommendation-section {
  482. margin-bottom: 20px;
  483. }
  484. .recommendation-section h3 {
  485. font-size: 16px;
  486. color: #1f2937;
  487. margin-bottom: 8px;
  488. }
  489. .recommendation-section p {
  490. font-size: 14px;
  491. color: #6b7280;
  492. margin: 0;
  493. }
  494. .examples {
  495. display: flex;
  496. flex-direction: column;
  497. gap: 12px;
  498. margin-top: 20px;
  499. }
  500. .example {
  501. background: #f8f9fa;
  502. border: 1px solid #e5e7eb;
  503. border-radius: 12px;
  504. padding: 12px 16px;
  505. cursor: pointer;
  506. transition: all 0.2s;
  507. font-size: 14px;
  508. color: #374151;
  509. }
  510. .example:hover {
  511. background: #eef2ff;
  512. border-color: #4f46e5;
  513. color: #4f46e5;
  514. }
  515. .messages-container {
  516. flex: 1;
  517. overflow-y: auto;
  518. padding: 24px;
  519. padding-bottom: 0;
  520. }
  521. .message {
  522. margin-bottom: 24px;
  523. }
  524. .message-content {
  525. display: flex;
  526. max-width: 100%;
  527. }
  528. .user-message {
  529. background: #4f46e5;
  530. color: white;
  531. padding: 12px 18px;
  532. border-radius: 18px;
  533. margin-left: auto;
  534. max-width: 70%;
  535. word-wrap: break-word;
  536. line-height: 1.4;
  537. }
  538. .assistant-message {
  539. display: flex;
  540. align-items: flex-start;
  541. gap: 12px;
  542. max-width: 80%;
  543. }
  544. .assistant-icon {
  545. width: 32px;
  546. height: 32px;
  547. background: linear-gradient(135deg, #f5f5f7, #cfd5e1);
  548. border-radius: 50%;
  549. display: flex;
  550. align-items: center;
  551. justify-content: center;
  552. font-size: 14px;
  553. flex-shrink: 0;
  554. }
  555. .assistant-text {
  556. background: white;
  557. border: 1px solid #e5e7eb;
  558. padding: 12px 18px;
  559. border-radius: 18px;
  560. line-height: 1.5;
  561. white-space: pre-line;
  562. color: #1f2937;
  563. }
  564. .typing-indicator {
  565. display: flex;
  566. gap: 4px;
  567. padding: 12px 18px;
  568. background: white;
  569. border: 1px solid #e5e7eb;
  570. border-radius: 18px;
  571. }
  572. .dot {
  573. width: 6px;
  574. height: 6px;
  575. background: #9ca3af;
  576. border-radius: 50%;
  577. animation: typing 1.4s infinite;
  578. }
  579. .dot:nth-child(2) {
  580. animation-delay: 0.2s;
  581. }
  582. .dot:nth-child(3) {
  583. animation-delay: 0.4s;
  584. }
  585. @keyframes typing {
  586. 0%, 60%, 100% {
  587. transform: translateY(0);
  588. }
  589. 30% {
  590. transform: translateY(-10px);
  591. }
  592. }
  593. .input-container {
  594. padding: 24px;
  595. background: white;
  596. border-top: 1px solid #e5e7eb;
  597. }
  598. .input-box {
  599. max-width: 100%;
  600. }
  601. .input-wrapper {
  602. position: relative;
  603. background: white;
  604. border: 2px solid #e5e7eb;
  605. border-radius: 24px;
  606. padding: 8px 16px;
  607. display: flex;
  608. align-items: center;
  609. gap: 8px;
  610. transition: border-color 0.2s;
  611. }
  612. .input-wrapper:focus-within {
  613. border-color: #4f46e5;
  614. }
  615. .message-input {
  616. flex: 1;
  617. border: none;
  618. outline: none;
  619. font-size: 16px;
  620. padding: 8px 0;
  621. background: transparent;
  622. color: #1f2937;
  623. }
  624. .message-input::placeholder {
  625. color: #9ca3af;
  626. }
  627. .input-actions {
  628. display: flex;
  629. align-items: center;
  630. gap: 8px;
  631. }
  632. .action-btn {
  633. width: 32px;
  634. height: 32px;
  635. border: none;
  636. background: transparent;
  637. border-radius: 6px;
  638. cursor: pointer;
  639. display: flex;
  640. align-items: center;
  641. justify-content: center;
  642. font-size: 14px;
  643. transition: background 0.2s;
  644. }
  645. .action-btn:hover {
  646. background: #f3f4f6;
  647. }
  648. .send-btn {
  649. width: 32px;
  650. height: 32px;
  651. background: #4f46e5;
  652. border: none;
  653. border-radius: 50%;
  654. color: white;
  655. cursor: pointer;
  656. display: flex;
  657. align-items: center;
  658. justify-content: center;
  659. font-size: 16px;
  660. font-weight: bold;
  661. transition: all 0.2s;
  662. }
  663. .send-btn:hover:not(:disabled) {
  664. background: #4338ca;
  665. transform: translateY(-1px);
  666. }
  667. .send-btn:disabled {
  668. background: #d1d5db;
  669. cursor: not-allowed;
  670. }
  671. .disclaimer {
  672. text-align: center;
  673. font-size: 12px;
  674. color: #9ca3af;
  675. margin-top: 12px;
  676. margin-bottom: 0;
  677. }
  678. /* 响应式设计 */
  679. @media (max-width: 768px) {
  680. .sidebar {
  681. width: 50px;
  682. padding: 12px 0;
  683. }
  684. .welcome-title {
  685. font-size: 24px;
  686. }
  687. .messages-container {
  688. padding: 16px;
  689. }
  690. .input-container {
  691. padding: 16px;
  692. }
  693. .user-message,
  694. .assistant-message {
  695. max-width: 90%;
  696. }
  697. .examples {
  698. gap: 8px;
  699. }
  700. .example {
  701. padding: 10px 12px;
  702. font-size: 13px;
  703. }
  704. }
  705. </style>
  706. </body>
  707. </html>