mirror of
https://github.com/informaticker/cchat.git
synced 2024-11-23 18:11:56 +01:00
201 lines
10 KiB
HTML
201 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
|
|
<head>
|
|
<title>cchat</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" 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="{{ url_for('static', filename='js/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="{{ url_for('static', filename='css/index.css') }}">
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/common.css') }}">
|
|
</head>
|
|
|
|
<body>
|
|
<main x-data="state" x-init="console.log(endpoint)">
|
|
<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="
|
|
if (home === 2) {
|
|
home = -1;
|
|
cstate = { time: null, messages: [] };
|
|
time_till_first = 0;
|
|
tokens_per_second = 0;
|
|
total_tokens = 0;
|
|
}
|
|
">
|
|
<h1 class="title megrim-regular">cchat</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="
|
|
loadChat(_state);
|
|
// 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);
|
|
$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>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<div x-ref="messages" class="messages" x-init="
|
|
$watch('cstate', value => {
|
|
$el.innerHTML = '';
|
|
value.messages.forEach(({ role, content }) => {
|
|
const div = document.createElement('div');
|
|
div.className = `message message-role-${role}`;
|
|
try {
|
|
div.innerHTML = DOMPurify.sanitize(marked.parse(content));
|
|
} catch (e) {
|
|
console.log(content);
|
|
console.error(e);
|
|
}
|
|
|
|
// add a clipboard button to all code blocks
|
|
const codeBlocks = div.querySelectorAll('pre code');
|
|
codeBlocks.forEach(codeBlock => {
|
|
const button = document.createElement('button');
|
|
button.className = 'clipboard-button';
|
|
button.innerHTML = '<i class=\'fas fa-clipboard\'></i>';
|
|
button.onclick = () => {
|
|
navigator.clipboard.writeText(codeBlock.textContent);
|
|
button.innerHTML = '<i class=\'fas fa-check\'></i>';
|
|
setTimeout(() => button.innerHTML = '<i class=\'fas fa-clipboard\'></i>', 1000);
|
|
};
|
|
codeBlock.parentNode.insertBefore(button, codeBlock);
|
|
});
|
|
|
|
$el.appendChild(div);
|
|
});
|
|
|
|
$el.scrollTo({ top: $el.scrollHeight, behavior: 'smooth' });
|
|
});
|
|
" 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"></p>
|
|
<p class="megrim-regular">TTFT</p>
|
|
</span>
|
|
<span class="input-performance-point">
|
|
<p class="monospace" x-text="tokens_per_second.toFixed(0)"></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">
|
|
<textarea x-ref="inputForm" id="input-form" class="input-form" :class="{ 'input-form-generating': generating }" autofocus rows=1 x-autosize
|
|
:placeholder="generating ? 'Generating...' : 'Say something'" :disabled="generating" @input="
|
|
home = (home === 0) ? 1 : home
|
|
if (cstate.messages.length === 0 && $el.value === '') home = -1;
|
|
|
|
if ($el.value !== '') {
|
|
const messages = [...cstate.messages];
|
|
messages.push({ role: 'user', content: $el.value });
|
|
updateTotalTokens(messages);
|
|
} else {
|
|
if (cstate.messages.length === 0) total_tokens = 0;
|
|
else updateTotalTokens(cstate.messages);
|
|
}
|
|
" x-effect="
|
|
console.log(generating);
|
|
if (!generating) $nextTick(() => {
|
|
$el.focus();
|
|
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 class="menu-container" x-data="{ isOpen: false }">
|
|
<button class="menu-button" @click="isOpen = !isOpen">
|
|
<i class="fas fa-bars"></i>
|
|
</button>
|
|
<div class="menu-dropdown" x-show="isOpen" @click.away="isOpen = false">
|
|
<button @click="
|
|
home = 0;
|
|
cstate = { time: null, messages: [] };
|
|
time_till_first = 0;
|
|
tokens_per_second = 0;
|
|
total_tokens = 0;
|
|
isOpen = false;
|
|
">
|
|
<i class="fas fa-plus"></i> New Chat
|
|
</button>
|
|
<button @click="debug = !debug; isOpen = false;">
|
|
<i class="fas" :class="debug ? 'fa-toggle-on' : 'fa-toggle-off'"></i> Debug Mode
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div x-show="errorMessage" x-transition:enter="transition ease-out duration-500"
|
|
x-transition:enter-start="opacity-0 transform translate-y-10 scale-95"
|
|
x-transition:enter-end="opacity-100 transform translate-y-0 scale-100"
|
|
x-transition:leave="transition ease-in duration-300"
|
|
x-transition:leave-start="opacity-100 transform translate-y-0 scale-100"
|
|
x-transition:leave-end="opacity-0 transform translate-y-10 scale-95" @click="errorMessage = null"
|
|
class="error-toast"
|
|
x-init="$el.style.animation = 'shake 0.82s cubic-bezier(.36,.07,.19,.97) both'">
|
|
<div x-text="errorMessage"></div>
|
|
</div>
|
|
</main>
|
|
</body>
|
|
|
|
</html>
|