391 lines
9.7 KiB
Python
391 lines
9.7 KiB
Python
import os
|
|
import configparser
|
|
import requests
|
|
from flask import Flask, render_template_string, jsonify
|
|
|
|
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")
|
|
PORT = int(os.environ.get("PORT", "80"))
|
|
|
|
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" />
|
|
<title>Programming Dad Jokes</title>
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 24px;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
/* Vibrant animated gradient with bright red, yellow, and blue */
|
|
background: linear-gradient(-45deg, #ff0844 0%, #ff4b2b 20%, #ffdd00 45%, #00d2ff 75%, #0052d4 100%);
|
|
background-size: 400% 400%;
|
|
animation: gradientShift 14s ease infinite;
|
|
position: relative;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
/* Ambient glowing gradient orbs for depth */
|
|
body::before,
|
|
body::after {
|
|
content: "";
|
|
position: fixed;
|
|
border-radius: 50%;
|
|
filter: blur(80px);
|
|
z-index: 0;
|
|
pointer-events: none;
|
|
opacity: 0.65;
|
|
animation: floatOrbs 10s ease-in-out infinite alternate;
|
|
}
|
|
|
|
body::before {
|
|
width: 380px;
|
|
height: 380px;
|
|
background: radial-gradient(circle, #ff0055 0%, #ffaa00 100%);
|
|
top: -80px;
|
|
left: -80px;
|
|
}
|
|
|
|
body::after {
|
|
width: 420px;
|
|
height: 420px;
|
|
background: radial-gradient(circle, #00d2ff 0%, #0044ff 100%);
|
|
bottom: -100px;
|
|
right: -100px;
|
|
animation-delay: -5s;
|
|
}
|
|
|
|
@keyframes gradientShift {
|
|
0% { background-position: 0% 50%; }
|
|
50% { background-position: 100% 50%; }
|
|
100% { background-position: 0% 50%; }
|
|
}
|
|
|
|
@keyframes floatOrbs {
|
|
0% { transform: translate(0, 0) scale(1); }
|
|
100% { transform: translate(40px, 40px) scale(1.1); }
|
|
}
|
|
|
|
.container {
|
|
position: relative;
|
|
z-index: 1;
|
|
width: 100%;
|
|
max-width: 680px;
|
|
}
|
|
|
|
.card {
|
|
background: rgba(255, 255, 255, 0.92);
|
|
backdrop-filter: blur(20px);
|
|
-webkit-backdrop-filter: blur(20px);
|
|
border-radius: 28px;
|
|
border: 2px solid rgba(255, 255, 255, 0.85);
|
|
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.22),
|
|
0 0 35px rgba(255, 255, 255, 0.35);
|
|
padding: 44px 38px;
|
|
text-align: center;
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 6px 16px;
|
|
border-radius: 999px;
|
|
font-size: 0.82rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: #1e293b;
|
|
background: linear-gradient(90deg, rgba(255, 75, 43, 0.15), rgba(255, 221, 0, 0.25), rgba(0, 210, 255, 0.2));
|
|
border: 1px solid rgba(255, 75, 43, 0.3);
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.badge-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: #10b981;
|
|
box-shadow: 0 0 8px #10b981;
|
|
}
|
|
|
|
.badge-divider {
|
|
opacity: 0.4;
|
|
margin: 0 2px;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 2.2rem;
|
|
font-weight: 800;
|
|
color: #0f172a;
|
|
letter-spacing: -0.02em;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.subtitle {
|
|
font-size: 1rem;
|
|
color: #64748b;
|
|
margin-bottom: 28px;
|
|
}
|
|
|
|
.joke-box {
|
|
background: #f8fafc;
|
|
border-radius: 18px;
|
|
padding: 26px 24px;
|
|
margin-bottom: 30px;
|
|
position: relative;
|
|
border: 1px solid #e2e8f0;
|
|
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.02);
|
|
text-align: left;
|
|
}
|
|
|
|
.joke-box::before {
|
|
content: '"';
|
|
position: absolute;
|
|
top: -12px;
|
|
left: 18px;
|
|
font-size: 3.5rem;
|
|
line-height: 1;
|
|
font-family: Georgia, serif;
|
|
color: #ff0844;
|
|
opacity: 0.3;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.joke-text {
|
|
font-size: 1.25rem;
|
|
line-height: 1.7;
|
|
color: #1e293b;
|
|
font-weight: 500;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
min-height: 80px;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 14px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.btn {
|
|
appearance: none;
|
|
border: none;
|
|
outline: none;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
padding: 14px 28px;
|
|
border-radius: 14px;
|
|
color: #ffffff;
|
|
background: linear-gradient(135deg, #ff0844 0%, #ff8c00 40%, #0099ff 100%);
|
|
background-size: 200% 200%;
|
|
box-shadow: 0 10px 25px rgba(255, 8, 68, 0.35);
|
|
transition: all 0.25s ease;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.btn:hover:not(:disabled) {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 14px 30px rgba(0, 153, 255, 0.45);
|
|
background-position: 100% 100%;
|
|
}
|
|
|
|
.btn:active:not(:disabled) {
|
|
transform: translateY(1px);
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.65;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.spinner {
|
|
display: none;
|
|
width: 18px;
|
|
height: 18px;
|
|
border: 3px solid rgba(255, 255, 255, 0.35);
|
|
border-top-color: #ffffff;
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.meta {
|
|
font-size: 0.82rem;
|
|
color: #94a3b8;
|
|
display: flex;
|
|
justify-content: center;
|
|
flex-wrap: wrap;
|
|
gap: 16px;
|
|
border-top: 1px solid #f1f5f9;
|
|
padding-top: 18px;
|
|
}
|
|
|
|
.meta span {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.meta code {
|
|
background: #f1f5f9;
|
|
padding: 2px 6px;
|
|
border-radius: 6px;
|
|
font-size: 0.78rem;
|
|
color: #475569;
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.card {
|
|
padding: 30px 20px;
|
|
}
|
|
h1 {
|
|
font-size: 1.75rem;
|
|
}
|
|
.joke-text {
|
|
font-size: 1.1rem;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<main class="card">
|
|
<div class="badge">
|
|
<span class="badge-dot"></span>
|
|
<span>Ollama • Gemma 4</span>
|
|
<span class="badge-divider">•</span>
|
|
<span>v{{ version }}</span>
|
|
</div>
|
|
<h1>Programming Dad Joke</h1>
|
|
<p class="subtitle">Freshly compiled humor from our AI model</p>
|
|
|
|
<div class="joke-box">
|
|
<div id="joke" class="joke-text">{{ joke }}</div>
|
|
</div>
|
|
|
|
<div class="actions">
|
|
<button id="refreshBtn" class="btn" onclick="fetchNewJoke()">
|
|
<span class="spinner" id="spinner"></span>
|
|
<span id="btnText">Tell Me Another Joke! 🚀</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="meta">
|
|
<span>Host: <code>{{ host }}</code></span>
|
|
<span>Model: <code>{{ model }}</code></span>
|
|
<span>Prompt: <code>"{{ prompt }}"</code></span>
|
|
<span>App Version: <code>v{{ version }}</code></span>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
async function fetchNewJoke() {
|
|
const btn = document.getElementById("refreshBtn");
|
|
const btnText = document.getElementById("btnText");
|
|
const spinner = document.getElementById("spinner");
|
|
const jokeEl = document.getElementById("joke");
|
|
|
|
btn.disabled = true;
|
|
spinner.style.display = "inline-block";
|
|
btnText.textContent = "Compiling humor...";
|
|
|
|
try {
|
|
const response = await fetch("/api/joke");
|
|
if (!response.ok) {
|
|
throw new Error("HTTP error " + response.status);
|
|
}
|
|
const data = await response.json();
|
|
jokeEl.textContent = data.joke || "No joke returned.";
|
|
} catch (err) {
|
|
console.error("Failed to load joke:", err);
|
|
jokeEl.textContent = "Oops! Could not fetch a new joke right now. Please try again.";
|
|
} finally {
|
|
btn.disabled = false;
|
|
spinner.style.display = "none";
|
|
btnText.textContent = "Tell Me Another Joke! 🚀";
|
|
}
|
|
}
|
|
</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 = request_joke()
|
|
version = get_app_version()
|
|
return render_template_string(
|
|
HTML_TEMPLATE,
|
|
joke=joke,
|
|
host=OLLAMA_HOST,
|
|
model=OLLAMA_MODEL,
|
|
prompt=PROMPT,
|
|
version=version
|
|
)
|
|
|
|
@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)
|