You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

185 lines
7.7 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Diagram → TikZ</title>
<meta property="og:title" content="Diagram → TikZ" />
<meta
property="og:description"
content="Upload an image of your diagram. The server sends it to an LLM and returns standalone LaTeX/TikZ code, then compiles it to PDF and renders a PNG preview."
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="{{ base_path }}/" />
<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=Space+Grotesk:wght@400;700;800&family=JetBrains+Mono:wght@400;600&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="{{ base_path }}/public/styles.css" />
</head>
<body class="page page-index">
<div class="wrap">
<h1>Handmade Diagram → Standalone TikZ</h1>
<p>
Upload an image of your diagram. The server sends it to an LLM and returns standalone LaTeX/TikZ code,
then compiles it to PDF and renders a PNG preview.
</p>
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
<form class="card" action="{{ base_path }}/convert" method="post" enctype="multipart/form-data">
<div class="field">
<label for="image">Diagram image</label>
<input id="image" class="sr-only" name="image" type="file" accept="image/*" required />
<label for="image" id="dropzone" class="dropzone"
>Click here to upload an image (or drag & drop / paste)</label
>
<div class="hint">Accepted: any format your browser can upload (PNG/JPG recommended).</div>
<div class="hint">Daily convert quota: {{ convert_remaining }}/{{ convert_limit }} left today.</div>
<div id="preview" class="preview" aria-live="polite">
<img id="preview-img" alt="Selected upload preview" />
<div id="preview-meta" class="meta"></div>
</div>
</div>
<div class="field">
<label for="notes">Notes for the model (optional)</label>
<textarea
id="notes"
name="notes"
rows="3"
placeholder="Constraints, naming, style, etc."
></textarea>
<div class="hint">These notes are sent along with the image to guide the conversion.</div>
</div>
<div id="submit-status" class="hint" aria-live="polite" style="display: none"></div>
<button id="convert-btn" class="btn" type="submit">Convert</button>
</form>
</div>
<script>
;(function () {
const form = document.querySelector("form")
const submitStatus = document.getElementById("submit-status")
const convertBtn = document.getElementById("convert-btn")
const input = document.getElementById("image")
const dropzone = document.getElementById("dropzone")
const preview = document.getElementById("preview")
const img = document.getElementById("preview-img")
const meta = document.getElementById("preview-meta")
let lastUrl = null
function clearPreview() {
if (lastUrl) URL.revokeObjectURL(lastUrl)
lastUrl = null
img.removeAttribute("src")
meta.textContent = ""
preview.style.display = "none"
}
function showFile(file) {
if (!file || !file.type || !file.type.startsWith("image/")) {
clearPreview()
return
}
if (lastUrl) URL.revokeObjectURL(lastUrl)
lastUrl = URL.createObjectURL(file)
img.src = lastUrl
meta.textContent = `${file.name || "pasted-image"}${(file.size / 1024).toFixed(1)} KB`
preview.style.display = "block"
}
function setInputFile(file) {
try {
const dt = new DataTransfer()
dt.items.add(file)
input.files = dt.files
input.dispatchEvent(new Event("change", { bubbles: true }))
} catch {
// If the browser blocks programmatic assignment to input.files,
// still show preview so the user can manually re-select if needed.
showFile(file)
}
}
input.addEventListener("change", () => {
const file = input.files && input.files[0]
if (!file) {
clearPreview()
return
}
showFile(file)
})
// Drag and drop support on label dropzone
dropzone.addEventListener("dragenter", e => {
e.preventDefault()
e.stopPropagation()
dropzone.classList.add("drag-over")
})
dropzone.addEventListener("dragleave", e => {
e.preventDefault()
e.stopPropagation()
dropzone.classList.remove("drag-over")
})
dropzone.addEventListener("dragover", e => {
e.preventDefault()
e.stopPropagation()
})
dropzone.addEventListener("drop", e => {
e.preventDefault()
e.stopPropagation()
dropzone.classList.remove("drag-over")
const files = e.dataTransfer.files
if (!files || files.length === 0) return
for (const file of files) {
if (file.type && file.type.startsWith("image/")) {
setInputFile(file)
return
}
}
})
window.addEventListener("paste", e => {
const cd = e.clipboardData
if (!cd || !cd.items) return
for (const item of cd.items) {
if (item.kind === "file" && item.type && item.type.startsWith("image/")) {
const file = item.getAsFile()
if (!file) continue
e.preventDefault()
setInputFile(file)
return
}
}
})
if (form && convertBtn && submitStatus) {
form.addEventListener("submit", () => {
convertBtn.disabled = true
convertBtn.textContent = "Converting…"
submitStatus.textContent = "Be patient — this may take up to a minute."
submitStatus.style.display = "block"
})
}
})()
</script>
</body>
</html>