|
@@ -1,57 +1,44 @@
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<head>
|
|
|
- <title>tinychat</title>
|
|
|
- <meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
- <link rel="icon" href="favicon.svg" type="image/svg+xml">
|
|
|
-
|
|
|
- <script defer src="https://cdn.jsdelivr.net/npm/@alpine-collective/toolkit@1.0.2/dist/cdn.min.js"></script>
|
|
|
- <script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js"></script>
|
|
|
- <script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js"></script>
|
|
|
- <script defer src="https://unpkg.com/@marcreichel/alpine-autosize@1.3.x/dist/alpine-autosize.min.js"></script>
|
|
|
- <script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
|
-
|
|
|
- <script src="https://unpkg.com/dompurify@3.1.5/dist/purify.min.js"></script>
|
|
|
- <script src="https://unpkg.com/marked@13.0.0/marked.min.js"></script>
|
|
|
- <script src="https://unpkg.com/marked-highlight@2.1.2/lib/index.umd.js"></script>
|
|
|
- <script src="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js"></script>
|
|
|
-
|
|
|
- <script src="index.js"></script>
|
|
|
-
|
|
|
- <link rel="preconnect" href="https://fonts.googleapis.com">
|
|
|
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
|
- <link href="https://fonts.googleapis.com/css2?family=Megrim&display=swap" rel="stylesheet">
|
|
|
-
|
|
|
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/base-min.css">
|
|
|
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
|
|
|
- integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A=="
|
|
|
- crossorigin="anonymous" referrerpolicy="no-referrer" />
|
|
|
- <link rel="stylesheet" href="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/vs2015.min.css">
|
|
|
-
|
|
|
- <link rel="stylesheet" href="index.css">
|
|
|
- <link rel="stylesheet" href="common.css">
|
|
|
-</head>
|
|
|
-
|
|
|
+<title>tinychat</title>
|
|
|
+<meta content="width=device-width, initial-scale=1" name="viewport"/>
|
|
|
+<link href="favicon.svg" rel="icon" type="image/svg+xml"/>
|
|
|
+<script defer="" src="static/cdn.jsdelivr.net/npm/@alpine-collective/toolkit@1.0.2/dist/cdn.min.js"></script>
|
|
|
+<script defer="" src="static/cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js"></script>
|
|
|
+<script defer="" src="static/cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js"></script>
|
|
|
+<script defer="" src="static/unpkg.com/@marcreichel/alpine-autosize@1.3.x/dist/alpine-autosize.min.js"></script>
|
|
|
+<script defer="" src="static/unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
|
+<script src="static/unpkg.com/dompurify@3.1.5/dist/purify.min.js"></script>
|
|
|
+<script src="static/unpkg.com/marked@13.0.0/marked.min.js"></script>
|
|
|
+<script src="static/unpkg.com/marked-highlight@2.1.2/lib/index.umd.js"></script>
|
|
|
+<script src="static/unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js"></script>
|
|
|
+<script src="index.js"></script>
|
|
|
+<link href="static/fonts.googleapis.com" rel="preconnect"/>
|
|
|
+<link crossorigin="" href="static/fonts.gstatic.com" rel="preconnect"/>
|
|
|
+<link href="static/fonts.googleapis.com/css2" rel="stylesheet"/>
|
|
|
+<link href="static/cdn.jsdelivr.net/npm/purecss@3.0.0/build/base-min.css" rel="stylesheet"/>
|
|
|
+<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">
|
|
|
+<link href="static/unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/vs2015.min.css" rel="stylesheet"/>
|
|
|
+<link href="index.css" rel="stylesheet"/>
|
|
|
+<link href="common.css" rel="stylesheet"/>
|
|
|
+</link></head>
|
|
|
<body>
|
|
|
- <main x-data="state" x-init="console.log(endpoint)">
|
|
|
- <div class="model-selector">
|
|
|
- <select x-model="cstate.selectedModel" @change="if (cstate) cstate.selectedModel = $event.target.value">
|
|
|
- <option value="llama-3.1-8b" selected>Llama 3.1 8B</option>
|
|
|
- <option value="llama-3.1-70b">Llama 3.1 70B</option>
|
|
|
- <option value="llama-3.1-405b">Llama 3.1 405B</option>
|
|
|
- <option value="llama-3-8b">Llama 3 8B</option>
|
|
|
- <option value="llama-3-70b">Llama 3 70B</option>
|
|
|
- <option value="mistral-nemo">Mistral Nemo</option>
|
|
|
- <option value="mistral-large">Mistral Large</option>
|
|
|
- <option value="deepseek-coder-v2-lite">Deepseek Coder V2 Lite</option>
|
|
|
- <option value="llava-1.5-7b-hf">LLaVa 1.5 7B (Vision Model)</option>
|
|
|
- </select>
|
|
|
- </div>
|
|
|
- <div class="home centered" x-show="home === 0" x-transition x-effect="
|
|
|
- $refs.inputForm.focus();
|
|
|
- if (home === 1) setTimeout(() => home = 2, 100);
|
|
|
- if (home === -1) setTimeout(() => home = 0, 100);
|
|
|
- " @popstate.window="
|
|
|
+<main x-data="state" x-init="console.log(endpoint)">
|
|
|
+<div class="model-selector">
|
|
|
+<select @change="if (cstate) cstate.selectedModel = $event.target.value" x-model="cstate.selectedModel">
|
|
|
+<option selected="" value="llama-3.1-8b">Llama 3.1 8B</option>
|
|
|
+<option value="llama-3.1-70b">Llama 3.1 70B</option>
|
|
|
+<option value="llama-3.1-405b">Llama 3.1 405B</option>
|
|
|
+<option value="llama-3-8b">Llama 3 8B</option>
|
|
|
+<option value="llama-3-70b">Llama 3 70B</option>
|
|
|
+<option value="mistral-nemo">Mistral Nemo</option>
|
|
|
+<option value="mistral-large">Mistral Large</option>
|
|
|
+<option value="deepseek-coder-v2-lite">Deepseek Coder V2 Lite</option>
|
|
|
+<option value="llava-1.5-7b-hf">LLaVa 1.5 7B (Vision Model)</option>
|
|
|
+</select>
|
|
|
+</div>
|
|
|
+<div @popstate.window="
|
|
|
if (home === 2) {
|
|
|
home = -1;
|
|
|
cstate = { time: null, messages: [], selectedModel: 'llama-3.1-8b' };
|
|
@@ -59,51 +46,55 @@
|
|
|
tokens_per_second = 0;
|
|
|
total_tokens = 0;
|
|
|
}
|
|
|
- ">
|
|
|
- <h1 class="title megrim-regular">tinychat</h1>
|
|
|
- <div class="histories-container-container">
|
|
|
- <template x-if="histories.length">
|
|
|
- <div class="histories-start"></div>
|
|
|
- </template>
|
|
|
- <div class="histories-container" x-intersect="
|
|
|
+ " class="home centered" x-effect="
|
|
|
+ $refs.inputForm.focus();
|
|
|
+ if (home === 1) setTimeout(() => home = 2, 100);
|
|
|
+ if (home === -1) setTimeout(() => home = 0, 100);
|
|
|
+ " x-show="home === 0" x-transition="">
|
|
|
+<h1 class="title megrim-regular">tinychat</h1>
|
|
|
+<div class="histories-container-container">
|
|
|
+<template x-if="histories.length">
|
|
|
+<div class="histories-start"></div>
|
|
|
+</template>
|
|
|
+<div class="histories-container" x-intersect="
|
|
|
$el.scrollTo({ top: 0, behavior: 'smooth' });
|
|
|
">
|
|
|
- <template x-for="_state in histories.toSorted((a, b) => b.time - a.time)">
|
|
|
- <div x-data="{ otx: 0, trigger: 75 }" class="history" @click="
|
|
|
+<template x-for="_state in histories.toSorted((a, b) => b.time - a.time)">
|
|
|
+<div @click="
|
|
|
cstate = _state;
|
|
|
if (cstate) cstate.selectedModel = document.querySelector('.model-selector select').value
|
|
|
// updateTotalTokens(cstate.messages);
|
|
|
home = 1;
|
|
|
// ensure that going back in history will go back to home
|
|
|
window.history.pushState({}, '', '/');
|
|
|
- " @touchstart="
|
|
|
- otx = $event.changedTouches[0].clientX;
|
|
|
- " @touchmove="
|
|
|
- $el.style.setProperty('--tx', $event.changedTouches[0].clientX - otx);
|
|
|
- $el.style.setProperty('--opacity', 1 - (Math.abs($event.changedTouches[0].clientX - otx) / trigger));
|
|
|
" @touchend="
|
|
|
- if (Math.abs($event.changedTouches[0].clientX - otx) > trigger) removeHistory(_state);
|
|
|
+ if (Math.abs($event.changedTouches[0].clientX - otx) > trigger) removeHistory(_state);
|
|
|
$el.style.setProperty('--tx', 0);
|
|
|
$el.style.setProperty('--opacity', 1);
|
|
|
- ">
|
|
|
- <h3 x-text="new Date(_state.time).toLocaleString()"></h3>
|
|
|
- <p x-text="$truncate(_state.messages[0].content, 80)"></p>
|
|
|
- <!-- delete button -->
|
|
|
- <button class="history-delete-button" @click.stop="removeHistory(_state);">
|
|
|
- <i class=" fas fa-trash"></i>
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- </div>
|
|
|
- <template x-if="histories.length">
|
|
|
- <div class="histories-end"></div>
|
|
|
- </template>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div x-ref="messages" class="messages" x-init="
|
|
|
- $watch('cstate', value => {
|
|
|
+ " @touchmove="
|
|
|
+ $el.style.setProperty('--tx', $event.changedTouches[0].clientX - otx);
|
|
|
+ $el.style.setProperty('--opacity', 1 - (Math.abs($event.changedTouches[0].clientX - otx) / trigger));
|
|
|
+ " @touchstart="
|
|
|
+ otx = $event.changedTouches[0].clientX;
|
|
|
+ " class="history" x-data="{ otx: 0, trigger: 75 }">
|
|
|
+<h3 x-text="new Date(_state.time).toLocaleString()"></h3>
|
|
|
+<p x-text="$truncate(_state.messages[0].content, 80)"></p>
|
|
|
+<!-- delete button -->
|
|
|
+<button @click.stop="removeHistory(_state);" class="history-delete-button">
|
|
|
+<i class="fas fa-trash"></i>
|
|
|
+</button>
|
|
|
+</div>
|
|
|
+</template>
|
|
|
+</div>
|
|
|
+<template x-if="histories.length">
|
|
|
+<div class="histories-end"></div>
|
|
|
+</template>
|
|
|
+</div>
|
|
|
+</div>
|
|
|
+<div class="messages" x-init="
|
|
|
+ $watch('cstate', value => {
|
|
|
$el.innerHTML = '';
|
|
|
- value.messages.forEach(({ role, content }) => {
|
|
|
+ value.messages.forEach(({ role, content }) => {
|
|
|
const div = document.createElement('div');
|
|
|
div.className = `message message-role-${role}`;
|
|
|
try {
|
|
@@ -115,11 +106,11 @@
|
|
|
|
|
|
// add a clipboard button to all code blocks
|
|
|
const codeBlocks = div.querySelectorAll('.hljs');
|
|
|
- codeBlocks.forEach(codeBlock => {
|
|
|
+ codeBlocks.forEach(codeBlock => {
|
|
|
const button = document.createElement('button');
|
|
|
button.className = 'clipboard-button';
|
|
|
- button.innerHTML = '<i class=\'fas fa-clipboard\'></i>';
|
|
|
- button.onclick = () => {
|
|
|
+ button.innerHTML = '<i class=\'fas fa-clipboard\'></i>';
|
|
|
+ button.onclick = () => {
|
|
|
// navigator.clipboard.writeText(codeBlock.textContent);
|
|
|
const range = document.createRange();
|
|
|
range.setStartBefore(codeBlock);
|
|
@@ -129,8 +120,8 @@
|
|
|
document.execCommand('copy');
|
|
|
window.getSelection()?.removeAllRanges();
|
|
|
|
|
|
- button.innerHTML = '<i class=\'fas fa-check\'></i>';
|
|
|
- setTimeout(() => button.innerHTML = '<i class=\'fas fa-clipboard\'></i>', 1000);
|
|
|
+ button.innerHTML = '<i class=\'fas fa-check\'></i>';
|
|
|
+ setTimeout(() => button.innerHTML = '<i class=\'fas fa-clipboard\'></i>', 1000);
|
|
|
};
|
|
|
codeBlock.appendChild(button);
|
|
|
});
|
|
@@ -142,38 +133,37 @@
|
|
|
});
|
|
|
" x-intersect="
|
|
|
$el.scrollTo({ top: $el.scrollHeight, behavior: 'smooth' });
|
|
|
- " x-show="home === 2" x-transition>
|
|
|
- </div>
|
|
|
- <div class="input-container">
|
|
|
- <div class="input-performance">
|
|
|
- <span class="input-performance-point">
|
|
|
- <p class="monospace" x-text="(time_till_first / 1000).toFixed(2)"></p>
|
|
|
- <p class="megrim-regular">SEC TO FIRST TOKEN</p>
|
|
|
- </span>
|
|
|
- <span class="input-performance-point">
|
|
|
- <p class="monospace" x-text="tokens_per_second.toFixed(1)"></p>
|
|
|
- <p class="megrim-regular">TOKENS/SEC</p>
|
|
|
- </span>
|
|
|
- <span class="input-performance-point">
|
|
|
- <p class="monospace" x-text="total_tokens"></p>
|
|
|
- <p class="megrim-regular">TOKENS</p>
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <div class="input">
|
|
|
- <button x-show="cstate.selectedModel === 'llava-1.5-7b-hf'" class="image-input-button" @click="$refs.imageUpload.click()">
|
|
|
- <i class="fas fa-image"></i>
|
|
|
- </button>
|
|
|
- <input x-ref="imageUpload" type="file" id="image-upload" accept="image/*" @change="$data.handleImageUpload($event)" style="display: none;">
|
|
|
- <div x-show="imagePreview" class="image-preview-container">
|
|
|
- <img :src="imagePreview" alt="Uploaded Image" class="image-preview">
|
|
|
- <button @click="imagePreview = null; imageUrl = null;" class="remove-image-button">
|
|
|
- <i class="fas fa-times"></i>
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- <textarea x-ref="inputForm" id="input-form" class="input-form" autofocus rows=1 x-autosize
|
|
|
- :placeholder="generating ? 'Generating...' : 'Say something'" :disabled="generating" @input="
|
|
|
+ " x-ref="messages" x-show="home === 2" x-transition="">
|
|
|
+</div>
|
|
|
+<div class="input-container">
|
|
|
+<div class="input-performance">
|
|
|
+<span class="input-performance-point">
|
|
|
+<p class="monospace" x-text="(time_till_first / 1000).toFixed(2)"></p>
|
|
|
+<p class="megrim-regular">SEC TO FIRST TOKEN</p>
|
|
|
+</span>
|
|
|
+<span class="input-performance-point">
|
|
|
+<p class="monospace" x-text="tokens_per_second.toFixed(1)"></p>
|
|
|
+<p class="megrim-regular">TOKENS/SEC</p>
|
|
|
+</span>
|
|
|
+<span class="input-performance-point">
|
|
|
+<p class="monospace" x-text="total_tokens"></p>
|
|
|
+<p class="megrim-regular">TOKENS</p>
|
|
|
+</span>
|
|
|
+</div>
|
|
|
+<div class="input">
|
|
|
+<button @click="$refs.imageUpload.click()" class="image-input-button" x-show="cstate.selectedModel === 'llava-1.5-7b-hf'">
|
|
|
+<i class="fas fa-image"></i>
|
|
|
+</button>
|
|
|
+<input @change="$data.handleImageUpload($event)" accept="image/*" id="image-upload" style="display: none;" type="file" x-ref="imageUpload"/>
|
|
|
+<div class="image-preview-container" x-show="imagePreview">
|
|
|
+<img :src="imagePreview" alt="Uploaded Image" class="image-preview"/>
|
|
|
+<button @click="imagePreview = null; imageUrl = null;" class="remove-image-button">
|
|
|
+<i class="fas fa-times"></i>
|
|
|
+</button>
|
|
|
+</div>
|
|
|
+<textarea :disabled="generating" :placeholder="generating ? 'Generating...' : 'Say something'" @input="
|
|
|
home = (home === 0) ? 1 : home
|
|
|
- if (cstate.messages.length === 0 && $el.value === '') home = -1;
|
|
|
+ if (cstate.messages.length === 0 && $el.value === '') home = -1;
|
|
|
|
|
|
if ($el.value !== '') {
|
|
|
const messages = [...cstate.messages];
|
|
@@ -183,19 +173,17 @@
|
|
|
if (cstate.messages.length === 0) total_tokens = 0;
|
|
|
// else updateTotalTokens(cstate.messages);
|
|
|
}
|
|
|
- " x-effect="
|
|
|
+ " @keydown.enter="await handleEnter($event)" @keydown.escape.window="$focus.focus($el)" autofocus="" class="input-form" id="input-form" rows="1" x-autosize="" x-effect="
|
|
|
console.log(generating);
|
|
|
- if (!generating) $nextTick(() => {
|
|
|
+ if (!generating) $nextTick(() => {
|
|
|
$el.focus();
|
|
|
- setTimeout(() => $refs.messages.scrollTo({ top: $refs.messages.scrollHeight, behavior: 'smooth' }), 100);
|
|
|
+ setTimeout(() => $refs.messages.scrollTo({ top: $refs.messages.scrollHeight, behavior: 'smooth' }), 100);
|
|
|
});
|
|
|
- " @keydown.enter="await handleEnter($event)" @keydown.escape.window="$focus.focus($el)"></textarea>
|
|
|
- <button class="input-button" :disabled="generating" @click="await handleSend()">
|
|
|
- <i class="fas" :class="generating ? 'fa-spinner fa-spin' : 'fa-paper-plane'"></i>
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </main>
|
|
|
+ " x-ref="inputForm"></textarea>
|
|
|
+<button :disabled="generating" @click="await handleSend()" class="input-button">
|
|
|
+<i :class="generating ? 'fa-spinner fa-spin' : 'fa-paper-plane'" class="fas"></i>
|
|
|
+</button>
|
|
|
+</div>
|
|
|
+</div>
|
|
|
+</main>
|
|
|
</body>
|
|
|
-
|
|
|
-</html>
|