staeiou's picture
Upload folder using huggingface_hub
365084a verified
Raw
History Blame
9.69 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>BartlebyGPT</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #fafafa;
--surface: #ffffff;
--border: #d4d4d4;
--border-focus: #888;
--text: #1a1a1a;
--text-muted: #666;
--user-bg: #e8e8e8;
--bot-bg: #f0f0f0;
--accent: #333;
--accent-text: #fff;
--input-bg: #fff;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #111;
--surface: #1a1a1a;
--border: #333;
--border-focus: #666;
--text: #e5e5e5;
--text-muted: #999;
--user-bg: #2a2a2a;
--bot-bg: #222;
--accent: #e5e5e5;
--accent-text: #111;
--input-bg: #1a1a1a;
}
}
html, body {
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 15px;
line-height: 1.5;
background: var(--bg);
color: var(--text);
}
#app {
display: flex;
flex-direction: column;
height: 100dvh;
max-width: 720px;
margin: 0 auto;
padding: 8px;
}
/* -- Header / Model Loader -- */
#header {
flex: 0 0 auto;
padding: 8px 0;
text-align: center;
}
#header h1 {
font-size: 1.1em;
font-weight: 600;
margin-bottom: 6px;
}
#model-loader {
display: flex;
gap: 6px;
align-items: center;
}
#model-loader input {
flex: 1;
padding: 6px 10px;
border: 1.5px solid var(--border);
border-radius: 6px;
background: var(--input-bg);
color: var(--text);
font-size: 13px;
outline: none;
}
#model-loader input:focus {
border-color: var(--border-focus);
}
#load-btn {
padding: 6px 14px;
border: none;
border-radius: 6px;
background: var(--accent);
color: var(--accent-text);
font-size: 13px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
}
#load-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
#status {
font-size: 12px;
color: var(--text-muted);
margin-top: 4px;
min-height: 1.4em;
}
/* -- Chat Area -- */
#chat {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
padding: 12px 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.msg-row {
display: flex;
max-width: 100%;
}
.msg-row.user { justify-content: flex-start; }
.msg-row.bot { justify-content: flex-end; }
.msg-bubble {
max-width: 85%;
padding: 10px 14px;
border-radius: 14px;
font-size: 14px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.msg-row.user .msg-bubble {
background: var(--user-bg);
border-bottom-left-radius: 4px;
}
.msg-row.bot .msg-bubble {
background: var(--bot-bg);
border-bottom-right-radius: 4px;
}
/* -- Input Area -- */
#input-area {
flex: 0 0 auto;
padding: 6px 0;
display: flex;
gap: 6px;
}
#msg-input {
flex: 1;
padding: 8px 12px;
border: 1.5px solid var(--border);
border-radius: 8px;
background: var(--input-bg);
color: var(--text);
font-size: 15px;
outline: none;
resize: none;
height: 2.6em;
line-height: 1.25;
}
#msg-input:focus {
border-color: var(--border-focus);
}
#send-btn {
padding: 8px 18px;
border: none;
border-radius: 8px;
background: var(--accent);
color: var(--accent-text);
font-size: 15px;
font-weight: 600;
cursor: pointer;
}
#send-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* -- Params Accordion -- */
#params-toggle {
flex: 0 0 auto;
padding: 4px 0;
}
#params-toggle summary {
cursor: pointer;
font-size: 13px;
color: var(--text-muted);
user-select: none;
}
#params-content {
display: flex;
gap: 12px;
flex-wrap: wrap;
padding: 8px 0;
}
.param-group {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
min-width: 120px;
}
.param-group label {
font-size: 12px;
color: var(--text-muted);
}
.param-group input[type="range"] {
width: 100%;
}
.param-group .param-val {
font-size: 11px;
color: var(--text-muted);
text-align: right;
}
/* -- Generating indicator -- */
.generating::after {
content: "▌";
animation: blink 0.6s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
</style>
</head>
<body>
<div id="app">
<div id="header">
<h1>BartlebyGPT</h1>
<div id="model-loader">
<input type="text" id="repo-input" value="staeiou/bartleby-qwen3-4b-2507_v2_onnx"
placeholder="HF ONNX repo ID">
<button id="load-btn" onclick="loadModel()">Load Model</button>
</div>
<div id="status">Enter a Hugging Face ONNX model repo and click Load Model.</div>
</div>
<div id="chat"></div>
<div id="input-area">
<input type="text" id="msg-input" placeholder="What do you want?"
disabled autocomplete="off">
<button id="send-btn" onclick="sendMessage()" disabled>Send</button>
</div>
<details id="params-toggle">
<summary>Params</summary>
<div id="params-content">
<div class="param-group">
<label>Max tokens</label>
<input type="range" id="p-max-tokens" min="1" max="512" value="256"
oninput="this.nextElementSibling.textContent=this.value">
<span class="param-val">256</span>
</div>
<div class="param-group">
<label>Temperature</label>
<input type="range" id="p-temp" min="0" max="2" step="0.05" value="0.7"
oninput="this.nextElementSibling.textContent=this.value">
<span class="param-val">0.7</span>
</div>
<div class="param-group">
<label>Top-p</label>
<input type="range" id="p-top-p" min="0.1" max="1" step="0.05" value="0.9"
oninput="this.nextElementSibling.textContent=this.value">
<span class="param-val">0.9</span>
</div>
</div>
</details>
</div>
<script type="module">
import { AutoTokenizer, AutoModelForCausalLM, TextStreamer } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3";
let tokenizer = null;
let model = null;
let generating = false;
const chatEl = document.getElementById("chat");
const msgInput = document.getElementById("msg-input");
const sendBtn = document.getElementById("send-btn");
const loadBtn = document.getElementById("load-btn");
const repoInput = document.getElementById("repo-input");
const statusEl = document.getElementById("status");
function setStatus(msg) {
statusEl.textContent = msg;
}
function setInputEnabled(enabled) {
msgInput.disabled = !enabled;
sendBtn.disabled = !enabled;
}
function addMessage(role, text) {
const row = document.createElement("div");
row.className = `msg-row ${role}`;
const bubble = document.createElement("div");
bubble.className = "msg-bubble";
bubble.textContent = text;
row.appendChild(bubble);
chatEl.appendChild(row);
chatEl.scrollTop = chatEl.scrollHeight;
return bubble;
}
window.loadModel = async function () {
const repo = repoInput.value.trim();
if (!repo) return;
loadBtn.disabled = true;
setInputEnabled(false);
setStatus("Loading tokenizer...");
try {
tokenizer = await AutoTokenizer.from_pretrained(repo);
setStatus("Loading model (this may take a while for large models)...");
model = await AutoModelForCausalLM.from_pretrained(repo, {
dtype: "q8",
use_external_data_format: 2,
progress_callback: (progress) => {
if (progress.status === "progress" && progress.progress != null) {
setStatus(`Loading model... ${Math.round(progress.progress)}%`);
}
},
});
setStatus("Model loaded. Start chatting!");
setInputEnabled(true);
msgInput.focus();
} catch (e) {
setStatus(`Error: ${e.message}`);
console.error(e);
} finally {
loadBtn.disabled = false;
}
};
window.sendMessage = async function () {
if (!model || !tokenizer || generating) return;
let userText = (msgInput.value || "").trim();
if (!userText) return;
// Truncate to 2000 chars
if (userText.length > 2000) userText = userText.slice(0, 2000);
msgInput.value = "";
addMessage("user", userText);
// Only send last message (not full history)
const messages = [{ role: "user", content: userText }];
const text = tokenizer.apply_chat_template(messages, {
tokenize: false,
add_generation_prompt: true,
});
const inputs = tokenizer(text, { return_tensors: "pt" });
const inputLen = inputs.input_ids.dims[1];
const botBubble = addMessage("bot", "");
botBubble.classList.add("generating");
generating = true;
setInputEnabled(false);
const maxTokens = parseInt(document.getElementById("p-max-tokens").value);
const temperature = parseFloat(document.getElementById("p-temp").value);
const topP = parseFloat(document.getElementById("p-top-p").value);
try {
// Streamer that updates the bubble token by token
const streamer = new TextStreamer(tokenizer, {
skip_prompt: true,
skip_special_tokens: true,
callback_function: (text) => {
botBubble.textContent += text;
chatEl.scrollTop = chatEl.scrollHeight;
},
});
await model.generate({
...inputs,
max_new_tokens: maxTokens,
do_sample: temperature > 0,
temperature: temperature > 0 ? temperature : undefined,
top_p: topP,
streamer,
});
} catch (e) {
botBubble.textContent += `\n[Error: ${e.message}]`;
console.error(e);
} finally {
botBubble.classList.remove("generating");
generating = false;
setInputEnabled(true);
msgInput.focus();
}
};
// Enter to send
msgInput.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
window.sendMessage();
}
});
</script>
</body>
</html>