Inkling / index.html
akhaliq's picture
akhaliq HF Staff
Switch inference to Tinker production endpoint (docs snippet)
4eb5082
Raw
History Blame Contribute Delete
20.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Inkling · Chat</title>
<style>
:root {
--bg: #fdfcfb;
--paper: #fff;
--ink: #1a1a1a;
--ink-soft: #6b6964;
--ink-faint: #b0ada6;
--accent: #4a3f35;
--border: #e6e4df;
}
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
font-family: Georgia, "Times New Roman", "Libre Baskerville", serif;
background: var(--bg);
color: var(--ink);
display: flex;
flex-direction: column;
height: 100vh;
letter-spacing: 0.01em;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 22px 28px;
background: var(--paper);
border-bottom: 1px solid var(--border);
}
.brand { display: flex; align-items: center; gap: 14px; }
.brand img { width: 36px; height: 36px; border-radius: 50%; object-fit: cover; display: block; }
.brand h1 { font-size: 26px; font-weight: 300; letter-spacing: 0.08em; margin: 0; color: var(--ink); text-transform: uppercase; }
.brand .sub { font-size: 12px; color: var(--ink-soft); letter-spacing: 0.06em; }
.playground-link {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
text-decoration: none; transition: background .15s, border-color .15s, transform .15s;
letter-spacing: 0.02em; cursor: pointer;
}
.playground-link.secondary {
font-size: 13px; color: var(--ink-soft);
padding: 7px 12px; border: 1px solid var(--border); border-radius: 8px;
background: var(--paper);
}
.playground-link.secondary:hover { background: #f2f1ed; color: var(--ink); border-color: var(--ink-faint); }
.playground-link.primary {
display: inline-flex; align-items: center; gap: 10px;
font-size: 15px; font-weight: 500; color: var(--paper);
background: var(--ink); padding: 12px 22px; border-radius: 24px;
border: 1px solid var(--ink);
}
.playground-link.primary:hover { background: #000; border-color: #000; transform: translateY(-1px); }
.playground-link.primary .hero-tag {
font-size: 11px; font-weight: 400; color: rgba(255,255,255,0.7);
padding: 3px 8px; border-radius: 10px; background: rgba(255,255,255,0.12);
text-transform: uppercase; letter-spacing: 0.08em;
}
main { flex: 1; overflow-y: auto; padding: 24px 0; }
#messages { max-width: 820px; margin: 0 auto; padding: 0 20px; display: flex; flex-direction: column; gap: 16px; }
.msg { display: flex; flex-direction: column; gap: 6px; }
.msg .who { font-size: 11px; color: var(--ink-faint); padding-left: 4px; letter-spacing: 0.06em; text-transform: uppercase; }
.bubble {
padding: 13px 16px;
border-radius: 14px;
line-height: 1.55;
font-size: 15px;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: anywhere;
}
.msg.user .bubble { background: #f2f1ed; border: none; align-self: flex-end; max-width: 78%; color: var(--ink); }
.msg.assistant .bubble { background: var(--paper); border: 1px solid var(--border); align-self: flex-start; max-width: 92%; color: var(--ink); box-shadow: 0 1px 4px rgba(0,0,0,0.04); }
.bubble.error { color: #b94c4c; border-color: #b94c4c; }
.empty { text-align: center; color: var(--ink-soft); margin-top: 60px; font-size: 14px; }
.empty-hero { display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 24px 16px; }
.hero-logo { width: 72px; height: 72px; border-radius: 50%; object-fit: cover; }
.hero-title { font-size: 38px; font-weight: 300; letter-spacing: 0.04em; margin: 0; color: var(--ink); }
.hero-sub { font-size: 15px; color: var(--ink-soft); margin: 0 0 12px; }
.cursor::after { content: "▋"; color: var(--accent); animation: blink 1s steps(2) infinite; }
@keyframes blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }
.composer { border-top: 1px solid var(--border); background: var(--paper); padding: 14px 20px 18px; }
.composer-inner { max-width: 820px; margin: 0 auto; display: flex; gap: 10px; align-items: flex-end; }
/* Image attachment preview chips above the textarea */
.attachments { display: flex; flex-wrap: wrap; gap: 6px; max-width: 820px; margin: 0 auto 6px; padding: 0; }
.attachment {
display: flex; align-items: center; gap: 6px;
background: #f2f1ed; border: 1px solid var(--border); border-radius: 10px;
padding: 4px 8px 4px 4px; font-size: 12px; color: var(--ink);
}
.attachment img { width: 28px; height: 28px; border-radius: 6px; object-fit: cover; display: block; }
.attachment .remove { cursor: pointer; color: var(--ink-soft); font-size: 14px; padding: 0 2px; }
.attachment .remove:hover { color: #b94c4c; }
/* Composer textarea */
#input {
flex: 1; background: var(--bg); color: var(--ink); border: 1px solid var(--border);
border-radius: 14px; padding: 13px 16px; font-size: 15px; resize: none;
max-height: 200px; min-height: 46px; outline: none; font-family: inherit; line-height: 1.4;
}
#input:focus { border-color: var(--ink-faint); }
/* Image upload button — paper pill with paperclip */
.icon-btn {
background: var(--paper); border: 1px solid var(--border); border-radius: 50%;
width: 42px; height: 42px; display: flex; align-items: center; justify-content: center;
cursor: pointer; color: var(--ink-soft); transition: background 0.15s, color 0.15s;
flex: 0 0 auto;
}
.icon-btn:hover { background: #f2f1ed; color: var(--ink); }
.icon-btn svg { width: 18px; height: 18px; }
.icon-btn input[type="file"] { display: none; }
/* Send button — round, dark, arrow */
button#send {
background: var(--ink); color: var(--paper); border: none; border-radius: 50%;
width: 42px; height: 42px; font-size: 16px; font-family: Georgia, serif; cursor: pointer;
flex: 0 0 auto; transition: opacity 0.15s;
}
button#send:disabled { opacity: 0.4; cursor: not-allowed; }
/* Docs modal */
.docs-overlay {
position: fixed; inset: 0; background: rgba(26, 26, 26, 0.45);
display: none; align-items: center; justify-content: center; z-index: 100;
padding: 20px;
}
.docs-overlay.open { display: flex; }
.docs-modal {
background: var(--paper); border-radius: 16px; max-width: 720px; width: 100%;
max-height: 80vh; overflow: auto; box-shadow: 0 20px 60px rgba(0,0,0,0.25);
border: 1px solid var(--border);
}
.docs-header {
display: flex; align-items: center; justify-content: space-between;
padding: 18px 24px; border-bottom: 1px solid var(--border);
}
.docs-header h3 { margin: 0; font-size: 18px; font-weight: 400; letter-spacing: 0.04em; }
.docs-close {
background: none; border: none; font-size: 22px; cursor: pointer; color: var(--ink-soft);
padding: 0 4px; line-height: 1;
}
.docs-close:hover { color: var(--ink); }
.docs-body { padding: 20px 24px 24px; font-size: 14px; line-height: 1.55; color: var(--ink); }
.docs-body p { margin: 0 0 14px; }
.docs-body code {
font-family: "SF Mono", "Menlo", "Monaco", "Courier New", monospace;
font-size: 13px; background: #f4f2ed; padding: 1px 5px; border-radius: 4px;
}
.docs-snippet {
background: #1a1a1a; color: #e8eaf0; padding: 16px 18px; border-radius: 10px;
overflow: auto; font-family: "SF Mono", "Menlo", "Monaco", "Courier New", monospace;
font-size: 13px; line-height: 1.55; margin: 14px 0 0; position: relative;
white-space: pre;
}
.docs-snippet .copy-btn {
position: absolute; top: 10px; right: 10px;
background: rgba(255,255,255,0.1); color: #e8eaf0; border: 1px solid rgba(255,255,255,0.15);
padding: 4px 10px; border-radius: 6px; font-size: 11px; cursor: pointer;
font-family: -apple-system, BlinkMacSystemFont, sans-serif; text-transform: uppercase; letter-spacing: 0.08em;
}
.docs-snippet .copy-btn:hover { background: rgba(255,255,255,0.18); }
.docs-footer { font-size: 12px; color: var(--ink-soft); margin-top: 14px; }
</style>
</head>
<body>
<header>
<div class="brand">
<img src="https://cdn-avatars.huggingface.co/v1/production/uploads/680a76de1b8d08640089fab0/r60-JyW_pv4llgjEyDYj9.png" alt="" />
<div>
<h1>Thinking Machines</h1>
<div class="sub">Inkling</div>
</div>
</div>
<button class="playground-link secondary" id="openDocs" type="button">
How to use →
</button>
</header>
<main>
<div id="messages">
<div class="empty">
<div class="empty-hero">
<img class="hero-logo" src="https://cdn-avatars.huggingface.co/v1/production/uploads/680a76de1b8d08640089fab0/r60-JyW_pv4llgjEyDYj9.png" alt="" />
<h2 class="hero-title">Inkling</h2>
<p class="hero-sub">Your AI assistant from Thinking Machines.</p>
<a class="playground-link primary" href="https://tinker.thinkingmachines.ai/playground" target="_blank" rel="noopener noreferrer">
Try the Playground →<span class="hero-tag">Faster experience</span>
</a>
</div>
</div>
</div>
</main>
<div class="composer">
<div class="attachments" id="attachments" hidden></div>
<div class="composer-inner">
<label class="icon-btn" title="Attach image">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12.5l-8.5 8.5a5.5 5.5 0 0 1-7.8-7.8l9.4-9.4a3.7 3.7 0 0 1 5.2 5.2L9.4 18.9a1.85 1.85 0 0 1-2.6-2.6L15 8.2" />
</svg>
<input type="file" id="imgFile" accept="image/*" multiple />
</label>
<textarea id="input" rows="1" placeholder="Message Inkling…"></textarea>
<button id="send" title="Send"></button>
</div>
</div>
<div class="docs-overlay" id="docsOverlay">
<div class="docs-modal" id="docsModal">
<div class="docs-header">
<h3>How to use Inkling via Inference Providers</h3>
<button class="docs-close" id="closeDocs" type="button" aria-label="Close">×</button>
</div>
<div class="docs-body">
<p>Reach the model through Hugging Face's OpenAI-compatible router. Set <code>HF_TOKEN</code> in your environment and use the <code>thinkingmachines/Inkling:together</code> model id.</p>
<pre class="docs-snippet" id="docsCode"><button class="copy-btn" id="copySnippet" type="button">Copy</button>import os
from openai import OpenAI
client = OpenAI(
base_url="https://router.huggingface.co/v1",
api_key=os.environ["HF_TOKEN"],
)
stream = client.chat.completions.create(
model="thinkingmachines/Inkling:together",
messages=[
{
"role": "user",
"content": [
{
"type": "text",
"text": "Describe this image in one sentence."
},
{
"type": "image_url",
"image_url": {
"url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg"
}
}
]
}
],
stream=True,
)
for chunk in stream:
print(chunk.choices[0].delta.content, end="")</pre>
<p class="docs-footer">The <code>:auto</code> suffix routes to your preferred provider in settings; you can also use <code>:cheapest</code> or <code>:fastest</code>.</p>
</div>
</div>
</div>
<script>
"use strict";
/* Wire protocol: Gradio v6 /gradio_api/call/v2 — join-first, then SSE.
Images uploaded as files go through POST /upload (FastAPI) which returns
a public /uploads/<id> URL the router can fetch. */
const $ = (id) => document.getElementById(id);
const messagesEl = $("messages");
const inputEl = $("input");
const sendBtn = $("send");
const fileInput = $("imgFile");
const attachmentsEl = $("attachments");
const pendingFiles = []; // {file, url, objectUrl} — staged before send
const history = []; // {role, content} sent so far
let liveStream = null;
function escape(s) {
return s.replace(/[&<>"']/g, (c) =>
({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
}
function renderMarkdown(src) {
let out = escape(src);
out = out.replace(/```([\s\S]*?)```/g, (_, c) =>
`<pre style="background:#0e0f15;padding:10px;border-radius:8px;overflow:auto;border:1px solid var(--border);color:#e8eaf0;"><code>${c.replace(/^\n/, "")}</code></pre>`);
out = out.replace(/`([^`]+)`/g, (_, c) =>
`<code style="background:#0e0f15;padding:1px 5px;border-radius:5px;color:#e8eaf0;">${c}</code>`);
out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
out = out.replace(/(^|[^*])\*([^*]+)\*/g, "$1<em>$2</em>");
out = out.replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g,
'<a href="$2" target="_blank" rel="noopener" style="color:var(--accent);">$1</a>');
out = out.replace(/\n/g, "<br>");
return out;
}
function clearEmpty() {
const e = messagesEl.querySelector(".empty");
if (e) e.remove();
}
function addMessage(role, content = "") {
clearEmpty();
const wrap = document.createElement("div");
wrap.className = `msg ${role}`;
const who = document.createElement("div");
who.className = "who";
who.textContent = role === "user" ? "You" : "Inkling";
const bubble = document.createElement("div");
bubble.className = "bubble";
bubble.innerHTML = content;
wrap.append(who, bubble);
messagesEl.appendChild(wrap);
scrollToEnd();
return bubble;
}
function scrollToEnd() {
messagesEl.parentElement.scrollTop = messagesEl.parentElement.scrollHeight;
}
function setBusy(busy) {
sendBtn.disabled = busy;
inputEl.disabled = busy;
}
/* Attachment chips */
function renderAttachments() {
attachmentsEl.innerHTML = "";
if (!pendingFiles.length) { attachmentsEl.hidden = true; return; }
attachmentsEl.hidden = false;
pendingFiles.forEach((pf, i) => {
const chip = document.createElement("div");
chip.className = "attachment";
chip.innerHTML = `
<img src="${pf.objectUrl}" alt="" />
<span>${escape(pf.file.name)}</span>
<span class="remove" data-i="${i}">×</span>`;
attachmentsEl.appendChild(chip);
});
}
function addFiles(fileList) {
for (const file of fileList) {
if (!file.type.startsWith("image/")) continue;
pendingFiles.push({ file, url: null, objectUrl: URL.createObjectURL(file) });
}
renderAttachments();
}
function removeAttachment(i) {
const pf = pendingFiles[i];
if (pf) URL.revokeObjectURL(pf.objectUrl);
pendingFiles.splice(i, 1);
renderAttachments();
}
fileInput.addEventListener("change", () => {
if (fileInput.files && fileInput.files.length) {
addFiles(fileInput.files);
fileInput.value = "";
}
});
attachmentsEl.addEventListener("click", (e) => {
const t = e.target.closest(".remove");
if (t) removeAttachment(Number(t.dataset.i));
});
/* Backend: upload files → public URL, then send chat. */
async function uploadOne(pf) {
const fd = new FormData();
fd.append("image", pf.file);
const res = await fetch("/upload", { method: "POST", body: fd });
if (!res.ok) throw new Error(`Upload failed: HTTP ${res.status}`);
const { url } = await res.json();
pf.url = url;
}
async function jpost(path, body) {
const res = await fetch(path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`HTTP ${res.status} ${path}`);
return res.json();
}
async function streamChat(payload) {
const join = await jpost("/gradio_api/call/v2/chat", payload);
const eventId = join.event_id;
if (!eventId) throw new Error("No event_id from server.");
const es = new EventSource(`/gradio_api/call/v2/chat/${encodeURIComponent(eventId)}`);
liveStream = es;
return new Promise((resolve, reject) => {
let latest = "", settled = false;
const finish = (fn) => {
if (settled) return;
settled = true;
setTimeout(() => { try { es.close(); } catch (_) {} }, 0);
fn();
};
es.addEventListener("generating", (ev) => {
try {
const arr = JSON.parse(ev.data);
if (Array.isArray(arr) && typeof arr[0] === "string") {
latest = arr[0]; onDelta(latest);
}
} catch (_) {}
});
es.addEventListener("complete", (ev) => {
try {
const arr = JSON.parse(ev.data);
if (Array.isArray(arr) && typeof arr[0] === "string") {
latest = arr[0]; onDelta(latest);
}
} catch (_) {}
finish(() => resolve(latest));
});
es.addEventListener("error", (ev) => {
let msg = "Stream error.";
if (ev.data) { try { msg = JSON.parse(ev.data) ?? msg; } catch (_) {} }
finish(() => reject(new Error(typeof msg === "string" ? msg : "Stream error.")));
});
es.onerror = () => {
if (settled) return;
finish(() => reject(new Error(latest ? null : "Connection lost before any output arrived.")));
};
});
}
async function send() {
const text = inputEl.value.trim();
if (!text && pendingFiles.length === 0) return;
const files = pendingFiles.slice();
pendingFiles.length = 0;
renderAttachments();
inputEl.value = "";
inputEl.style.height = "auto";
const bubble = addMessage("assistant", '<span class="cursor"></span>');
setBusy(true);
let assistantText = "";
function onDelta(t) {
assistantText = t;
bubble.innerHTML = renderMarkdown(t) + '<span class="cursor"></span>';
scrollToEnd();
}
try {
// Upload each file to /upload → public URL the router can fetch.
for (const pf of files) await uploadOne(pf);
// Build user message content (text + vision parts).
const parts = [];
if (text) parts.push({ type: "text", text });
for (const pf of files) {
const fullUrl = new URL(pf.url, window.location.origin).toString();
parts.push({ type: "image_url", image_url: { url: fullUrl } });
}
const userContent = parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts;
// Render user message: text + thumbnails (if any).
let displayHtml = "";
if (text) displayHtml += escape(text);
for (const pf of files) displayHtml += `<img src="${pf.objectUrl}" style="max-width:120px;max-height:120px;border-radius:8px;display:block;margin-top:${text ? "8px" : "0"};" alt="" />`;
addMessage("user", displayHtml);
history.push({ role: "user", content: userContent });
const payload = {
messages: history.slice(),
temperature: 0.7,
};
assistantText = await streamChat(payload);
if (!assistantText) {
assistantText = "(No content)";
bubble.textContent = assistantText;
} else if (assistantText.startsWith("⚠️")) {
bubble.classList.add("error");
} else {
history.push({ role: "assistant", content: assistantText });
}
bubble.innerHTML = renderMarkdown(assistantText);
} catch (err) {
bubble.classList.add("error");
bubble.textContent = `⚠️ ${err?.message ?? err}`;
} finally {
liveStream = null;
setBusy(false);
inputEl.focus();
}
}
sendBtn.addEventListener("click", send);
inputEl.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); }
});
inputEl.addEventListener("input", () => {
inputEl.style.height = "auto";
inputEl.style.height = Math.min(inputEl.scrollHeight, 200) + "px";
});
inputEl.focus();
/* Docs modal */
const docsOverlay = $("docsOverlay");
const docsModal = $("docsModal");
$("openDocs").addEventListener("click", () => docsOverlay.classList.add("open"));
$("closeDocs").addEventListener("click", () => docsOverlay.classList.remove("open"));
docsOverlay.addEventListener("click", (e) => {
if (e.target === docsOverlay) docsOverlay.classList.remove("open");
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && docsOverlay.classList.contains("open")) {
docsOverlay.classList.remove("open");
}
});
$("copySnippet").addEventListener("click", () => {
const code = $("docsCode").innerText.replace(/^Copy$/, "").trim();
navigator.clipboard.writeText(code).then(() => {
const btn = $("copySnippet");
const orig = btn.textContent;
btn.textContent = "Copied";
setTimeout(() => (btn.textContent = orig), 1200);
}).catch(() => {});
});
</script>
</body>
</html>