index.html 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
  6. <meta name="color-scheme" content="light dark">
  7. <title>🦙 llama.cpp - chat</title>
  8. </head>
  9. <body>
  10. <div id="app" class="opacity-0"> <!-- opacity-0 will be removed on app mounted -->
  11. <div class="flex flex-row drawer lg:drawer-open">
  12. <input id="toggle-drawer" type="checkbox" class="drawer-toggle" checked />
  13. <!-- sidebar -->
  14. <div class="drawer-side h-screen lg:h-screen z-50 lg:max-w-64">
  15. <label for="toggle-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
  16. <div class="flex flex-col bg-base-200 min-h-full max-w-64 py-4 px-4">
  17. <div class="flex flex-row items-center justify-between mb-4 mt-4">
  18. <h2 class="font-bold ml-4">Conversations</h2>
  19. <!-- close sidebar button -->
  20. <label for="toggle-drawer" class="btn btn-ghost lg:hidden">
  21. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-bar-left" viewBox="0 0 16 16">
  22. <path fill-rule="evenodd" d="M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5M10 8a.5.5 0 0 1-.5.5H3.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L3.707 7.5H9.5a.5.5 0 0 1 .5.5"/>
  23. </svg>
  24. </label>
  25. </div>
  26. <!-- list of conversations -->
  27. <div :class="{
  28. 'btn btn-ghost justify-start': true,
  29. 'btn-active': messages.length === 0,
  30. }" @click="newConversation">
  31. + New conversation
  32. </div>
  33. <div v-for="conv in conversations" :class="{
  34. 'btn btn-ghost justify-start font-normal': true,
  35. 'btn-active': conv.id === viewingConvId,
  36. }" @click="setViewingConv(conv.id)" dir="auto">
  37. <span class="truncate">{{ conv.messages[0].content }}</span>
  38. </div>
  39. <div class="text-center text-xs opacity-40 mt-auto mx-4">
  40. Conversations are saved to browser's localStorage
  41. </div>
  42. </div>
  43. </div>
  44. <!-- main view -->
  45. <div class="chat-screen drawer-content grow flex flex-col h-screen w-screen mx-auto px-4">
  46. <!-- header -->
  47. <div class="flex flex-row items-center mt-6 mb-6">
  48. <!-- open sidebar button -->
  49. <label for="toggle-drawer" class="btn btn-ghost lg:hidden">
  50. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-list" viewBox="0 0 16 16">
  51. <path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5"/>
  52. </svg>
  53. </label>
  54. <div class="grow text-2xl font-bold ml-2">llama.cpp</div>
  55. <!-- action buttons (top right) -->
  56. <div class="flex items-center">
  57. <div v-if="messages.length > 0" class="dropdown dropdown-end">
  58. <!-- "..." button -->
  59. <button tabindex="0" role="button" class="btn m-1" :disabled="isGenerating">
  60. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-three-dots-vertical" viewBox="0 0 16 16">
  61. <path d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0"/>
  62. </svg>
  63. </button>
  64. <!-- "delete" dropdown menu -->
  65. <ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow">
  66. <li @click="downloadConv(viewingConvId)"><a>Download</a></li>
  67. <li class="text-error" @click="deleteConv(viewingConvId)"><a>Delete</a></li>
  68. </ul>
  69. </div>
  70. <div class="tooltip tooltip-bottom" data-tip="Settings">
  71. <button class="btn" @click="showConfigDialog = true" :disabled="isGenerating">
  72. <!-- settings button -->
  73. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-gear" viewBox="0 0 16 16">
  74. <path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492M5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0"/>
  75. <path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115z"/>
  76. </svg>
  77. </button>
  78. </div>
  79. <!-- theme controller is copied from https://daisyui.com/components/theme-controller/ -->
  80. <div class="tooltip tooltip-bottom" data-tip="Themes">
  81. <div class="dropdown dropdown-end dropdown-bottom">
  82. <div tabindex="0" role="button" class="btn m-1">
  83. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-palette2" viewBox="0 0 16 16">
  84. <path d="M0 .5A.5.5 0 0 1 .5 0h5a.5.5 0 0 1 .5.5v5.277l4.147-4.131a.5.5 0 0 1 .707 0l3.535 3.536a.5.5 0 0 1 0 .708L10.261 10H15.5a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-.5.5H3a3 3 0 0 1-2.121-.879A3 3 0 0 1 0 13.044m6-.21 7.328-7.3-2.829-2.828L6 7.188zM4.5 13a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0M15 15v-4H9.258l-4.015 4zM0 .5v12.495zm0 12.495V13z"/>
  85. </svg>
  86. </div>
  87. <ul tabindex="0" class="dropdown-content bg-base-300 rounded-box z-[1] w-52 p-2 shadow-2xl h-80 overflow-y-auto">
  88. <li>
  89. <button
  90. class="btn btn-sm btn-block btn-ghost justify-start"
  91. :class="{ 'btn-active': selectedTheme === 'auto' }"
  92. @click="setSelectedTheme('auto')">
  93. auto
  94. </button>
  95. </li>
  96. <li v-for="theme in themes">
  97. <input
  98. type="radio"
  99. name="theme-dropdown"
  100. class="theme-controller btn btn-sm btn-block btn-ghost justify-start"
  101. :aria-label="theme"
  102. :value="theme"
  103. :checked="selectedTheme === theme"
  104. @click="setSelectedTheme(theme)" />
  105. </li>
  106. </ul>
  107. </div>
  108. </div>
  109. </div>
  110. </div>
  111. <!-- chat messages -->
  112. <div id="messages-list" class="flex flex-col grow overflow-y-auto">
  113. <div class="mt-auto flex justify-center">
  114. <!-- placeholder to shift the message to the bottom -->
  115. {{ messages.length === 0 ? 'Send a message to start' : '' }}
  116. </div>
  117. <div v-for="msg in messages" class="group">
  118. <message-bubble
  119. :config="config"
  120. :msg="msg"
  121. :key="msg.id"
  122. :is-generating="isGenerating"
  123. :edit-user-msg-and-regenerate="editUserMsgAndRegenerate"
  124. :regenerate-msg="regenerateMsg"></message-bubble>
  125. </div>
  126. <!-- pending (ongoing) assistant message -->
  127. <div id="pending-msg" class="group">
  128. <message-bubble
  129. v-if="pendingMsg"
  130. :config="config"
  131. :msg="pendingMsg"
  132. :key="pendingMsg.id"
  133. :is-generating="isGenerating"
  134. :show-thought-in-progress="config.showThoughtInProgress"
  135. :edit-user-msg-and-regenerate="() => {}"
  136. :regenerate-msg="() => {}"></message-bubble>
  137. </div>
  138. </div>
  139. <!-- chat input -->
  140. <div class="flex flex-row items-center mt-8 mb-6">
  141. <textarea
  142. class="textarea textarea-bordered w-full"
  143. placeholder="Type a message (Shift+Enter to add a new line)"
  144. v-model="inputMsg"
  145. @keydown.enter.exact.prevent="sendMessage"
  146. @keydown.enter.shift.exact.prevent="inputMsg += '\n'"
  147. :disabled="isGenerating"
  148. id="msg-input"
  149. dir="auto"
  150. ></textarea>
  151. <button v-if="!isGenerating" class="btn btn-primary ml-2" @click="sendMessage" :disabled="inputMsg.length === 0">Send</button>
  152. <button v-else class="btn btn-neutral ml-2" @click="stopGeneration">Stop</button>
  153. </div>
  154. </div>
  155. </div>
  156. <!-- modal for editing config -->
  157. <dialog class="modal" :class="{'modal-open': showConfigDialog}">
  158. <div class="modal-box">
  159. <h3 class="text-lg font-bold mb-6">Settings</h3>
  160. <div class="h-[calc(90vh-12rem)] overflow-y-auto">
  161. <p class="opacity-40 mb-6">Settings below are saved in browser's localStorage</p>
  162. <settings-modal-short-input :config-key="'apiKey'" :config-default="configDefault" :config-info="configInfo" v-model="config.apiKey"></settings-modal-short-input>
  163. <label class="form-control mb-2">
  164. <div class="label">System Message</div>
  165. <textarea class="textarea textarea-bordered h-24" :placeholder="'Default: ' + configDefault.systemMessage" v-model="config.systemMessage"></textarea>
  166. </label>
  167. <template v-for="configKey in ['temperature', 'top_k', 'top_p', 'min_p', 'max_tokens']">
  168. <settings-modal-short-input :config-key="configKey" :config-default="configDefault" :config-info="configInfo" v-model="config[configKey]"></settings-modal-short-input>
  169. </template>
  170. <!-- TODO: add more sampling-related configs, please regroup them into different "collapse" sections -->
  171. <!-- Section: Other sampler settings -->
  172. <details class="collapse collapse-arrow bg-base-200 mb-2 overflow-visible">
  173. <summary class="collapse-title font-bold">Other sampler settings</summary>
  174. <div class="collapse-content">
  175. <!-- Samplers queue -->
  176. <settings-modal-short-input label="Samplers queue" :config-key="'samplers'" :config-default="configDefault" :config-info="configInfo" v-model="config.samplers"></settings-modal-short-input>
  177. <!-- Samplers -->
  178. <template v-for="configKey in ['dynatemp_range', 'dynatemp_exponent', 'typical_p', 'xtc_probability', 'xtc_threshold']">
  179. <settings-modal-short-input :config-key="configKey" :config-default="configDefault" :config-info="configInfo" v-model="config[configKey]"></settings-modal-short-input>
  180. </template>
  181. </div>
  182. </details>
  183. <!-- Section: Penalties settings -->
  184. <details class="collapse collapse-arrow bg-base-200 mb-2 overflow-visible">
  185. <summary class="collapse-title font-bold">Penalties settings</summary>
  186. <div class="collapse-content">
  187. <template v-for="configKey in ['repeat_last_n', 'repeat_penalty', 'presence_penalty', 'frequency_penalty', 'dry_multiplier', 'dry_base', 'dry_allowed_length', 'dry_penalty_last_n']">
  188. <settings-modal-short-input :config-key="configKey" :config-default="configDefault" :config-info="configInfo" v-model="config[configKey]"></settings-modal-short-input>
  189. </template>
  190. </div>
  191. </details>
  192. <!-- Section: Reasoning models -->
  193. <details class="collapse collapse-arrow bg-base-200 mb-2 overflow-visible">
  194. <summary class="collapse-title font-bold">Reasoning models</summary>
  195. <div class="collapse-content">
  196. <div class="flex flex-row items-center mb-2">
  197. <input type="checkbox" class="checkbox" v-model="config.showThoughtInProgress" />
  198. <span class="ml-4">Expand though process by default for generating message</span>
  199. </div>
  200. <div class="flex flex-row items-center mb-2">
  201. <input type="checkbox" class="checkbox" v-model="config.excludeThoughtOnReq" />
  202. <span class="ml-4">Exclude thought process when sending request to API (Recommended for DeepSeek-R1)</span>
  203. </div>
  204. </div>
  205. </details>
  206. <!-- Section: Advanced config -->
  207. <details class="collapse collapse-arrow bg-base-200 mb-2 overflow-visible">
  208. <summary class="collapse-title font-bold">Advanced config</summary>
  209. <div class="collapse-content">
  210. <div class="flex flex-row items-center mb-2" v-if="isDev">
  211. <!-- this button only shows in dev mode, used to import a demo conversation to test message rendering -->
  212. <button class="btn" @click="debugImportDemoConv()">(debug) Import demo conversation</button>
  213. </div>
  214. <div class="flex flex-row items-center mb-2">
  215. <input type="checkbox" class="checkbox" v-model="config.showTokensPerSecond" />
  216. <span class="ml-4">Show tokens per second</span>
  217. </div>
  218. <label class="form-control mb-2">
  219. <!-- Custom parameters input -->
  220. <div class="label inline">Custom JSON config (For more info, refer to <a class="underline" href="https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md" target="_blank" rel="noopener noreferrer">server documentation</a>)</div>
  221. <textarea class="textarea textarea-bordered h-24" placeholder="Example: { &quot;mirostat&quot;: 1, &quot;min_p&quot;: 0.1 }" v-model="config.custom"></textarea>
  222. </label>
  223. </div>
  224. </details>
  225. </div>
  226. <!-- action buttons -->
  227. <div class="modal-action">
  228. <button class="btn" @click="resetConfigDialog">Reset to default</button>
  229. <button class="btn" @click="closeAndDiscardConfigDialog">Close</button>
  230. <button class="btn btn-primary" @click="closeAndSaveConfigDialog">Save</button>
  231. </div>
  232. </div>
  233. </dialog>
  234. </div>
  235. <!-- Template to be used as message bubble -->
  236. <template id="message-bubble">
  237. <div :class="{
  238. 'chat': true,
  239. 'chat-start': msg.role !== 'user',
  240. 'chat-end': msg.role === 'user',
  241. }">
  242. <div :class="{
  243. 'chat-bubble markdown': true,
  244. 'chat-bubble-base-300': msg.role !== 'user',
  245. }">
  246. <!-- textarea for editing message -->
  247. <template v-if="editingContent !== null">
  248. <textarea
  249. dir="auto"
  250. class="textarea textarea-bordered bg-base-100 text-base-content w-[calc(90vw-8em)] lg:w-96"
  251. v-model="editingContent"></textarea>
  252. <br/>
  253. <button class="btn btn-ghost mt-2 mr-2" @click="editingContent = null">Cancel</button>
  254. <button class="btn mt-2" @click="editMsg()">Submit</button>
  255. </template>
  256. <template v-else>
  257. <!-- show loading dots for pending message -->
  258. <span v-if="msg.content === null" class="loading loading-dots loading-md"></span>
  259. <!-- render message as markdown -->
  260. <div v-else dir="auto">
  261. <details v-if="msg.role === 'assistant' && splitMsgContent.cot" class="collapse bg-base-200 collapse-arrow mb-4" :open="splitMsgContent.isThinking && showThoughtInProgress">
  262. <summary class="collapse-title">
  263. <span v-if="splitMsgContent.isThinking">
  264. <span v-if="isGenerating" class="loading loading-spinner loading-md mr-2" style="vertical-align: middle;"></span>
  265. <b>Thinking</b>
  266. </span>
  267. <b v-else>Thought Process</b>
  268. </summary>
  269. <vue-markdown :source="splitMsgContent.cot" dir="auto" class="collapse-content"></vue-markdown>
  270. </details>
  271. <vue-markdown :source="splitMsgContent.content"></vue-markdown>
  272. </div>
  273. <!-- render timings if enabled -->
  274. <div class="dropdown dropdown-hover dropdown-top mt-2" v-if="timings && config.showTokensPerSecond">
  275. <div tabindex="0" role="button" class="cursor-pointer font-semibold text-sm opacity-60">Speed: {{ timings.predicted_per_second.toFixed(1) }} t/s</div>
  276. <div class="dropdown-content bg-base-100 z-10 w-64 p-2 shadow mt-4">
  277. <b>Prompt</b><br/>
  278. - Tokens: {{ timings.prompt_n }}<br/>
  279. - Time: {{ timings.prompt_ms }} ms<br/>
  280. - Speed: {{ timings.prompt_per_second.toFixed(1) }} t/s<br/>
  281. <b>Generation</b><br/>
  282. - Tokens: {{ timings.predicted_n }}<br/>
  283. - Time: {{ timings.predicted_ms }} ms<br/>
  284. - Speed: {{ timings.predicted_per_second.toFixed(1) }} t/s<br/>
  285. </div>
  286. </div>
  287. </template>
  288. </div>
  289. </div>
  290. <!-- actions for each message -->
  291. <div :class="{'text-right': msg.role === 'user', 'opacity-0': isGenerating}" class="mx-4 mt-2 mb-2">
  292. <!-- user message -->
  293. <button v-if="msg.role === 'user'" class="badge btn-mini show-on-hover" @click="editingContent = msg.content" :disabled="isGenerating">
  294. ✍️ Edit
  295. </button>
  296. <!-- assistant message -->
  297. <button v-if="msg.role === 'assistant'" class="badge btn-mini show-on-hover mr-2" @click="regenerateMsg(msg)" :disabled="isGenerating">
  298. 🔄 Regenerate
  299. </button>
  300. <button v-if="msg.role === 'assistant'" class="badge btn-mini show-on-hover mr-2" @click="copyMsg()" :disabled="isGenerating">
  301. 📋 Copy
  302. </button>
  303. </div>
  304. </template>
  305. <!-- Template to be used by settings modal -->
  306. <template id="settings-modal-short-input">
  307. <label class="input input-bordered join-item grow flex items-center gap-2 mb-2">
  308. <!-- Show help message on hovering on the input label -->
  309. <div class="dropdown dropdown-hover">
  310. <div tabindex="0" role="button" class="font-bold">{{ label || configKey }}</div>
  311. <div class="dropdown-content menu bg-base-100 rounded-box z-10 w-64 p-2 shadow mt-4">
  312. {{ configInfo[configKey] || '(no help message available)' }}
  313. </div>
  314. </div>
  315. <!-- Here we forward v-model from parent to child component, see: https://stackoverflow.com/questions/47311936/v-model-and-child-components -->
  316. <input type="text" class="grow" :placeholder="'Default: ' + (configDefault[configKey] || 'none')" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
  317. </label>
  318. </template>
  319. <script type="module" src="/src/main.js"></script>
  320. </body>
  321. </html>