index.html 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  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-405b">Llama 3.1 405B</option>
  32. <option value="llama-3-8b">Llama 3 8B</option>
  33. <option value="llama-3-70b">Llama 3 70B</option>
  34. <option value="mistral-nemo">Mistral Nemo</option>
  35. <option value="mistral-large">Mistral Large</option>
  36. <option value="deepseek-coder-v2-lite">Deepseek Coder V2 Lite</option>
  37. <option value="llava-1.5-7b-hf">LLaVa 1.5 7B (Vision Model)</option>
  38. </select>
  39. </div>
  40. <div @popstate.window="
  41. if (home === 2) {
  42. home = -1;
  43. cstate = { time: null, messages: [], selectedModel: 'llama-3.1-8b' };
  44. time_till_first = 0;
  45. tokens_per_second = 0;
  46. total_tokens = 0;
  47. }
  48. " class="home centered" x-effect="
  49. $refs.inputForm.focus();
  50. if (home === 1) setTimeout(() =&gt; home = 2, 100);
  51. if (home === -1) setTimeout(() =&gt; home = 0, 100);
  52. " x-show="home === 0" x-transition="">
  53. <h1 class="title megrim-regular">tinychat</h1>
  54. <div class="histories-container-container">
  55. <template x-if="histories.length">
  56. <div class="histories-start"></div>
  57. </template>
  58. <div class="histories-container" x-intersect="
  59. $el.scrollTo({ top: 0, behavior: 'smooth' });
  60. ">
  61. <template x-for="_state in histories.toSorted((a, b) =&gt; b.time - a.time)">
  62. <div @click="
  63. cstate = _state;
  64. if (cstate) cstate.selectedModel = document.querySelector('.model-selector select').value
  65. // updateTotalTokens(cstate.messages);
  66. home = 1;
  67. // ensure that going back in history will go back to home
  68. window.history.pushState({}, '', '/');
  69. " @touchend="
  70. if (Math.abs($event.changedTouches[0].clientX - otx) &gt; trigger) removeHistory(_state);
  71. $el.style.setProperty('--tx', 0);
  72. $el.style.setProperty('--opacity', 1);
  73. " @touchmove="
  74. $el.style.setProperty('--tx', $event.changedTouches[0].clientX - otx);
  75. $el.style.setProperty('--opacity', 1 - (Math.abs($event.changedTouches[0].clientX - otx) / trigger));
  76. " @touchstart="
  77. otx = $event.changedTouches[0].clientX;
  78. " class="history" x-data="{ otx: 0, trigger: 75 }">
  79. <h3 x-text="new Date(_state.time).toLocaleString()"></h3>
  80. <p x-text="$truncate(_state.messages[0].content, 80)"></p>
  81. <!-- delete button -->
  82. <button @click.stop="removeHistory(_state);" class="history-delete-button">
  83. <i class="fas fa-trash"></i>
  84. </button>
  85. </div>
  86. </template>
  87. </div>
  88. <template x-if="histories.length">
  89. <div class="histories-end"></div>
  90. </template>
  91. </div>
  92. </div>
  93. <div class="messages" x-init="
  94. $watch('cstate', value =&gt; {
  95. $el.innerHTML = '';
  96. value.messages.forEach(({ role, content }) =&gt; {
  97. const div = document.createElement('div');
  98. div.className = `message message-role-${role}`;
  99. try {
  100. div.innerHTML = DOMPurify.sanitize(marked.parse(content));
  101. } catch (e) {
  102. console.log(content);
  103. console.error(e);
  104. }
  105. // add a clipboard button to all code blocks
  106. const codeBlocks = div.querySelectorAll('.hljs');
  107. codeBlocks.forEach(codeBlock =&gt; {
  108. const button = document.createElement('button');
  109. button.className = 'clipboard-button';
  110. button.innerHTML = '&lt;i class=\'fas fa-clipboard\'&gt;&lt;/i&gt;';
  111. button.onclick = () =&gt; {
  112. // navigator.clipboard.writeText(codeBlock.textContent);
  113. const range = document.createRange();
  114. range.setStartBefore(codeBlock);
  115. range.setEndAfter(codeBlock);
  116. window.getSelection()?.removeAllRanges();
  117. window.getSelection()?.addRange(range);
  118. document.execCommand('copy');
  119. window.getSelection()?.removeAllRanges();
  120. button.innerHTML = '&lt;i class=\'fas fa-check\'&gt;&lt;/i&gt;';
  121. setTimeout(() =&gt; button.innerHTML = '&lt;i class=\'fas fa-clipboard\'&gt;&lt;/i&gt;', 1000);
  122. };
  123. codeBlock.appendChild(button);
  124. });
  125. $el.appendChild(div);
  126. });
  127. $el.scrollTo({ top: $el.scrollHeight, behavior: 'smooth' });
  128. });
  129. " x-intersect="
  130. $el.scrollTo({ top: $el.scrollHeight, behavior: 'smooth' });
  131. " x-ref="messages" x-show="home === 2" x-transition="">
  132. </div>
  133. <div class="input-container">
  134. <div class="input-performance">
  135. <span class="input-performance-point">
  136. <p class="monospace" x-text="(time_till_first / 1000).toFixed(2)"></p>
  137. <p class="megrim-regular">SEC TO FIRST TOKEN</p>
  138. </span>
  139. <span class="input-performance-point">
  140. <p class="monospace" x-text="tokens_per_second.toFixed(1)"></p>
  141. <p class="megrim-regular">TOKENS/SEC</p>
  142. </span>
  143. <span class="input-performance-point">
  144. <p class="monospace" x-text="total_tokens"></p>
  145. <p class="megrim-regular">TOKENS</p>
  146. </span>
  147. </div>
  148. <div class="input">
  149. <button @click="$refs.imageUpload.click()" class="image-input-button" x-show="cstate.selectedModel === 'llava-1.5-7b-hf'">
  150. <i class="fas fa-image"></i>
  151. </button>
  152. <input @change="$data.handleImageUpload($event)" accept="image/*" id="image-upload" style="display: none;" type="file" x-ref="imageUpload"/>
  153. <div class="image-preview-container" x-show="imagePreview">
  154. <img :src="imagePreview" alt="Uploaded Image" class="image-preview"/>
  155. <button @click="imagePreview = null; imageUrl = null;" class="remove-image-button">
  156. <i class="fas fa-times"></i>
  157. </button>
  158. </div>
  159. <textarea :disabled="generating" :placeholder="generating ? 'Generating...' : 'Say something'" @input="
  160. home = (home === 0) ? 1 : home
  161. if (cstate.messages.length === 0 &amp;&amp; $el.value === '') home = -1;
  162. if ($el.value !== '') {
  163. const messages = [...cstate.messages];
  164. messages.push({ role: 'user', content: $el.value });
  165. // updateTotalTokens(messages);
  166. } else {
  167. if (cstate.messages.length === 0) total_tokens = 0;
  168. // else updateTotalTokens(cstate.messages);
  169. }
  170. " @keydown.enter="await handleEnter($event)" @keydown.escape.window="$focus.focus($el)" autofocus="" class="input-form" id="input-form" rows="1" x-autosize="" x-effect="
  171. console.log(generating);
  172. if (!generating) $nextTick(() =&gt; {
  173. $el.focus();
  174. setTimeout(() =&gt; $refs.messages.scrollTo({ top: $refs.messages.scrollHeight, behavior: 'smooth' }), 100);
  175. });
  176. " x-ref="inputForm"></textarea>
  177. <button :disabled="generating" @click="await handleSend()" class="input-button">
  178. <i :class="generating ? 'fa-spinner fa-spin' : 'fa-paper-plane'" class="fas"></i>
  179. </button>
  180. </div>
  181. </div>
  182. </main>
  183. </body>