mirror of
https://github.com/informaticker/cchat.git
synced 2024-11-23 18:11:56 +01:00
Various fixes
This commit is contained in:
parent
cd5ad0b61a
commit
7f0ee0e8c9
89
app.py
89
app.py
@ -1,64 +1,83 @@
|
|||||||
from flask import Flask, request, jsonify, render_template, Response, stream_with_context
|
|
||||||
from transformers import AutoTokenizer
|
|
||||||
import os
|
import os
|
||||||
import requests
|
from flask import Flask, request, jsonify, render_template, Response, stream_with_context
|
||||||
from flask_limiter import Limiter
|
from flask_limiter import Limiter
|
||||||
from flask_limiter.util import get_remote_address
|
from flask_limiter.util import get_remote_address
|
||||||
|
from transformers import AutoTokenizer
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Initialize rate limiter
|
# Initialize rate limiter
|
||||||
limiter = Limiter(
|
limiter = Limiter(
|
||||||
get_remote_address,
|
get_remote_address,
|
||||||
app=app,
|
app=app,
|
||||||
storage_uri="memory://"
|
storage_uri="memory://",
|
||||||
|
default_limits=[os.getenv('RATE_LIMIT', '15 per minute')]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Load the tokenizer
|
# Load the tokenizer
|
||||||
tokenizer = AutoTokenizer.from_pretrained(os.environ.get('TOKENIZER', 'gpt2'))
|
tokenizer = AutoTokenizer.from_pretrained(os.getenv('TOKENIZER', 'gpt2'))
|
||||||
|
|
||||||
api_url = os.environ.get('API_URL', 'https://api.openai.com/v1')
|
# API configuration
|
||||||
api_key = os.environ.get('API_KEY')
|
API_URL = os.getenv('API_URL', 'https://api.openai.com/v1')
|
||||||
api_model = os.environ.get('API_MODEL', 'gpt-3.5-turbo')
|
API_KEY = os.getenv('API_KEY')
|
||||||
temperature = int(os.environ.get('TEMPERATURE', 0))
|
API_MODEL = os.getenv('API_MODEL', 'gpt-3.5-turbo')
|
||||||
|
TEMPERATURE = float(os.getenv('TEMPERATURE', 0))
|
||||||
|
|
||||||
|
logger.info(f"Chat initialized using endpoint: {API_URL}, model: {API_MODEL}, temperature: {TEMPERATURE}")
|
||||||
|
|
||||||
@app.route('/v1/tokenizer/count', methods=['POST'])
|
@app.route('/v1/tokenizer/count', methods=['POST'])
|
||||||
def token_count():
|
def token_count():
|
||||||
data = request.json
|
try:
|
||||||
messages = data.get('messages', [])
|
data = request.json
|
||||||
full_text = " ".join([f"{msg['role']}: {msg['content']}" for msg in messages])
|
messages = data.get('messages', [])
|
||||||
tokens = tokenizer.encode(full_text)
|
full_text = " ".join([f"{msg['role']}: {msg['content']}" for msg in messages])
|
||||||
token_count = len(tokens)
|
tokens = tokenizer.encode(full_text)
|
||||||
return jsonify({"token_count": token_count})
|
return jsonify({"token_count": len(tokens)})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in token_count: {str(e)}")
|
||||||
|
return jsonify({"error": "Invalid request"}), 400
|
||||||
|
|
||||||
@app.route('/v1/chat/completions', methods=['POST'])
|
@app.route('/v1/chat/completions', methods=['POST'])
|
||||||
@limiter.limit(os.environ.get('RATE_LIMIT', '20/minute'))
|
@limiter.limit(os.getenv('RATE_LIMIT', '15/minute'))
|
||||||
def proxy_chat_completions():
|
def proxy_chat_completions():
|
||||||
headers = {
|
headers = {
|
||||||
'Authorization': f'Bearer {api_key}',
|
'Authorization': f'Bearer {API_KEY}',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
|
|
||||||
request_data = request.json
|
try:
|
||||||
|
request_data = request.json
|
||||||
|
request_data['model'] = API_MODEL
|
||||||
|
request_data['temperature'] = TEMPERATURE
|
||||||
|
request_data['stream'] = True
|
||||||
|
|
||||||
request_data['model'] = api_model
|
response = requests.post(f"{API_URL}/chat/completions",
|
||||||
request_data['temperature'] = temperature
|
json=request_data,
|
||||||
|
headers=headers,
|
||||||
|
stream=True)
|
||||||
|
|
||||||
request_data['stream'] = True
|
response.raise_for_status()
|
||||||
|
|
||||||
response = requests.post(f"{api_url}/chat/completions",
|
def generate():
|
||||||
json=request_data,
|
for chunk in response.iter_content(chunk_size=8):
|
||||||
headers=headers,
|
if chunk:
|
||||||
stream=True)
|
yield chunk
|
||||||
|
|
||||||
# Stream the response back to the client
|
return Response(stream_with_context(generate()),
|
||||||
def generate():
|
content_type=response.headers['content-type'])
|
||||||
for chunk in response.iter_content(chunk_size=8):
|
|
||||||
if chunk:
|
|
||||||
yield chunk
|
|
||||||
|
|
||||||
return Response(stream_with_context(generate()),
|
except requests.RequestException as e:
|
||||||
content_type=response.headers['content-type'])
|
logger.error(f"API request failed: {str(e)}")
|
||||||
|
return jsonify({"error": "Failed to connect to the API"}), 503
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error: {str(e)}")
|
||||||
|
return jsonify({"error": "An unexpected error occurred"}), 500
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
@ -68,5 +87,9 @@ def index():
|
|||||||
def serve_static(filename):
|
def serve_static(filename):
|
||||||
return app.send_static_file(filename)
|
return app.send_static_file(filename)
|
||||||
|
|
||||||
|
@app.errorhandler(429)
|
||||||
|
def ratelimit_handler(e):
|
||||||
|
return jsonify({"error": "Rate limit exceeded. Please try again later."}), 429
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=False, port=5000)
|
app.run(debug=False, port=int(os.getenv('PORT', 5000)))
|
||||||
|
@ -366,3 +366,51 @@ p {
|
|||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error-toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background-color: var(--red-color);
|
||||||
|
color: var(--foreground-color);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
max-width: 300px;
|
||||||
|
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 1000;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-toast:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 12px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.error-toast {
|
||||||
|
left: 20px;
|
||||||
|
right: 20px;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shake {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
10%,
|
||||||
|
30%,
|
||||||
|
50%,
|
||||||
|
70%,
|
||||||
|
90% {
|
||||||
|
transform: translateX(-5px);
|
||||||
|
}
|
||||||
|
20%,
|
||||||
|
40%,
|
||||||
|
60%,
|
||||||
|
80% {
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -12,7 +12,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
home: 0,
|
home: 0,
|
||||||
generating: false,
|
generating: false,
|
||||||
endpoint: window.location.origin + "/v1",
|
endpoint: window.location.origin + "/v1",
|
||||||
model: "llama3-8b-8192", // This doesen't matter anymore as the backend handles it now
|
model: "llama3-8b-8192", // This doesn't matter anymore as the backend handles it now
|
||||||
stopToken: "<|eot_id|>", // We may need this for some models
|
stopToken: "<|eot_id|>", // We may need this for some models
|
||||||
|
|
||||||
// performance tracking
|
// performance tracking
|
||||||
@ -20,6 +20,9 @@ document.addEventListener("alpine:init", () => {
|
|||||||
tokens_per_second: 0,
|
tokens_per_second: 0,
|
||||||
total_tokens: 0,
|
total_tokens: 0,
|
||||||
|
|
||||||
|
// New property for error messages
|
||||||
|
errorMessage: null,
|
||||||
|
|
||||||
removeHistory(cstate) {
|
removeHistory(cstate) {
|
||||||
const index = this.histories.findIndex((state) => {
|
const index = this.histories.findIndex((state) => {
|
||||||
return state.time === cstate.time;
|
return state.time === cstate.time;
|
||||||
@ -37,6 +40,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
|
|
||||||
if (this.generating) return;
|
if (this.generating) return;
|
||||||
this.generating = true;
|
this.generating = true;
|
||||||
|
this.errorMessage = null; // Clear any previous error messages
|
||||||
if (this.home === 0) this.home = 1;
|
if (this.home === 0) this.home = 1;
|
||||||
|
|
||||||
// ensure that going back in history will go back to home
|
// ensure that going back in history will go back to home
|
||||||
@ -56,48 +60,56 @@ document.addEventListener("alpine:init", () => {
|
|||||||
let tokens = 0;
|
let tokens = 0;
|
||||||
this.tokens_per_second = 0;
|
this.tokens_per_second = 0;
|
||||||
|
|
||||||
// start receiving server sent events
|
try {
|
||||||
let gottenFirstChunk = false;
|
// start receiving server sent events
|
||||||
for await (const chunk of this.openaiChatCompletion(
|
let gottenFirstChunk = false;
|
||||||
this.cstate.messages,
|
for await (const chunk of this.openaiChatCompletion(
|
||||||
)) {
|
this.cstate.messages,
|
||||||
if (!gottenFirstChunk) {
|
)) {
|
||||||
this.cstate.messages.push({ role: "assistant", content: "" });
|
if (!gottenFirstChunk) {
|
||||||
gottenFirstChunk = true;
|
this.cstate.messages.push({ role: "assistant", content: "" });
|
||||||
}
|
gottenFirstChunk = true;
|
||||||
|
}
|
||||||
|
|
||||||
// add chunk to the last message
|
// add chunk to the last message
|
||||||
this.cstate.messages[this.cstate.messages.length - 1].content += chunk;
|
this.cstate.messages[this.cstate.messages.length - 1].content +=
|
||||||
|
chunk;
|
||||||
|
|
||||||
// calculate performance tracking
|
// calculate performance tracking
|
||||||
tokens += 1;
|
tokens += 1;
|
||||||
this.total_tokens += 1;
|
this.total_tokens += 1;
|
||||||
if (start_time === 0) {
|
if (start_time === 0) {
|
||||||
start_time = Date.now();
|
start_time = Date.now();
|
||||||
this.time_till_first = start_time - prefill_start;
|
this.time_till_first = start_time - prefill_start;
|
||||||
} else {
|
} else {
|
||||||
const diff = Date.now() - start_time;
|
const diff = Date.now() - start_time;
|
||||||
if (diff > 0) {
|
if (diff > 0) {
|
||||||
this.tokens_per_second = tokens / (diff / 1000);
|
this.tokens_per_second = tokens / (diff / 1000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// update the state in histories or add it if it doesn't exist
|
// update the state in histories or add it if it doesn't exist
|
||||||
const index = this.histories.findIndex((cstate) => {
|
const index = this.histories.findIndex((cstate) => {
|
||||||
return cstate.time === this.cstate.time;
|
return cstate.time === this.cstate.time;
|
||||||
});
|
});
|
||||||
this.cstate.time = Date.now();
|
this.cstate.time = Date.now();
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
// update the time
|
// update the time
|
||||||
this.histories[index] = this.cstate;
|
this.histories[index] = this.cstate;
|
||||||
} else {
|
} else {
|
||||||
this.histories.push(this.cstate);
|
this.histories.push(this.cstate);
|
||||||
|
}
|
||||||
|
// update in local storage
|
||||||
|
localStorage.setItem("histories", JSON.stringify(this.histories));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in handleSend:", error);
|
||||||
|
this.showError(
|
||||||
|
error.message || "An error occurred processing your request.",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.generating = false;
|
||||||
}
|
}
|
||||||
// update in local storage
|
|
||||||
localStorage.setItem("histories", JSON.stringify(this.histories));
|
|
||||||
|
|
||||||
this.generating = false;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async handleEnter(event) {
|
async handleEnter(event) {
|
||||||
@ -108,67 +120,90 @@ document.addEventListener("alpine:init", () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showError(message) {
|
||||||
|
this.errorMessage = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.errorMessage = null;
|
||||||
|
}, 3000); // Hide after 5 seconds
|
||||||
|
},
|
||||||
|
|
||||||
updateTotalTokens(messages) {
|
updateTotalTokens(messages) {
|
||||||
fetch(`${window.location.origin}/v1/tokenizer/count`, {
|
fetch(`${window.location.origin}/v1/tokenizer/count`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ messages }),
|
body: JSON.stringify({ messages }),
|
||||||
})
|
})
|
||||||
.then((response) => response.json())
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to count tokens");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.total_tokens = data.token_count;
|
this.total_tokens = data.token_count;
|
||||||
})
|
})
|
||||||
.catch(console.error);
|
.catch((error) => {
|
||||||
|
console.error("Error updating total tokens:", error);
|
||||||
|
this.showError("Failed to update token count. Please try again.");
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async *openaiChatCompletion(messages) {
|
async *openaiChatCompletion(messages) {
|
||||||
// stream response
|
try {
|
||||||
const response = await fetch(`${this.endpoint}/chat/completions`, {
|
const response = await fetch(`${this.endpoint}/chat/completions`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${this.apiKey}`,
|
Authorization: `Bearer ${this.apiKey}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: this.model,
|
model: this.model,
|
||||||
messages: messages,
|
messages: messages,
|
||||||
stream: true,
|
stream: true,
|
||||||
stop: [this.stopToken],
|
stop: [this.stopToken],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to fetch");
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = response.body.getReader();
|
if (!response.ok) {
|
||||||
const decoder = new TextDecoder("utf-8");
|
const errorData = await response.json();
|
||||||
let buffer = "";
|
throw new Error(errorData.error || "Failed to fetch");
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
buffer += decoder.decode(value, { stream: true });
|
|
||||||
const lines = buffer.split("\n");
|
|
||||||
buffer = lines.pop();
|
|
||||||
|
|
||||||
for (const line of lines) {
|
const reader = response.body.getReader();
|
||||||
if (line.startsWith("data: ")) {
|
const decoder = new TextDecoder("utf-8");
|
||||||
const data = line.slice(6);
|
let buffer = "";
|
||||||
if (data === "[DONE]") {
|
|
||||||
return;
|
while (true) {
|
||||||
}
|
const { done, value } = await reader.read();
|
||||||
try {
|
if (done) break;
|
||||||
const json = JSON.parse(data);
|
|
||||||
if (json.choices && json.choices[0].delta.content) {
|
buffer += decoder.decode(value, { stream: true });
|
||||||
yield json.choices[0].delta.content;
|
const lines = buffer.split("\n");
|
||||||
|
buffer = lines.pop();
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith("data: ")) {
|
||||||
|
const data = line.slice(6);
|
||||||
|
if (data === "[DONE]") return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(data);
|
||||||
|
if (json.choices && json.choices[0].delta.content) {
|
||||||
|
yield json.choices[0].delta.content;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error parsing JSON:", error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error("Error parsing JSON:", error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in openaiChatCompletion:", error);
|
||||||
|
this.showError(
|
||||||
|
error.message ||
|
||||||
|
"An error occurred while communicating with the server.",
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -1,178 +1,190 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>cchat</title>
|
<title>cchat</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/svg+xml">
|
<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/@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/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://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/@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 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/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@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/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="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js"></script>
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/index.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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Megrim&display=swap" rel="stylesheet">
|
<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://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"
|
<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=="
|
integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A=="
|
||||||
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
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="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/index.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/common.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/common.css') }}">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<main x-data="state" x-init="console.log(endpoint)">
|
<main x-data="state" x-init="console.log(endpoint)">
|
||||||
<button class="new-chat-button" @click="
|
<button class="new-chat-button" @click="
|
||||||
home = 0;
|
home = 0;
|
||||||
cstate = { time: null, messages: [] };
|
cstate = { time: null, messages: [] };
|
||||||
time_till_first = 0;
|
time_till_first = 0;
|
||||||
tokens_per_second = 0;
|
tokens_per_second = 0;
|
||||||
total_tokens = 0;
|
total_tokens = 0;
|
||||||
">
|
|
||||||
<i class="fas fa-plus"></i>
|
|
||||||
<span class="new-chat-text">New Chat</span>
|
|
||||||
</button>
|
|
||||||
<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)">
|
<i class="fas fa-plus"></i>
|
||||||
<div x-data="{ otx: 0, trigger: 75 }" class="history" @click="
|
<span class="new-chat-text">New Chat</span>
|
||||||
cstate = _state;
|
|
||||||
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);
|
|
||||||
$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 => {
|
|
||||||
$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" 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>
|
</button>
|
||||||
</div>
|
<div class="home centered" x-show="home === 0" x-transition x-effect="
|
||||||
</div>
|
$refs.inputForm.focus();
|
||||||
</main>
|
if (home === 1) setTimeout(() => home = 2, 100);
|
||||||
</body>
|
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="
|
||||||
|
cstate = _state;
|
||||||
|
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);
|
||||||
|
$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 => {
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
|
||||||
</html>
|
// 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" 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>
|
||||||
|
</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>
|
||||||
|
Loading…
Reference in New Issue
Block a user