iacconfDemo2026/app.py

742 lines
23 KiB
Python
Raw Normal View History

2026-09-19 20:49:53 +00:00
import os
import random
2026-09-19 20:49:53 +00:00
import configparser
import requests
2026-10-30 02:56:22 +00:00
from flask import Flask, render_template_string, jsonify, send_from_directory
2026-09-19 20:49:53 +00:00
app = Flask(__name__)
OLLAMA_HOST = os.environ.get("OLLAMA_HOST", "http://harbor.freshbrewed.science:11444")
OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "gemma4:e4b")
PROMPT = os.environ.get("JOKE_PROMPT", "tell me a dad joke about programming")
2026-09-19 20:49:53 +00:00
PORT = int(os.environ.get("PORT", "80"))
PRESET_JOKES = [
"What do you call parenting with a software engineer? Pair programming (or O-pair programming).",
"My kids were rolling on the floor and screaming. My wife asked what was happening, and I told her it was a version conflict.",
"When I wrote this code, only God and I knew how it worked. Now, only God knows.",
"A programmer is sent to the store by his wife: “Get a gallon of milk, and if they have eggs, get a dozen.” The programmer returns home with 13 gallons of milk and says, “They had eggs”.",
"Where do coder dads store all their projects? In a dad-abase.",
"Why don't programmers like going outside into nature? Too many bugs.",
"Why did the programmer die taking a shower? The shampoo bottle instructions said: Lather. Rinse. Repeat."
]
2026-09-19 20:49:53 +00:00
def get_app_version():
config = configparser.ConfigParser()
version_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "version.ini")
if os.path.exists(version_file):
try:
config.read(version_file)
return config.get("metadata", "version", fallback="unknown")
except Exception:
return "unknown"
return "unknown"
HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
2026-10-30 02:56:22 +00:00
<meta name="description" content="Dry programming dad jokes set on a typographic studio proof sheet, pulled live from your local Ollama model." />
<title>Programming Dad Joke</title>
<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=Hedvig+Letters+Serif:opsz@12..24&family=Geist:wght@400;500&display=swap" rel="stylesheet" />
2026-09-19 20:49:53 +00:00
<style>
2026-10-30 02:56:22 +00:00
/* Hallmark · shape: Workbench (draw 9cbbc9) · genre: editorial · theme: Studio (deal 3/3, seed programming-dad-joke-generator-254f·3aca28a0·2026-10-30·r0) · redesign: full
* hero: Stage · nav: edge-aligned · footer: letter close · imagery: the product · signature: crop-marked proof sheet with orange metric construction guides
* axes: light/roman-serif/warm · posture: restrained · contrast: pass · fold: pass · v2.0.0 */
/* Hallmark · pre-emit critique: P5 H5 E5 S4 R5 V5 · weakest: S, generic joke card could fit any trivia app; framed the joke as a crop-marked Studio proof sheet with live typographic metric guides */
/* Hallmark · reworded "Tell Me Another Joke! 🚀" to "Pull another joke" · 30a bans emoji in controls */
/* Hallmark · reworded "Freshly compiled humor from our AI model" to "Dry humor for engineers after hours, pulled live from your local model." · 19 names generic AI filler */
/* Hallmark · dropped "Ollama • Gemma 4" · 54 and 63 ban middle-dot eyebrow badges */
@import url("./tokens.css");
@import url("./base.css");
.skip {
position: absolute;
left: var(--space-md);
top: calc(-1 * var(--space-3xl));
z-index: var(--z-toast);
padding: var(--space-xs) var(--space-md);
background: var(--color-ink);
color: var(--color-paper);
text-decoration: none;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
.skip:focus-visible {
top: var(--space-md);
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
.proof {
background-image: radial-gradient(circle at 1px 1px, var(--color-dot) 1px, transparent 1.4px);
background-size: 24px 24px;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
.crop {
position: relative;
--l: 8px;
--o: 12px;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
.crop::before {
content: "";
position: absolute;
inset: calc(-1 * var(--o));
pointer-events: none;
background:
linear-gradient(var(--color-mark), var(--color-mark)) 0 var(--o) / var(--l) 1px no-repeat,
linear-gradient(var(--color-mark), var(--color-mark)) var(--o) 0 / 1px var(--l) no-repeat,
linear-gradient(var(--color-mark), var(--color-mark)) 100% var(--o) / var(--l) 1px no-repeat,
linear-gradient(var(--color-mark), var(--color-mark)) calc(100% - var(--o)) 0 / 1px var(--l) no-repeat,
linear-gradient(var(--color-mark), var(--color-mark)) 0 calc(100% - var(--o)) / var(--l) 1px no-repeat,
linear-gradient(var(--color-mark), var(--color-mark)) var(--o) 100% / 1px var(--l) no-repeat,
linear-gradient(var(--color-mark), var(--color-mark)) 100% calc(100% - var(--o)) / var(--l) 1px no-repeat,
linear-gradient(var(--color-mark), var(--color-mark)) calc(100% - var(--o)) 100% / 1px var(--l) no-repeat;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
.site-header {
border-bottom: var(--rule-hair) solid var(--color-rule);
padding-block: var(--space-md);
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
.site-header__inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-lg);
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
.wordmark {
font-family: var(--font-display);
font-size: var(--text-lg);
font-weight: var(--display-weight);
letter-spacing: var(--tracking-display);
color: var(--color-ink);
text-decoration: none;
white-space: nowrap;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
.wordmark:hover {
text-decoration: underline;
text-decoration-color: var(--color-accent);
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
.site-nav {
display: flex;
2026-09-19 20:49:53 +00:00
align-items: center;
2026-10-30 02:56:22 +00:00
gap: var(--space-lg);
font-size: var(--text-sm);
color: var(--color-ink-2);
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
.site-nav a {
text-decoration: none;
white-space: nowrap;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
.site-nav a:hover {
text-decoration: underline;
text-decoration-color: var(--color-accent);
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
.hero {
padding-block: var(--space-3xl);
border-bottom: var(--rule-hair) solid var(--color-rule);
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
.hero__grid {
display: grid;
grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
gap: var(--gap-columns);
align-items: center;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
.hero__lead {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--space-lg);
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
.hero__lead .lead {
color: var(--color-ink-2);
2026-09-19 20:49:53 +00:00
}
.actions {
display: flex;
2026-10-30 02:56:22 +00:00
align-items: center;
gap: var(--space-md);
margin-top: var(--space-xs);
2026-09-19 20:49:53 +00:00
}
.btn {
2026-10-30 02:56:22 +00:00
position: relative;
2026-09-19 20:49:53 +00:00
appearance: none;
display: inline-flex;
align-items: center;
2026-10-30 02:56:22 +00:00
justify-content: center;
gap: var(--space-xs);
min-height: 48px;
padding: 0 var(--space-lg);
border: var(--rule-hair) solid var(--color-ink);
border-radius: var(--radius-card);
background: var(--color-paper-hi);
color: var(--color-ink);
font-family: var(--font-body);
font-size: var(--text-md);
font-weight: 500;
line-height: 1;
white-space: nowrap;
text-decoration: none;
cursor: pointer;
transition: background-color var(--dur-short) var(--ease-out), transform var(--dur-micro) var(--ease-out);
}
.btn::after {
content: "";
position: absolute;
inset: var(--space-3xs);
border: var(--rule-hair) solid var(--color-mark);
pointer-events: none;
}
.btn--primary {
background: var(--color-accent);
color: var(--color-accent-ink);
2026-09-19 20:49:53 +00:00
}
.btn:hover:not(:disabled) {
2026-10-30 02:56:22 +00:00
transform: translateY(-1px);
2026-09-19 20:49:53 +00:00
}
.btn:active:not(:disabled) {
transform: translateY(1px);
}
2026-10-30 02:56:22 +00:00
.btn:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
.btn:disabled,
.btn[aria-disabled="true"] {
opacity: 0.55;
2026-09-19 20:49:53 +00:00
cursor: not-allowed;
2026-10-30 02:56:22 +00:00
transform: none;
}
.btn[aria-busy="true"] {
cursor: progress;
2026-09-19 20:49:53 +00:00
}
.spinner {
display: none;
2026-10-30 02:56:22 +00:00
width: 16px;
height: 16px;
border: 2px solid var(--color-mark);
border-top-color: var(--color-ink);
2026-09-19 20:49:53 +00:00
border-radius: 50%;
2026-10-30 02:56:22 +00:00
animation: spin var(--dur-long) linear infinite;
flex-shrink: 0;
2026-09-19 20:49:53 +00:00
}
@keyframes spin {
to { transform: rotate(360deg); }
}
2026-10-30 02:56:22 +00:00
.sheet {
background: var(--color-paper-hi);
border: var(--rule-hair) solid var(--color-rule);
border-radius: var(--radius-card);
box-shadow: var(--shadow-sheet);
padding: var(--space-xl);
2026-09-19 20:49:53 +00:00
display: flex;
2026-10-30 02:56:22 +00:00
flex-direction: column;
gap: var(--space-lg);
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
.guide-bar {
display: flex;
2026-09-19 20:49:53 +00:00
align-items: center;
2026-10-30 02:56:22 +00:00
gap: var(--space-sm);
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
.guide-line {
flex: 1;
height: var(--rule-hair);
background: var(--color-accent);
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
.guide-readout {
font-size: var(--text-xs);
font-weight: 500;
color: var(--color-muted);
white-space: nowrap;
}
.joke-specimen {
margin: 0;
padding-block: var(--space-md);
border-top: var(--rule-hair) solid var(--color-accent);
border-bottom: var(--rule-hair) solid var(--color-accent);
}
.joke-text {
font-family: var(--font-display);
font-size: var(--text-xl);
font-weight: var(--display-weight);
line-height: 1.4;
letter-spacing: var(--tracking-display);
color: var(--color-ink);
white-space: pre-wrap;
overflow-wrap: break-word;
min-height: var(--space-3xl);
display: flex;
align-items: center;
transition: opacity var(--dur-short) var(--ease-out);
}
.sheet[data-loading="true"] .joke-text {
opacity: 0.45;
}
.proof-progress {
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.proof-progress__meta {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-sm);
font-size: var(--text-xs);
font-weight: 500;
color: var(--color-muted);
}
.proof-progress__track {
position: relative;
height: var(--space-2xs);
background: var(--color-paper-3);
overflow: hidden;
}
.proof-progress__fill {
width: 100%;
height: 100%;
background: var(--color-accent);
transform-origin: left center;
transform: scaleX(var(--progress, 0));
transition: transform var(--dur-short) var(--ease-out);
}
.section--notes {
padding-block: var(--section-gap-s);
}
.section__head {
display: grid;
grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
gap: var(--gap-columns);
align-items: end;
margin-bottom: var(--space-2xl);
}
.section__head p {
color: var(--color-ink-2);
}
.set {
list-style: none;
margin: 0;
padding: 0;
border-top: var(--rule-hair) solid var(--color-ink);
counter-reset: rule-counter;
}
.set__row {
counter-increment: rule-counter;
display: grid;
grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
gap: var(--gap-columns);
align-items: baseline;
padding-block: var(--space-lg);
border-bottom: var(--rule-hair) solid var(--color-rule);
}
.set__row h3::before {
content: "0" counter(rule-counter) " ";
font-family: var(--font-body);
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-muted);
margin-right: var(--space-sm);
font-variant-numeric: tabular-nums;
}
.set__row p {
color: var(--color-ink-2);
}
.site-footer {
background: var(--color-paper-2);
color: var(--color-ink);
border-top: var(--rule-hair) solid var(--color-ink);
padding-block: var(--space-2xl);
}
.footer__head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-lg);
margin-bottom: var(--space-xl);
flex-wrap: wrap;
}
.footer__head p {
color: var(--color-ink-2);
font-size: var(--text-sm);
}
.colophon-grid {
margin: 0;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: var(--gap-columns);
border-top: var(--rule-hair) solid var(--color-rule);
padding-top: var(--space-lg);
}
.colophon-item {
display: flex;
flex-direction: column;
gap: var(--space-2xs);
min-width: 0;
}
.colophon-item dt {
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-ink-2);
}
.colophon-item dd {
margin: 0;
font-size: var(--text-md);
color: var(--color-ink);
overflow-wrap: break-word;
}
@media (max-width: 60rem) {
.hero__grid,
.section__head,
.set__row {
grid-template-columns: minmax(0, 1fr);
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
.colophon-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
}
@media (max-width: 40rem) {
.colophon-grid {
grid-template-columns: minmax(0, 1fr);
}
.sheet {
padding: var(--space-lg);
}
}
@media (prefers-reduced-motion: reduce) {
.btn {
transition: background-color var(--dur-short) linear;
}
.joke-text,
.proof-progress__fill {
transition: none;
2026-09-19 20:49:53 +00:00
}
}
</style>
</head>
<body>
2026-10-30 02:56:22 +00:00
<a class="skip" href="#main">Skip to content</a>
<header class="site-header proof">
<div class="container site-header__inner">
<a href="#main" class="wordmark" aria-label="Programming Dad Joke home">Proof Sheet</a>
<nav class="site-nav" aria-label="Page sections">
<a href="#specimen">Specimen</a>
<a href="#notes">Bench notes</a>
<a href="#colophon">Colophon</a>
<span class="num">v{{ version }}</span>
</nav>
</div>
</header>
<main id="main">
<section class="hero proof">
<div class="container hero__grid">
<div class="hero__lead">
<h1>Programming Dad Joke</h1>
<p class="lead">Dry humor for engineers after hours, pulled live from your local model.</p>
<div class="actions">
<button id="refreshBtn" class="btn btn--primary crop" type="button" onclick="fetchNewJoke()">
<span class="spinner keep-motion" id="spinner" aria-hidden="true"></span>
<span id="btnText">Pull another joke</span>
</button>
</div>
</div>
<div id="specimen" class="hero__stage">
<article id="jokeSheet" class="sheet crop" aria-live="polite" aria-atomic="true">
<div class="guide-bar" aria-hidden="true">
<span class="guide-line"></span>
<span class="guide-readout num">Hedvig Letters Serif, 25pt / 1.4</span>
</div>
<blockquote class="joke-specimen">
<p id="joke" class="joke-text">{{ joke }}</p>
</blockquote>
<div id="proofProgress" class="proof-progress" hidden>
<div class="proof-progress__meta">
<span id="progressStatus">Setting type on the press (30 to 50s typical)…</span>
<span id="progressReadout" class="num">0% (0s)</span>
</div>
<div
id="progressBar"
class="proof-progress__track"
role="progressbar"
aria-label="Joke generation progress"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="0"
>
<div id="progressFill" class="proof-progress__fill"></div>
</div>
</div>
<div class="guide-bar" aria-hidden="true">
<span class="guide-readout num">Proofed on {{ model }}</span>
<span class="guide-line"></span>
<span class="guide-readout num">Release v{{ version }}</span>
</div>
</article>
</div>
2026-09-19 20:49:53 +00:00
</div>
2026-10-30 02:56:22 +00:00
</section>
<section class="section--notes" id="notes">
<div class="container">
<div class="section__head">
<h2>Three rules on the press</h2>
<p class="lead">Every groaner on the sheet clears three checks before it leaves the studio.</p>
</div>
<ol class="set">
<li class="set__row">
<h3>Pun on the literal</h3>
<p>Take a noun every developer says ten times before lunch, such as cache, branch, or deadlock, and read it as plain kitchen English.</p>
</li>
<li class="set__row">
<h3>Keep the setup short</h3>
<p>One question across the counter. If the premise needs a whiteboard diagram, the punchline already missed its train.</p>
</li>
<li class="set__row">
<h3>Aim for the eye roll</h3>
<p>A proper dad joke never chases an applause break. It settles for a quiet sigh from across the table.</p>
</li>
</ol>
2026-09-19 20:49:53 +00:00
</div>
2026-10-30 02:56:22 +00:00
</section>
</main>
<footer class="site-footer" id="colophon">
<div class="container">
<div class="footer__head">
<h2>Press colophon</h2>
<p>Compiled on demand from your local inference runtime.</p>
2026-09-19 20:49:53 +00:00
</div>
2026-10-30 02:56:22 +00:00
<dl class="colophon-grid">
<div class="colophon-item">
<dt>Host</dt>
<dd class="num">{{ host }}</dd>
</div>
<div class="colophon-item">
<dt>Model</dt>
<dd class="num">{{ model }}</dd>
</div>
<div class="colophon-item">
<dt>Prompt</dt>
<dd>“{{ prompt }}”</dd>
</div>
<div class="colophon-item">
<dt>App Version</dt>
<dd class="num">v{{ version }}</dd>
</div>
</dl>
</div>
</footer>
2026-09-19 20:49:53 +00:00
<script>
2026-10-30 02:56:22 +00:00
let progressTimer = null;
function computeExpectedProgress(elapsedSeconds) {
if (elapsedSeconds <= 0) return 0.02;
if (elapsedSeconds <= 30) {
return 0.02 + (elapsedSeconds / 30) * 0.66;
}
if (elapsedSeconds <= 50) {
return 0.68 + ((elapsedSeconds - 30) / 20) * 0.24;
}
return 0.92 + 0.06 * (1 - Math.exp(-(elapsedSeconds - 50) / 20));
}
function setProgressState(ratio, elapsedSeconds, statusText, isComplete) {
const progressBar = document.getElementById("progressBar");
const progressFill = document.getElementById("progressFill");
const progressReadout = document.getElementById("progressReadout");
const progressStatus = document.getElementById("progressStatus");
const clamped = Math.max(0, Math.min(1, ratio));
const pct = Math.round(clamped * 100);
const secs = isComplete
? Math.max(0.1, elapsedSeconds).toFixed(1)
: String(Math.max(0, Math.floor(elapsedSeconds)));
progressFill.style.setProperty("--progress", clamped.toFixed(4));
progressBar.setAttribute("aria-valuenow", String(pct));
progressReadout.textContent = pct + "% (" + secs + "s)";
if (statusText) {
progressStatus.textContent = statusText;
}
}
2026-09-19 20:49:53 +00:00
async function fetchNewJoke() {
const btn = document.getElementById("refreshBtn");
const btnText = document.getElementById("btnText");
const spinner = document.getElementById("spinner");
const jokeEl = document.getElementById("joke");
2026-10-30 02:56:22 +00:00
const sheet = document.getElementById("jokeSheet");
const progressWrap = document.getElementById("proofProgress");
const progressFill = document.getElementById("progressFill");
if (progressTimer) {
clearInterval(progressTimer);
progressTimer = null;
}
2026-09-19 20:49:53 +00:00
btn.disabled = true;
2026-10-30 02:56:22 +00:00
btn.setAttribute("aria-busy", "true");
2026-09-19 20:49:53 +00:00
spinner.style.display = "inline-block";
2026-10-30 02:56:22 +00:00
btnText.textContent = "Pulling fresh proof\u2026";
sheet.setAttribute("data-loading", "true");
progressWrap.hidden = false;
progressFill.style.transition = "none";
setProgressState(0, 0, "Setting type on the press (30 to 50s typical)\u2026", false);
void progressFill.offsetWidth;
progressFill.style.transition = "";
const startTime = performance.now();
setProgressState(0.02, 0, "Setting type on the press (30 to 50s typical)\u2026", false);
progressTimer = setInterval(() => {
const elapsed = (performance.now() - startTime) / 1000;
const ratio = computeExpectedProgress(elapsed);
const status =
elapsed < 30
? "Setting type on the press (30 to 50s typical)\u2026"
: elapsed <= 50
? "Inking the proof sheet (30 to 50s window)\u2026"
: "Finishing slow impression\u2026";
setProgressState(ratio, elapsed, status, false);
}, 100);
let nextJoke = "";
2026-09-19 20:49:53 +00:00
try {
const response = await fetch("/api/joke");
if (!response.ok) {
throw new Error("HTTP error " + response.status);
}
const data = await response.json();
2026-10-30 02:56:22 +00:00
nextJoke = data.joke || "No joke returned.";
2026-09-19 20:49:53 +00:00
} catch (err) {
console.error("Failed to load joke:", err);
2026-10-30 02:56:22 +00:00
nextJoke = "Could not reach the joke service right now. Check the Ollama host and try again.";
2026-09-19 20:49:53 +00:00
} finally {
2026-10-30 02:56:22 +00:00
if (progressTimer) {
clearInterval(progressTimer);
progressTimer = null;
}
const totalElapsed = (performance.now() - startTime) / 1000;
setProgressState(1, totalElapsed, "Proof complete (30 to 50s typical)", true);
jokeEl.textContent = nextJoke;
sheet.removeAttribute("data-loading");
2026-09-19 20:49:53 +00:00
btn.disabled = false;
2026-10-30 02:56:22 +00:00
btn.removeAttribute("aria-busy");
2026-09-19 20:49:53 +00:00
spinner.style.display = "none";
2026-10-30 02:56:22 +00:00
btnText.textContent = "Pull another joke";
2026-09-19 20:49:53 +00:00
}
}
</script>
</body>
</html>
"""
def request_joke():
url = f"{OLLAMA_HOST.rstrip('/')}/api/generate"
payload = {
"model": OLLAMA_MODEL,
"prompt": PROMPT,
"stream": False
}
try:
resp = requests.post(url, json=payload, timeout=60)
resp.raise_for_status()
data = resp.json()
return data.get("response", "No response received from Ollama.")
except Exception as e:
return f"Error contacting Ollama ({OLLAMA_HOST}): {e}"
@app.route("/")
def index():
joke = random.choice(PRESET_JOKES)
2026-09-19 20:49:53 +00:00
version = get_app_version()
return render_template_string(
HTML_TEMPLATE,
joke=joke,
host=OLLAMA_HOST,
model=OLLAMA_MODEL,
prompt=PROMPT,
version=version
)
2026-10-30 02:56:22 +00:00
@app.route("/tokens.css")
def tokens_css():
return send_from_directory(os.path.dirname(os.path.abspath(__file__)), "tokens.css", mimetype="text/css")
@app.route("/base.css")
def base_css():
return send_from_directory(os.path.dirname(os.path.abspath(__file__)), "base.css", mimetype="text/css")
2026-09-19 20:49:53 +00:00
@app.route("/api/joke")
def api_joke():
joke = request_joke()
version = get_app_version()
return jsonify({
"joke": joke,
"version": version
})
if __name__ == "__main__":
version = get_app_version()
print(f"Starting server v{version} on port {PORT} with model '{OLLAMA_MODEL}' at {OLLAMA_HOST}...")
app.run(host="0.0.0.0", port=PORT)