ai.html 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  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, maximum-scale=1.0, user-scalable=no"/>
  6. <title>灵兰助医</title>
  7. <link rel="stylesheet" href="./css/bootstrap.min.css">
  8. <link rel="stylesheet" href="./css/all.min.css">
  9. <link rel="stylesheet" href="./css/github.min.css">
  10. <link rel="stylesheet" href="./css/styles.css">
  11. <link rel="stylesheet" href="./css/chat.css">
  12. </head>
  13. <body>
  14. <div class="container-fluid not-bg vh-100 d-flex flex-column">
  15. <!-- 顶部标题栏 -->
  16. <header class="p-2" style="min-height: 44px; text-align: center; font-size: 18px; font-weight: 700;">
  17. <!--中医AI助手-->
  18. </header>
  19. <!-- 聊天主区域 -->
  20. <main class="flex-grow-1 d-flex flex-column overflow-hidden" id="chat-container">
  21. <div class="flex-grow-1 overflow-auto px-2 py-3" id="chat-messages">
  22. <!-- 初始系统消息 -->
  23. <div class="message system-message mb-3">
  24. <div class="avatar">
  25. <img src="./image/robot.png" alt="">
  26. </div>
  27. <div class="content">
  28. <div class="card">
  29. <div class="card-body">
  30. <p>您好!我是您的中医AI助手,可以帮助您:</p>
  31. <ul>
  32. <li>分析症状</li>
  33. <li>辨别证型</li>
  34. <li>推荐治疗方案</li>
  35. </ul>
  36. </div>
  37. </div>
  38. </div>
  39. </div>
  40. </div>
  41. <!-- 输入区域 -->
  42. <div class="px-3 py-2 border-top">
  43. <div class="input-group input-group-sm">
  44. <textarea
  45. id="message-input"
  46. class="form-control"
  47. placeholder="请输入..."
  48. rows="1"
  49. enterkeyhint="send"
  50. ></textarea>
  51. <button id="send-btn" class="btn btn-primary btn-sm">
  52. <i class="fas fa-paper-plane"></i>
  53. </button>
  54. </div>
  55. </div>
  56. </main>
  57. </div>
  58. <!-- 隐藏字段 -->
  59. <input type="hidden" id="session-id" value="{{ session_id }}">
  60. <!-- JavaScript 依赖 -->
  61. <script src="./js/bootstrap.bundle.min.js"></script>
  62. <script src="./js/marked.min.js"></script>
  63. <script src="./js/highlight.min.js"></script>
  64. <script>document.addEventListener('gesturestart', function (event) {
  65. event.preventDefault();
  66. }, false);</script>
  67. <!-- 主应用脚本 -->
  68. <script>
  69. const searchParams = new URLSearchParams(window.location.search);
  70. const host = `https://dev.hzliuzhi.com:62006`;
  71. document.addEventListener('DOMContentLoaded', function () {
  72. if (searchParams.has('hide_title')) document.querySelector('.container-fluid header').style.display = 'none';
  73. else document.querySelector('.container-fluid header').innerHTML = document.title;
  74. // 获取DOM元素
  75. const chatMessages = document.getElementById('chat-messages');
  76. const messageInput = document.getElementById('message-input');
  77. const sendBtn = document.getElementById('send-btn');
  78. let sessionId = document.getElementById('session-id').value;
  79. if (sessionId.replace(/\s/g, '') === `{{session_id}}`) document.querySelector(`#session-id`).value = sessionId = searchParams.get('session_id');
  80. let eventSource = null;
  81. // 初始化Marked和Highlight.js
  82. marked.setOptions({
  83. breaks: true,
  84. highlight: function (code, language) {
  85. const validLanguage = hljs.getLanguage(language) ? language : 'plaintext';
  86. return hljs.highlight(validLanguage, code).value;
  87. }
  88. });
  89. // 发送消息
  90. function sendMessage() {
  91. const message = messageInput.value.trim();
  92. if (!message) return;
  93. // 添加用户消息
  94. addMessage('user', message);
  95. messageInput.value = '';
  96. // 添加AI思考状态
  97. const aiMessageElement = addMessage('assistant', '', true);
  98. // 关闭之前的连接
  99. if (eventSource) {
  100. eventSource.close();
  101. eventSource = null;
  102. }
  103. let aiResponse = '';
  104. let responseElement = null;
  105. // 使用fetch进行流式请求
  106. fetch(`${host}/tcm_chat/chat/tcm`, {
  107. method: 'POST',
  108. headers: {
  109. 'Content-Type': 'application/json',
  110. },
  111. body: JSON.stringify({
  112. session_id: sessionId,
  113. message: message
  114. })
  115. })
  116. .then(response => {
  117. if (!response.ok) {
  118. throw new Error(`HTTP error! status: ${response.status}`);
  119. }
  120. const reader = response.body.getReader();
  121. const decoder = new TextDecoder();
  122. function readStream() {
  123. return reader.read().then(({done, value}) => {
  124. if (done) {
  125. return;
  126. }
  127. const chunk = decoder.decode(value);
  128. const lines = chunk.split('\n');
  129. for (const line of lines) {
  130. if (line.startsWith('think: ')) { // 思考内容
  131. try {
  132. const data = JSON.parse(line.slice(7));
  133. if (data.content) {
  134. console.log('流信号 思考内容', data.content)
  135. // 这里没实现呢,需要你参考openai的实现@xiong
  136. // 进入思考状态
  137. }
  138. } catch (e) {
  139. console.error('解析响应数据失败:', e);
  140. }
  141. } else if (line.startsWith('data: ')) { // 返回内容
  142. try {
  143. const data = JSON.parse(line.slice(6));
  144. if (data.error) {
  145. // 处理错误
  146. if (!responseElement) {
  147. aiMessageElement.innerHTML = `<div class="alert alert-danger">${data.error}</div>`;
  148. }
  149. return;
  150. }
  151. if (data.content) {
  152. console.log('流信号', data.content)
  153. aiResponse += data.content;
  154. if (!responseElement) {
  155. // 移除思考状态
  156. aiMessageElement.innerHTML = '';
  157. responseElement = aiMessageElement;
  158. }
  159. updateMessageContent(responseElement, aiResponse);
  160. }
  161. } catch (e) {
  162. console.error('解析响应数据失败:', e);
  163. }
  164. }
  165. }
  166. // 继续读取流
  167. return readStream();
  168. });
  169. }
  170. return readStream();
  171. })
  172. .catch(error => {
  173. console.error('请求失败:', error);
  174. if (!responseElement) {
  175. aiMessageElement.innerHTML = '<div class="alert alert-danger">连接错误,请重试</div>';
  176. }
  177. });
  178. }
  179. // 添加消息到聊天界面
  180. function addMessage(role, content, isThinking = false) {
  181. const messageDiv = document.createElement('div');
  182. messageDiv.className = `message ${role}-message mb-3`;
  183. const avatar = document.createElement('div');
  184. avatar.className = 'avatar';
  185. avatar.innerHTML = role === 'user' ?
  186. '<i class="fas fa-user"></i>' :
  187. '<img src="./image/robot.png" alt="">';
  188. const contentDiv = document.createElement('div');
  189. contentDiv.className = 'content';
  190. if (isThinking) {
  191. contentDiv.innerHTML = `
  192. <div class="thinking-container">
  193. <div class="thinking-indicator">
  194. <span></span><span></span><span></span>
  195. </div>
  196. <div class="thinking-text">思考中...</div>
  197. </div>
  198. `;
  199. } else {
  200. contentDiv.innerHTML = marked.parse(content);
  201. hljs.highlightAll();
  202. }
  203. messageDiv.appendChild(avatar);
  204. messageDiv.appendChild(contentDiv);
  205. chatMessages.appendChild(messageDiv);
  206. // 滚动到底部
  207. // chatMessages.scrollTop = chatMessages.scrollHeight;
  208. messageDiv.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'start' });
  209. return contentDiv;
  210. }
  211. // 更新消息内容
  212. function updateMessageContent(element, content) {
  213. element.innerHTML = marked.parse(content);
  214. hljs.highlightAll();
  215. }
  216. // 事件监听
  217. sendBtn.addEventListener('click', sendMessage);
  218. messageInput.addEventListener('keypress', function (e) {
  219. if (e.key === 'Enter' && !e.shiftKey) {
  220. e.preventDefault();
  221. sendMessage();
  222. }
  223. });
  224. // 初始聚焦输入框
  225. messageInput.focus();
  226. });
  227. </script>
  228. </body>
  229. </html>