index.html 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. <!DOCTYPE html>
  2. <head>
  3. <title>tinychat</title>
  4. <meta content="width=device-width, initial-scale=1" name="viewport"/>
  5. <link href="favicon.svg" rel="icon" type="image/svg+xml"/>
  6. <script defer="" src="/static/cdn.jsdelivr.net/npm/@alpine-collective/toolkit@1.0.2/dist/cdn.min.js"></script>
  7. <script defer="" src="/static/cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js"></script>
  8. <script defer="" src="/static/cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js"></script>
  9. <script defer="" src="/static/unpkg.com/@marcreichel/alpine-autosize@1.3.x/dist/alpine-autosize.min.js"></script>
  10. <script defer="" src="/static/unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
  11. <script src="/static/unpkg.com/dompurify@3.1.5/dist/purify.min.js"></script>
  12. <script src="/static/unpkg.com/marked@13.0.0/marked.min.js"></script>
  13. <script src="/static/unpkg.com/marked-highlight@2.1.2/lib/index.umd.js"></script>
  14. <script src="/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js"></script>
  15. <script src="/index.js"></script>
  16. <link href="/static/fonts.googleapis.com" rel="preconnect"/>
  17. <link crossorigin="" href="/static/fonts.gstatic.com" rel="preconnect"/>
  18. <link href="/static/fonts.googleapis.com/css2" rel="stylesheet"/>
  19. <link href="/static/cdn.jsdelivr.net/npm/purecss@3.0.0/build/base-min.css" rel="stylesheet"/>
  20. <link crossorigin="anonymous" href="/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" referrerpolicy="no-referrer" rel="stylesheet">
  21. <link href="/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/vs2015.min.css" rel="stylesheet"/>
  22. <link href="/index.css" rel="stylesheet"/>
  23. <link href="/common.css" rel="stylesheet"/>
  24. </link></head>
  25. <body>
  26. <main x-data="state" x-init="console.log(endpoint)">
  27. <div class="model-selector">
  28. <select @change="if (cstate) cstate.selectedModel = $event.target.value" x-model="cstate.selectedModel">
  29. <option selected="" value="llama-3.1-8b">Llama 3.1 8B</option>
  30. <option value="llama-3.1-70b">Llama 3.1 70B</option>
  31. <option value="llama-3.1-70b-bf16">Llama 3.1 70B (BF16)</option>
  32. <option value="llama-3.1-405b">Llama 3.1 405B</option>
  33. <option value="llama-3-8b">Llama 3 8B</option>
  34. <option value="llama-3-70b">Llama 3 70B</option>
  35. <option value="mistral-nemo">Mistral Nemo</option>
  36. <option value="mistral-large">Mistral Large</option>
  37. <option value="deepseek-coder-v2-lite">Deepseek Coder V2 Lite</option>
  38. <option value="llava-1.5-7b-hf">LLaVa 1.5 7B (Vision Model)</option>
  39. <option value="qwen-2.5-7b">Qwen 2.5 7B</option>
  40. <option value="qwen-2.5-math-7b">Qwen 2.5 7B (Math)</option>
  41. <option value="qwen-2.5-14b">Qwen 2.5 14B</option>
  42. <option value="qwen-2.5-72b">Qwen 2.5 72B</option>
  43. <option value="qwen-2.5-math-72b">Qwen 2.5 72B (Math)</option>
  44. </select>
  45. </div>
  46. <div @popstate.window="
  47. if (home === 2) {
  48. home = -1;
  49. cstate = { time: null, messages: [], selectedModel: 'llama-3.1-8b' };
  50. time_till_first = 0;
  51. tokens_per_second = 0;
  52. total_tokens = 0;
  53. }
  54. " class="home centered" x-effect="
  55. $refs.inputForm.focus();
  56. if (home === 1) setTimeout(() =&gt; home = 2, 100);
  57. if (home === -1) setTimeout(() =&gt; home = 0, 100);
  58. " x-show="home === 0" x-transition="">
  59. <h1 class="title megrim-regular">tinychat</h1>
  60. <div class="histories-container-container">
  61. <template x-if="histories.length">
  62. <div class="histories-start"></div>
  63. </template>
  64. <div class="histories-container" x-intersect="
  65. $el.scrollTo({ top: 0, behavior: 'smooth' });
  66. ">
  67. <template x-for="_state in histories.toSorted((a, b) =&gt; b.time - a.time)">
  68. <div @click="
  69. cstate = _state;
  70. if (cstate) cstate.selectedModel = document.querySelector('.model-selector select').value
  71. // updateTotalTokens(cstate.messages);
  72. home = 1;
  73. // ensure that going back in history will go back to home
  74. window.history.pushState({}, '', '/');
  75. " @touchend="
  76. if (Math.abs($event.changedTouches[0].clientX - otx) &gt; trigger) removeHistory(_state);
  77. $el.style.setProperty('--tx', 0);
  78. $el.style.setProperty('--opacity', 1);
  79. " @touchmove="
  80. $el.style.setProperty('--tx', $event.changedTouches[0].clientX - otx);
  81. $el.style.setProperty('--opacity', 1 - (Math.abs($event.changedTouches[0].clientX - otx) / trigger));
  82. " @touchstart="
  83. otx = $event.changedTouches[0].clientX;
  84. " class="history" x-data="{ otx: 0, trigger: 75 }">
  85. <h3 x-text="new Date(_state.time).toLocaleString()"></h3>
  86. <p x-text="$truncate(_state.messages[0].content, 80)"></p>
  87. <!-- delete button -->
  88. <button @click.stop="removeHistory(_state);" class="history-delete-button">
  89. <i class="fas fa-trash"></i>
  90. </button>
  91. </div>
  92. </template>
  93. </div>
  94. <template x-if="histories.length">
  95. <div class="histories-end"></div>
  96. </template>
  97. </div>
  98. </div>
  99. <div class="messages" x-init="
  100. $watch('cstate', value =&gt; {
  101. $el.innerHTML = '';
  102. value.messages.forEach(({ role, content }) =&gt; {
  103. const div = document.createElement('div');
  104. div.className = `message message-role-${role}`;
  105. try {
  106. div.innerHTML = DOMPurify.sanitize(marked.parse(content));
  107. } catch (e) {
  108. console.log(content);
  109. console.error(e);
  110. }
  111. // add a clipboard button to all code blocks
  112. const codeBlocks = div.querySelectorAll('.hljs');
  113. codeBlocks.forEach(codeBlock =&gt; {
  114. const button = document.createElement('button');
  115. button.className = 'clipboard-button';
  116. button.innerHTML = '&lt;i class=\'fas fa-clipboard\'&gt;&lt;/i&gt;';
  117. button.onclick = () =&gt; {
  118. // navigator.clipboard.writeText(codeBlock.textContent);
  119. const range = document.createRange();
  120. range.setStartBefore(codeBlock);
  121. range.setEndAfter(codeBlock);
  122. window.getSelection()?.removeAllRanges();
  123. window.getSelection()?.addRange(range);
  124. document.execCommand('copy');
  125. window.getSelection()?.removeAllRanges();
  126. button.innerHTML = '&lt;i class=\'fas fa-check\'&gt;&lt;/i&gt;';
  127. setTimeout(() =&gt; button.innerHTML = '&lt;i class=\'fas fa-clipboard\'&gt;&lt;/i&gt;', 1000);
  128. };
  129. codeBlock.appendChild(button);
  130. });
  131. $el.appendChild(div);
  132. });
  133. $el.scrollTo({ top: $el.scrollHeight, behavior: 'smooth' });
  134. });
  135. " x-intersect="
  136. $el.scrollTo({ top: $el.scrollHeight, behavior: 'smooth' });
  137. " x-ref="messages" x-show="home === 2" x-transition="">
  138. </div>
  139. <div class="input-container">
  140. <div class="input-performance">
  141. <span class="input-performance-point">
  142. <p class="monospace" x-text="(time_till_first / 1000).toFixed(2)"></p>
  143. <p class="megrim-regular">SEC TO FIRST TOKEN</p>
  144. </span>
  145. <span class="input-performance-point">
  146. <p class="monospace" x-text="tokens_per_second.toFixed(1)"></p>
  147. <p class="megrim-regular">TOKENS/SEC</p>
  148. </span>
  149. <span class="input-performance-point">
  150. <p class="monospace" x-text="total_tokens"></p>
  151. <p class="megrim-regular">TOKENS</p>
  152. </span>
  153. </div>
  154. <div class="input">
  155. <button @click="$refs.imageUpload.click()" class="image-input-button" x-show="cstate.selectedModel === 'llava-1.5-7b-hf'">
  156. <i class="fas fa-image"></i>
  157. </button>
  158. <input @change="$data.handleImageUpload($event)" accept="image/*" id="image-upload" style="display: none;" type="file" x-ref="imageUpload"/>
  159. <div class="image-preview-container" x-show="imagePreview">
  160. <img :src="imagePreview" alt="Uploaded Image" class="image-preview"/>
  161. <button @click="imagePreview = null; imageUrl = null;" class="remove-image-button">
  162. <i class="fas fa-times"></i>
  163. </button>
  164. </div>
  165. <textarea :disabled="generating" :placeholder="generating ? 'Generating...' : 'Say something'" @input="
  166. home = (home === 0) ? 1 : home
  167. if (cstate.messages.length === 0 &amp;&amp; $el.value === '') home = -1;
  168. if ($el.value !== '') {
  169. const messages = [...cstate.messages];
  170. messages.push({ role: 'user', content: $el.value });
  171. // updateTotalTokens(messages);
  172. } else {
  173. if (cstate.messages.length === 0) total_tokens = 0;
  174. // else updateTotalTokens(cstate.messages);
  175. }
  176. " @keydown.enter="await handleEnter($event)" @keydown.escape.window="$focus.focus($el)" autofocus="" class="input-form" id="input-form" rows="1" x-autosize="" x-effect="
  177. console.log(generating);
  178. if (!generating) $nextTick(() =&gt; {
  179. $el.focus();
  180. setTimeout(() =&gt; $refs.messages.scrollTo({ top: $refs.messages.scrollHeight, behavior: 'smooth' }), 100);
  181. });
  182. " x-ref="inputForm"></textarea>
  183. <button :disabled="generating" @click="await handleSend()" class="input-button">
  184. <i :class="generating ? 'fa-spinner fa-spin' : 'fa-paper-plane'" class="fas"></i>
  185. </button>
  186. </div>
  187. </div>
  188. </main>
  189. </body>