iacconfDemo2026/app.py

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("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)