divination-ai / app.py
irkan's picture
Deploy divination-ai from 85c2748
2d1b361 verified
Raw
History Blame Contribute Delete
21 kB
"""
Divination AI โ€” Modernized Gradio 6.x UI
Cleaner, more immersive mystical oracle experience with better typography and layout.
"""
from __future__ import annotations
import gradio as gr
import model_runtime
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# THEME
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
THEME = gr.themes.Base(
font=[gr.themes.GoogleFont("Cormorant Garamond"), "serif"],
font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"],
).set(
body_background_fill="#0a0812",
block_background_fill="rgba(10,8,18,0.6)",
button_primary_background_fill="#d4a444",
button_primary_text_color="#0a0812",
input_background_fill="rgba(26,15,46,0.6)",
input_border_color="rgba(212,164,68,0.3)",
)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# CSS
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
CSS = """
/* โ•โ•โ• GLOBAL RESET & FULL-SCREEN LAYOUT โ•โ•โ• */
@import url('https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@400;700&900&display=swap');
html, body, gradio-app {
height: 100%;
margin: 0;
overflow: hidden;
}
.gradio-container {
max-width: 100% !important;
padding: 0 !important;
height: 100%;
overflow-y: auto;
}
/* Remove Gradio dark mode artifacts */
.dark { filter: none !important; }
/* โ•โ•โ• MYSTICAL BACKGROUND โ•โ•โ• */
body {
background: radial-gradient(ellipse at center top, #1a0f2e 0%, #0a0812 60%, #000000 100%);
background-attachment: fixed;
position: relative;
overflow-x: hidden;
}
/* Animated stars layer */
#stars-layer {
position: fixed;
inset: 0;
background-image:
radial-gradient(2px 2px at 20% 30%, rgba(255,255,255,0.8), transparent),
radial-gradient(2px 2px at 80% 20%, rgba(255,255,255,0.6), transparent),
radial-gradient(1px 1px at 50% 50%, rgba(255,255,255,0.5), transparent),
radial-gradient(1px 1px at 30% 70%, rgba(255,255,255,0.7), transparent),
radial-gradient(2px 2px at 70% 80%, rgba(255,255,255,0.6), transparent);
background-size: 200% 200%, 200% 200%, 200% 200%, 200% 200%, 200% 200%;
background-position: 0% 0%, 100% 100%, 50% 50%, 80% 20%, 20% 80%;
animation: twinkle 8s ease-in-out infinite;
pointer-events: none;
z-index: 0;
}
@keyframes twinkle {
0%, 100% { opacity: 0.4; }
50% { opacity: 0.8; }
}
/* โ•โ•โ• TYPOGRAPHY โ•โ•โ• */
.oracle-heading {
font-family: 'Cinzel Decorative', serif;
font-weight: 900;
font-size: 3.5rem;
background: linear-gradient(135deg, #d4a444 0%, #e8b956 50%, #d4a444 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-align: center;
letter-spacing: 0.1em;
text-shadow: 0 0 40px rgba(212,164,68,0.5);
margin: 2rem 0 1rem 0;
line-height: 1.2;
}
.oracle-subtitle {
font-size: 1.4rem;
color: rgba(232,226,244,0.9);
text-align: center;
font-style: italic;
line-height: 1.7;
margin-bottom: 3rem;
}
/* โ•โ•โ• LAYOUT CONTAINERS โ•โ•โ• */
#welcome-col, #chat-col, #results-col {
padding: 40px 20px;
min-height: 600px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
z-index: 1;
}
#welcome-col {
max-width: 700px;
margin: 0 auto;
}
#chat-col {
max-width: 800px;
margin: 0 auto;
}
#results-col {
max-width: 1200px;
margin: 0 auto;
}
/* โ•โ•โ• ORACLE BUBBLE โ•โ•โ• */
.oracle-scroll {
background: rgba(10,8,18,0.85);
border: 2px solid rgba(212,164,68,0.4);
border-radius: 20px;
padding: 40px 50px;
margin: 0 0 2rem 0;
backdrop-filter: blur(16px);
box-shadow: 0 8px 40px rgba(0,0,0,0.6),
0 0 60px rgba(123,77,191,0.3),
inset 0 1px 0 rgba(255,255,255,0.1);
animation: oracleGlow 4s ease-in-out infinite;
}
@keyframes oracleGlow {
0%, 100% {
box-shadow: 0 8px 40px rgba(0,0,0,0.6),
0 0 60px rgba(123,77,191,0.3),
inset 0 1px 0 rgba(255,255,255,0.1);
}
50% {
box-shadow: 0 8px 40px rgba(0,0,0,0.6),
0 0 80px rgba(212,164,68,0.4),
inset 0 1px 0 rgba(255,255,255,0.1);
}
}
#oracle-bubble {
font-size: 1.5rem;
color: #e8e2f4;
line-height: 1.9;
min-height: 60px;
}
/* โ•โ•โ• INPUT FIELD โ•โ•โ• */
.input-parchment {
width: 100%;
max-width: 700px;
margin: 0 auto;
}
#user-input textarea {
background: rgba(26,15,46,0.7) !important;
border: 2px solid rgba(212,164,68,0.3) !important;
border-radius: 12px !important;
color: #e8e2f4 !important;
font-size: 1.3rem !important;
padding: 20px 24px !important;
font-family: 'Cormorant Garamond', serif !important;
transition: all 0.3s ease !important;
}
#user-input textarea:focus {
border-color: #d4a444 !important;
box-shadow: 0 0 30px rgba(212,164,68,0.4) !important;
background: rgba(26,15,46,0.9) !important;
}
#user-input textarea::placeholder {
color: rgba(232,226,244,0.4) !important;
font-style: italic !important;
}
/* โ•โ•โ• MYSTICAL BUTTONS โ•โ•โ• */
.mystical-btn button {
background: linear-gradient(135deg, #d4a444 0%, #b8832d 100%) !important;
color: #0a0812 !important;
border: none !important;
border-radius: 12px !important;
padding: 18px 50px !important;
font-size: 1.2rem !important;
font-weight: 700 !important;
font-family: 'Cinzel Decorative', serif !important;
letter-spacing: 0.08em !important;
text-transform: uppercase !important;
box-shadow: 0 6px 25px rgba(212,164,68,0.4) !important;
transition: all 0.3s ease !important;
cursor: pointer !important;
}
.mystical-btn button:hover {
transform: translateY(-3px) !important;
box-shadow: 0 10px 35px rgba(212,164,68,0.6) !important;
background: linear-gradient(135deg, #e8b956 0%, #d4a444 100%) !important;
}
.mystical-btn button:active {
transform: translateY(-1px) !important;
}
/* โ•โ•โ• SCENARIO CARDS โ•โ•โ• */
.scenarios-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 24px;
margin: 3rem 0;
width: 100%;
}
.scenario-card {
background: rgba(10,8,18,0.9);
border: 2px solid rgba(212,164,68,0.3);
border-radius: 16px;
padding: 35px 30px;
backdrop-filter: blur(12px);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
position: relative;
overflow: hidden;
}
.scenario-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(90deg, transparent, currentColor, transparent);
opacity: 0;
transition: opacity 0.4s ease;
}
.scenario-card:hover::before {
opacity: 1;
}
.scenario-card:hover {
transform: translateY(-8px);
border-color: #d4a444;
box-shadow: 0 12px 40px rgba(212,164,68,0.3);
}
.scenario-card.momentum {
border-left: 5px solid #d4a444;
color: #d4a444;
}
.scenario-card.pivot {
border-left: 5px solid #7b4dbf;
color: #7b4dbf;
}
.scenario-card.leap {
border-left: 5px solid #4db8a8;
color: #4db8a8;
}
.scenario-card h3 {
font-family: 'Cinzel Decorative', serif;
font-size: 1.9rem;
color: #d4a444;
margin-bottom: 0.7rem;
font-weight: 700;
}
.archetype {
font-size: 1rem;
color: currentColor;
text-transform: uppercase;
letter-spacing: 0.15em;
margin-bottom: 1.2rem;
opacity: 0.9;
}
.narrative {
font-size: 1.15rem;
color: #e8e2f4;
line-height: 1.7;
margin-bottom: 1.5rem;
}
.meta {
font-size: 1rem;
color: rgba(232,226,244,0.7);
line-height: 1.6;
}
.meta strong {
color: rgba(212,164,68,0.9);
}
/* โ•โ•โ• REFLECTION TEXT โ•โ•โ• */
#reflection-text {
font-size: 1.5rem;
color: #e8e2f4;
line-height: 1.9;
text-align: center;
font-style: italic;
margin: 3rem 0;
padding: 40px;
background: rgba(10,8,18,0.85);
border: 2px solid rgba(212,164,68,0.3);
border-radius: 20px;
backdrop-filter: blur(12px);
}
#motivation-text {
font-size: 1.4rem;
color: rgba(232,226,244,0.9);
line-height: 1.8;
text-align: center;
margin: 3rem 0;
font-weight: 500;
}
/* โ•โ•โ• DISCLAIMER โ•โ•โ• */
#disclaimer {
font-size: 0.9rem;
color: rgba(232,226,244,0.5);
text-align: center;
line-height: 1.6;
margin-top: 3rem;
}
/* โ•โ•โ• MUTE BUTTON โ•โ•โ• */
#mute-toggle {
position: fixed;
top: 24px;
right: 24px;
z-index: 1000;
background: rgba(10,8,18,0.8) !important;
border: 2px solid rgba(212,164,68,0.3) !important;
border-radius: 50% !important;
width: 56px !important;
height: 56px !important;
font-size: 1.6rem !important;
cursor: pointer !important;
transition: all 0.3s ease !important;
backdrop-filter: blur(10px) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
#mute-toggle:hover {
background: rgba(10,8,18,0.95) !important;
border-color: #d4a444 !important;
transform: scale(1.1) !important;
}
/* โ•โ•โ• LOADING STATE โ•โ•โ• */
#loading-screen {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 50vh;
}
#loading-text {
font-family: 'Cinzel Decorative', serif;
font-size: 1.8rem;
color: #d4a444;
text-shadow: 0 0 30px rgba(212,164,68,0.6);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
/* โ•โ•โ• RESPONSIVE โ•โ•โ• */
@media (max-width: 768px) {
.oracle-heading {
font-size: 2.5rem;
}
.oracle-subtitle {
font-size: 1.1rem;
}
#oracle-bubble {
font-size: 1.2rem;
}
.oracle-scroll {
padding: 30px 25px;
}
.scenarios-container {
grid-template-columns: 1fr;
}
}
"""
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# HEAD โ€” Audio + JS
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
HEAD = """
<script>
// โ”€โ”€โ”€ AUDIO ENGINE โ”€โ”€โ”€
window.audioCtx = null;
window.masterGain = null;
window.audioMuted = false;
window.startAmbient = function() {
if (window.audioCtx) return;
const ctx = new (window.AudioContext || window.webkitAudioContext)();
window.audioCtx = ctx;
const master = ctx.createGain();
master.gain.value = 0.3;
master.connect(ctx.destination);
window.masterGain = master;
// Ambient drone (A minor pentatonic mood)
const osc1 = ctx.createOscillator();
const osc2 = ctx.createOscillator();
const osc3 = ctx.createOscillator();
osc1.type = 'sine';
osc2.type = 'triangle';
osc3.type = 'sine';
osc1.frequency.value = 110; // A2
osc2.frequency.value = 164.81; // E3
osc3.frequency.value = 220; // A3
const gain1 = ctx.createGain();
const gain2 = ctx.createGain();
const gain3 = ctx.createGain();
gain1.gain.value = 0.06;
gain2.gain.value = 0.04;
gain3.gain.value = 0.03;
const filter = ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 800;
osc1.connect(gain1);
osc2.connect(gain2);
osc3.connect(gain3);
gain1.connect(filter);
gain2.connect(filter);
gain3.connect(filter);
filter.connect(master);
osc1.start();
osc2.start();
osc3.start();
};
window.toggleMute = function() {
if (!window.masterGain) return;
window.audioMuted = !window.audioMuted;
window.masterGain.gain.linearRampToValueAtTime(
window.audioMuted ? 0 : 0.3,
window.audioCtx.currentTime + 0.3
);
const btn = document.getElementById('mute-toggle');
if (btn) btn.textContent = window.audioMuted ? '๐Ÿ”‡' : '๐Ÿ”Š';
};
// Remove dark mode class
const observer = new MutationObserver(() => {
document.querySelectorAll('.dark').forEach(el => el.classList.remove('dark'));
});
observer.observe(document.documentElement, { attributes: true, subtree: true, attributeFilter: ['class'] });
</script>
"""
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# HELPERS
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def render_scenario_cards(reading: dict) -> str:
scenarios = reading.get("scenarios", [])
html = '<div class="scenarios-container">'
for s in scenarios:
archetype = s.get("archetype", "").lower()
html += f"""
<div class="scenario-card {archetype}">
<h3>{s.get('title', '')}</h3>
<div class="archetype">The Path of {s.get('archetype', '')}</div>
<div class="narrative">{s.get('narrative', '').replace(chr(10), '<br>')}</div>
<div class="meta">
<strong>First step:</strong> {s.get('first_step', '')}<br>
<strong>Watch for:</strong> {s.get('watch_for', '')}<br>
<strong>Tradeoff:</strong> {s.get('tradeoff', '')}
</div>
</div>
"""
html += '</div>'
return html
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# APP
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
with gr.Blocks(title="Divination AI") as demo:
state = gr.State(value={"messages": [], "intake_complete": False, "reading": None})
gr.HTML('<div id="stars-layer"></div>')
gr.HTML('<button id="mute-toggle" onclick="window.toggleMute()">๐Ÿ”Š</button>')
# โ”€โ”€ Welcome Screen โ”€โ”€
with gr.Column(visible=True, elem_id="welcome-col") as welcome_col:
gr.HTML('<h1 class="oracle-heading">The Oracle of Paths</h1>')
gr.HTML('<p class="oracle-subtitle">A glimpse into your possible futures,<br>grounded in the world as it is today.</p>')
begin_btn = gr.Button("Begin Your Reading", elem_classes="mystical-btn")
gr.HTML('<div id="disclaimer">This is an AI-powered reflection tool for entertainment and self-exploration.<br>Not a substitute for professional advice. Your future is shaped by your choices.</div>')
# โ”€โ”€ Conversation Screen โ”€โ”€
with gr.Column(visible=False, elem_id="chat-col") as chat_col:
oracle_bubble = gr.HTML('<div class="oracle-scroll"><div id="oracle-bubble">...</div></div>')
with gr.Group(elem_classes="input-parchment"):
user_input = gr.Textbox(
placeholder="Speak your truth...",
lines=3,
show_label=False,
elem_id="user-input",
)
submit_btn = gr.Button("Send", elem_classes="mystical-btn")
# โ”€โ”€ Results Screen โ”€โ”€
with gr.Column(visible=False, elem_id="results-col") as results_col:
results_html = gr.HTML("")
new_reading_btn = gr.Button("Begin New Reading", elem_classes="mystical-btn")
# โ”€โ”€ Event Handlers โ”€โ”€
def do_begin(session):
"""Start first oracle question."""
try:
result = model_runtime.intake(messages=[])
except model_runtime.BackendError as e:
return (
session,
f'<div class="oracle-scroll"><div id="oracle-bubble">The Oracle stirs but cannot yet speak... {e}</div></div>',
gr.update(visible=True),
gr.update(visible=False),
)
response = result["response"]
session["messages"] = [{"role": "assistant", "content": response}]
return (
session,
f'<div class="oracle-scroll"><div id="oracle-bubble">{response}</div></div>',
gr.update(visible=True),
gr.update(visible=False),
)
def on_send(session, user_text):
"""Handle user message."""
if not user_text or not user_text.strip():
return (session, gr.update(), gr.update(), gr.update(), gr.update(), gr.update())
session["messages"].append({"role": "user", "content": user_text.strip()})
try:
result = model_runtime.intake(session["messages"])
except model_runtime.BackendError as e:
session["messages"].pop()
return (
session,
f'<div class="oracle-scroll"><div id="oracle-bubble">The connection wavers... {e}</div></div>',
"",
gr.update(visible=True),
gr.update(visible=False),
gr.update(),
)
response = result["response"]
session["messages"].append({"role": "assistant", "content": response})
intake_complete = result.get("intake_complete", False)
if not intake_complete:
return (
session,
f'<div class="oracle-scroll"><div id="oracle-bubble">{response}</div></div>',
"",
gr.update(visible=True),
gr.update(visible=False),
gr.update(),
)
# Intake complete โ€” generate reading
try:
gen_result = model_runtime.generate(session["messages"])
except model_runtime.BackendError as e:
return (
session,
gr.update(),
"",
gr.update(visible=False),
gr.update(visible=True),
f'<div id="reflection-text">The Oracle could not complete your reading: {e}</div>',
)
reading = gen_result.get("reading", {})
session["reading"] = reading
reflection = reading.get("reflection", "")
cards_html = render_scenario_cards(reading)
motivation = reading.get("motivation", "")
full_results = (
f'<div id="reflection-text">{reflection}</div>'
+ cards_html
+ f'<div id="motivation-text">{motivation}</div>'
)
return (
session,
gr.update(),
"",
gr.update(visible=False),
gr.update(visible=True),
full_results,
)
def on_new_reading(session):
session = {"messages": [], "intake_complete": False, "reading": None}
return (
session,
gr.update(visible=True),
gr.update(visible=False),
gr.update(visible=False),
)
# โ”€โ”€ Event wiring โ”€โ”€
begin_btn.click(
fn=do_begin,
inputs=[state],
outputs=[state, oracle_bubble, chat_col, welcome_col],
js="() => { window.startAmbient(); }",
)
submit_btn.click(
fn=on_send,
inputs=[state, user_input],
outputs=[state, oracle_bubble, user_input, chat_col, results_col, results_html],
)
user_input.submit(
fn=on_send,
inputs=[state, user_input],
outputs=[state, oracle_bubble, user_input, chat_col, results_col, results_html],
)
new_reading_btn.click(
fn=on_new_reading,
inputs=[state],
outputs=[state, welcome_col, chat_col, results_col],
)
if __name__ == "__main__":
demo.queue(default_concurrency_limit=5).launch(
css=CSS,
head=HEAD,
theme=THEME,
ssr_mode=False,
)