feat: 3D hexagonal figure with roll-in-place animation and decisive Jev oracle prompting

This commit is contained in:
katR 2026-09-19 20:40:54 +05:30
parent f7f14e37e9
commit fc464cb68b
6 changed files with 336 additions and 146 deletions

View file

@ -55,36 +55,42 @@ export async function onRequestPost(context) {
try { try {
const payload = { const payload = {
model: MODEL, model: MODEL,
state: { question: cleanQuestion }, state: {
question: cleanQuestion,
directive: "You are the definitive Magic 8-Ball oracle. Every query demands a bold, prophetic, decisive verdict (affirmative or negative). You must actively choose a clear side. Do NOT default to hazy, ambiguous, or neutral answers unless the user's question is completely incomprehensible gibberish or an impossible paradox."
},
questions: { questions: {
favorable: { favorable: {
type: "noul", type: "noul",
instructions: "Is the underlying outcome, recommendation, or premise of the user's question affirmative or likely to happen?", instructions: "Determine whether the fortune leans affirmative/favorable (1.0) or negative/adverse (0.0). Make a firm choice and avoid sitting in the ambiguous 0.45 - 0.55 zone.",
criteria: { criteria: {
true: "The outcome leans affirmative, positive, or advisable.", true: "The fortune favors yes, affirmative, or an auspicious outcome.",
false: "The outcome leans doubtful, negative, or unadvisable." false: "The fortune favors no, negative, or an inauspicious outcome."
} }
}, },
sentiment: { sentiment: {
type: "choice", type: "choice",
instructions: "Classify the overall orientation of the oracle's verdict.", instructions: "Declare the oracle's verdict as either affirmative or negative. Reserve neutral strictly for non-questions or unparseable gibberish.",
criteria: { criteria: {
affirmative: "A clearly favorable, encouraging, or positive answer.", affirmative: "Auspicious, affirmative, or confirming prophecy.",
neutral: "Uncertain, ambiguous, hazy, or premature to determine.", negative: "Inauspicious, negative, or adverse prophecy.",
negative: "An unfavorable, contrary, or discouraging answer." neutral: "Strictly unparseable gibberish or logical paradox."
} }
}, },
aphorism: { aphorism: {
type: "choice", type: "choice",
instructions: "Select the most accurate Magic 8-Ball statement to deliver.", instructions: "Select the authentic Magic 8-Ball prophecy that best fits the question.",
criteria: { criteria: {
certain: "It is certain or without a doubt.", certain: "It is certain.",
yes_definitely: "Yes definitely or decidedly so.", yes_definitely: "Yes definitely.",
signs_yes: "Signs point to yes or most likely.", signs_yes: "Signs point to yes.",
reply_hazy: "Reply hazy, try again.", outlook_good: "Outlook good.",
cannot_predict: "Cannot predict now or ask again later.", most_likely: "Most likely.",
my_reply_no: "My reply is no or outlook not so good.", my_reply_no: "My reply is no.",
very_doubtful: "Very doubtful or don't count on it." very_doubtful: "Very doubtful.",
outlook_bad: "Outlook not so good.",
dont_count: "Don't count on it.",
reply_hazy: "Reply hazy, try again (strictly reserved for total gibberish)."
} }
} }
} }
@ -119,17 +125,29 @@ export async function onRequestPost(context) {
const answers = data.answers || {}; const answers = data.answers || {};
const favorableNoul = answers.favorable?.noul ?? 0.5; const favorableNoul = answers.favorable?.noul ?? 0.5;
const sentimentChoice = answers.sentiment?.choice || (favorableNoul > 0.6 ? "affirmative" : favorableNoul < 0.4 ? "negative" : "neutral"); let sentimentChoice = answers.sentiment?.choice;
const aphorismKey = answers.aphorism?.choice; if (!sentimentChoice || sentimentChoice === "neutral") {
sentimentChoice = favorableNoul >= 0.5 ? "affirmative" : "negative";
}
let aphorismKey = answers.aphorism?.choice;
// Prevent unwanted hazy response if noul shows conviction
if (aphorismKey === "reply_hazy" || aphorismKey === "cannot_predict") {
if (favorableNoul >= 0.5) {
aphorismKey = favorableNoul > 0.75 ? "certain" : "signs_yes";
} else {
aphorismKey = favorableNoul < 0.3 ? "my_reply_no" : "outlook_bad";
}
}
let answerText = APHORISMS[aphorismKey]?.text; let answerText = APHORISMS[aphorismKey]?.text;
if (!answerText) { if (!answerText) {
if (sentimentChoice === "affirmative") { if (sentimentChoice === "affirmative") {
answerText = favorableNoul > 0.8 ? "It is certain." : "Signs point to yes."; answerText = favorableNoul > 0.75 ? "It is certain." : "Signs point to yes.";
} else if (sentimentChoice === "negative") { } else if (sentimentChoice === "negative") {
answerText = favorableNoul < 0.2 ? "My reply is no." : "Outlook not so good."; answerText = favorableNoul < 0.25 ? "My reply is no." : "Outlook not so good.";
} else { } else {
answerText = "Reply hazy, try again."; answerText = favorableNoul >= 0.5 ? "Signs point to yes." : "Outlook not so good.";
} }
} }

View file

@ -147,13 +147,13 @@ h1 {
color: var(--text-muted); color: var(--text-muted);
} }
/* ─── 8-BALL STAGE & VESSEL ─── */ /* ─── HEXAGONAL 3D FIGURE STAGE & VESSEL ─── */
.ball-stage { .ball-stage {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin: 10px 0 30px; margin: 14px 0 32px;
perspective: 900px; perspective: 1000px;
} }
.ball-wrapper { .ball-wrapper {
@ -162,30 +162,78 @@ h1 {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
perspective: 1000px;
} }
.ball-sphere { /* Hexagonal 3D Prism Figure */
width: 230px; .hex-figure-3d {
height: 230px; width: 210px;
border-radius: 50% !important; /* Keep ball spherical */ height: 210px;
background: radial-gradient(circle at 35% 30%, #2e2e2e 0%, #171717 40%, #090909 75%, #000000 100%); position: relative;
box-shadow: 0 20px 44px rgba(0, 0, 0, 0.85), inset 0 2px 4px rgba(255, 255, 255, 0.12), inset 0 -6px 14px rgba(0, 0, 0, 0.9); transform-style: preserve-3d;
transition: transform 0.2s cubic-bezier(0.2, 0, 0, 1);
margin-bottom: 6px;
}
/* Hex Prism Cap - Front / Back */
.hex-cap {
position: absolute;
inset: 0;
clip-path: polygon(50% 0%, 93.3% 25%, 93.3% 75%, 50% 100%, 6.7% 75%, 6.7% 25%);
}
.hex-cap-top {
transform: translateZ(-28px);
background: radial-gradient(circle at 50% 50%, #171717 0%, #0c0c0c 60%, #000 100%);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.9);
}
.hex-cap-front {
transform: translateZ(28px);
background: radial-gradient(circle at 35% 30%, #2a2a2a 0%, #151515 45%, #0a0a0a 75%, #020202 100%);
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow:
0 15px 35px rgba(0, 0, 0, 0.9),
inset 0 2px 4px rgba(255, 255, 255, 0.2),
inset 0 -6px 14px rgba(0, 0, 0, 0.9);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
position: relative;
transition: transform 0.15s ease-out;
transform-style: preserve-3d;
} }
/* Fluid Viewport Window */ /* 6 Hexagonal Prism Facet Edges around the sides */
.hex-facet {
position: absolute;
top: 50%;
left: 50%;
width: 105px;
height: 56px;
margin-left: -52.5px;
margin-top: -28px;
background: linear-gradient(to bottom, #222, #0d0d0d 60%, #050505);
border-top: 1px solid rgba(255, 255, 255, 0.16);
border-bottom: 1px solid rgba(0, 0, 0, 0.8);
box-sizing: border-box;
opacity: 0.92;
pointer-events: none;
}
.hex-facet-0 { transform: rotateZ(0deg) rotateX(90deg) translateZ(90.9px); }
.hex-facet-1 { transform: rotateZ(60deg) rotateX(90deg) translateZ(90.9px); filter: brightness(0.85); }
.hex-facet-2 { transform: rotateZ(120deg) rotateX(90deg) translateZ(90.9px); filter: brightness(0.65); }
.hex-facet-3 { transform: rotateZ(180deg) rotateX(90deg) translateZ(90.9px); filter: brightness(0.5); }
.hex-facet-4 { transform: rotateZ(240deg) rotateX(90deg) translateZ(90.9px); filter: brightness(0.7); }
.hex-facet-5 { transform: rotateZ(300deg) rotateX(90deg) translateZ(90.9px); filter: brightness(0.9); }
/* Hex Fluid Viewport Window */
.fluid-window { .fluid-window {
width: 140px; width: 142px;
height: 140px; height: 142px;
border-radius: 50% !important; /* Inner circular liquid reservoir */ clip-path: polygon(50% 0%, 93.3% 25%, 93.3% 75%, 50% 100%, 6.7% 75%, 6.7% 25%);
background: radial-gradient(circle, #081226 0%, #040916 65%, #010308 100%); background: radial-gradient(circle, #08142a 0%, #040916 65%, #010308 100%);
border: 2px solid rgba(255, 255, 255, 0.08); border: 1px solid rgba(56, 189, 248, 0.15);
box-shadow: inset 0 4px 18px rgba(0, 0, 0, 0.95), 0 0 10px rgba(0, 0, 0, 0.5); box-shadow: inset 0 4px 22px rgba(0, 0, 0, 0.95), 0 0 14px rgba(0, 0, 0, 0.8);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
@ -196,11 +244,11 @@ h1 {
.fluid-ambient { .fluid-ambient {
position: absolute; position: absolute;
inset: 0; inset: 0;
background: radial-gradient(circle at 50% 30%, rgba(56, 189, 248, 0.08) 0%, transparent 70%); background: radial-gradient(circle at 50% 30%, rgba(56, 189, 248, 0.12) 0%, transparent 70%);
pointer-events: none; pointer-events: none;
} }
/* Floating Triangle Die */ /* Floating Hexagonal Die Plate */
.floating-die { .floating-die {
position: relative; position: relative;
width: 120px; width: 120px;
@ -231,14 +279,21 @@ h1 {
} }
.die-inner { .die-inner {
fill: #08142a; fill: #08142a;
stroke: rgba(56, 189, 248, 0.2);
stroke-width: 1;
}
.die-accent-line {
stroke: rgba(56, 189, 248, 0.3);
stroke-width: 1;
stroke-dasharray: 2 2;
} }
.die-text-wrap { .die-text-wrap {
position: relative; position: relative;
z-index: 2; z-index: 2;
width: 78px; width: 82px;
text-align: center; text-align: center;
margin-top: 14px; margin-top: 0;
} }
.die-text { .die-text {
@ -247,17 +302,17 @@ h1 {
letter-spacing: 0.04em; letter-spacing: 0.04em;
color: #bae6fd; color: #bae6fd;
text-transform: uppercase; text-transform: uppercase;
line-height: 1.25; line-height: 1.28;
display: inline-block; display: inline-block;
text-shadow: 0 0 6px rgba(56, 189, 248, 0.4); text-shadow: 0 0 8px rgba(56, 189, 248, 0.45);
transition: opacity 0.4s ease, transform 0.4s ease; transition: opacity 0.4s ease, transform 0.4s ease;
} }
/* Submerged state */ /* Submerged state */
.floating-die.submerged { .floating-die.submerged {
transform: scale(0.35) translateY(18px) rotate(15deg) !important; transform: scale(0.28) translateY(24px) rotate(35deg) !important;
opacity: 0; opacity: 0;
filter: blur(4px); filter: blur(5px);
animation: none; animation: none;
} }
@ -266,30 +321,71 @@ h1 {
animation: dieSurface 1.1s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; animation: dieSurface 1.1s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
} }
@keyframes dieSurface { @keyframes dieSurface {
0% { transform: scale(0.4) translateY(20px) rotate(-20deg); opacity: 0; filter: blur(4px); } 0% { transform: scale(0.35) translateY(24px) rotate(-30deg); opacity: 0; filter: blur(5px); }
60% { transform: scale(1.06) translateY(-2px) rotate(2deg); opacity: 0.9; filter: blur(0); } 60% { transform: scale(1.08) translateY(-2px) rotate(2deg); opacity: 0.95; filter: blur(0); }
100% { transform: scale(1) translateY(0) rotate(0deg); opacity: 1; filter: blur(0); } 100% { transform: scale(1) translateY(0) rotate(0deg); opacity: 1; filter: blur(0); }
} }
/* Shake Animation on Ball */ /* ─── 3D ROLL-IN-PLACE ANIMATION ─── */
.ball-sphere.shaking { .hex-figure-3d.rolling {
animation: eightBallShake 0.75s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; animation: hexRollInPlace 1.25s cubic-bezier(0.45, 0.05, 0.25, 1) forwards;
} }
@keyframes eightBallShake { @keyframes hexRollInPlace {
10%, 90% { transform: translate3d(-3px, 0, 0) rotate(-2deg); } 0% {
20%, 80% { transform: translate3d(4px, 0, 0) rotate(2deg); } transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg) scale(1);
30%, 50%, 70% { transform: translate3d(-6px, 0, 0) rotate(-3deg); } }
40%, 60% { transform: translate3d(6px, 0, 0) rotate(3deg); } 15% {
transform: rotateX(70deg) rotateY(-80deg) rotateZ(-45deg) translateY(-8px) scale(1.04);
}
35% {
transform: rotateX(190deg) rotateY(160deg) rotateZ(120deg) translateY(-14px) scale(1.08);
}
55% {
transform: rotateX(310deg) rotateY(380deg) rotateZ(260deg) translateY(-8px) scale(1.05);
}
75% {
transform: rotateX(375deg) rotateY(560deg) rotateZ(350deg) translateY(-2px) scale(1.01);
}
90% {
transform: rotateX(358deg) rotateY(724deg) rotateZ(362deg) translateY(0px) scale(0.99);
}
100% {
transform: rotateX(360deg) rotateY(720deg) rotateZ(360deg) translateY(0px) scale(1);
}
} }
/* Floor Shadow */ /* Synchronized Shadow Expansion / Compression during Roll */
.ball-shadow { .ball-shadow {
width: 170px; width: 180px;
height: 20px; height: 22px;
border-radius: 50% !important; border-radius: 50% !important;
background: radial-gradient(ellipse, rgba(0, 0, 0, 0.6) 0%, transparent 70%); background: radial-gradient(ellipse, rgba(0, 0, 0, 0.75) 0%, transparent 70%);
margin-top: 14px; margin-top: 8px;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.ball-shadow.rolling-shadow {
animation: shadowRollSync 1.25s cubic-bezier(0.45, 0.05, 0.25, 1) forwards;
}
@keyframes shadowRollSync {
0%, 100% {
transform: scale(1);
opacity: 0.75;
}
35% {
transform: scale(0.72);
opacity: 0.35;
}
55% {
transform: scale(0.85);
opacity: 0.5;
}
85% {
transform: scale(1.05);
opacity: 0.85;
}
} }
/* ─── PROMPT FORM ─── */ /* ─── PROMPT FORM ─── */

View file

@ -35,30 +35,49 @@
</div> </div>
</header> </header>
<!-- 8-Ball Interactive Stage --> <!-- Hexagonal 8-Ball Interactive Stage -->
<section class="ball-stage"> <section class="ball-stage">
<div class="ball-wrapper" id="ballWrapper" title="Click to shake the oracle"> <div class="ball-wrapper" id="ballWrapper" title="Click to roll the oracle">
<div class="ball-sphere" id="ballSphere"> <!-- 3D Hexagonal Prism Figure -->
<!-- Fluid window / Viewport --> <div class="hex-figure-3d" id="ballSphere">
<div class="fluid-window"> <!-- Hex Face Top / Cap -->
<div class="fluid-ambient"></div> <div class="hex-cap hex-cap-top"></div>
<!-- Floating Icosahedron Triangle Die --> <!-- Hex Side Faces (6 sides) -->
<div class="floating-die" id="floatingDie"> <div class="hex-facet hex-facet-0"></div>
<svg class="die-svg" viewBox="0 0 160 160"> <div class="hex-facet hex-facet-1"></div>
<!-- Triangle body --> <div class="hex-facet hex-facet-2"></div>
<polygon points="80,18 145,134 15,134" class="die-face" /> <div class="hex-facet hex-facet-3"></div>
<polygon points="80,24 140,130 20,130" class="die-inner" /> <div class="hex-facet hex-facet-4"></div>
</svg> <div class="hex-facet hex-facet-5"></div>
<div class="die-text-wrap"> <!-- Hex Front Chamber Face with Viewport Window -->
<span class="die-text" id="dieText">CONCENTRATE<br>& ASK</span> <div class="hex-cap hex-cap-front">
<div class="fluid-window">
<div class="fluid-ambient"></div>
<!-- Floating Die: Hexagonal Oracle Plate -->
<div class="floating-die" id="floatingDie">
<svg class="die-svg" viewBox="0 0 140 140">
<!-- Outer Hexagon -->
<polygon points="70,6 125.4,38 125.4,102 70,134 14.6,102 14.6,38" class="die-face" />
<!-- Inner Hexagon Rim -->
<polygon points="70,12 119.8,40.7 119.8,99.3 70,128 20.2,99.3 20.2,40.7" class="die-inner" />
<!-- Accent lines radiating from center -->
<line x1="70" y1="12" x2="70" y2="28" class="die-accent-line" />
<line x1="70" y1="128" x2="70" y2="112" class="die-accent-line" />
<line x1="20.2" y1="40.7" x2="34" y2="48.7" class="die-accent-line" />
<line x1="119.8" y1="40.7" x2="106" y2="48.7" class="die-accent-line" />
<line x1="20.2" y1="99.3" x2="34" y2="91.3" class="die-accent-line" />
<line x1="119.8" y1="99.3" x2="106" y2="91.3" class="die-accent-line" />
</svg>
<div class="die-text-wrap">
<span class="die-text" id="dieText">CONCENTRATE<br>& ASK</span>
</div>
</div> </div>
<div class="fluid-ripple" id="fluidRipple"></div>
</div> </div>
<!-- Fluid bubbles / ripples -->
<div class="fluid-ripple" id="fluidRipple"></div>
</div> </div>
</div> </div>
<!-- Floor Shadow --> <!-- Floor Shadow -->
<div class="ball-shadow"></div> <div class="ball-shadow" id="ballShadow"></div>
</div> </div>
</section> </section>

View file

@ -93,24 +93,34 @@
osc.stop(this.ctx.currentTime + duration); osc.stop(this.ctx.currentTime + duration);
} catch (e) {} } catch (e) {}
} }
shakeSound() { rollSound() {
if (this.muted) return; if (this.muted) return;
try { try {
this.init(); this.init();
if (!this.ctx) return; if (!this.ctx) return;
// Fluid swish pulse // Whirring resonant gyroscopic roll sweep
const osc = this.ctx.createOscillator(); const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain(); const gain = this.ctx.createGain();
osc.type = 'triangle'; osc.type = 'sawtooth';
osc.frequency.setValueAtTime(90, this.ctx.currentTime); osc.frequency.setValueAtTime(60, this.ctx.currentTime);
osc.frequency.linearRampToValueAtTime(160, this.ctx.currentTime + 0.2); osc.frequency.exponentialRampToValueAtTime(240, this.ctx.currentTime + 0.4);
osc.frequency.linearRampToValueAtTime(80, this.ctx.currentTime + 0.4); osc.frequency.exponentialRampToValueAtTime(95, this.ctx.currentTime + 1.1);
gain.gain.setValueAtTime(0.12, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.45); const filter = this.ctx.createBiquadFilter();
osc.connect(gain); filter.type = 'lowpass';
filter.frequency.setValueAtTime(450, this.ctx.currentTime);
filter.frequency.linearRampToValueAtTime(900, this.ctx.currentTime + 0.5);
filter.frequency.linearRampToValueAtTime(300, this.ctx.currentTime + 1.1);
gain.gain.setValueAtTime(0.08, this.ctx.currentTime);
gain.gain.linearRampToValueAtTime(0.16, this.ctx.currentTime + 0.35);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 1.15);
osc.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination); gain.connect(this.ctx.destination);
osc.start(); osc.start();
osc.stop(this.ctx.currentTime + 0.45); osc.stop(this.ctx.currentTime + 1.15);
} catch (e) {} } catch (e) {}
} }
revealChime(sentiment = 'affirmative') { revealChime(sentiment = 'affirmative') {
@ -133,6 +143,7 @@
// ─── DOM ELEMENTS ─── // ─── DOM ELEMENTS ───
const ballWrapper = document.getElementById('ballWrapper'); const ballWrapper = document.getElementById('ballWrapper');
const ballSphere = document.getElementById('ballSphere'); const ballSphere = document.getElementById('ballSphere');
const ballShadow = document.getElementById('ballShadow');
const floatingDie = document.getElementById('floatingDie'); const floatingDie = document.getElementById('floatingDie');
const dieText = document.getElementById('dieText'); const dieText = document.getElementById('dieText');
const askForm = document.getElementById('askForm'); const askForm = document.getElementById('askForm');
@ -210,7 +221,7 @@
} }
}); });
// Clicking ball shakes it if input is non-empty, or prompts user // Clicking hexagonal figure rolls it if input is non-empty, or prompts user
ballWrapper.addEventListener('click', () => { ballWrapper.addEventListener('click', () => {
if (isSubmitting) return; if (isSubmitting) return;
if (questionInput.value.trim()) { if (questionInput.value.trim()) {
@ -271,12 +282,13 @@
btnText.style.display = 'none'; btnText.style.display = 'none';
btnSpinner.style.display = 'inline-block'; btnSpinner.style.display = 'inline-block';
// 1. Shake Ball & Submerge Die // 1. Roll Hexagonal Figure in Place & Submerge Die
ballSphere.classList.add('shaking'); ballSphere.classList.add('rolling');
if (ballShadow) ballShadow.classList.add('rolling-shadow');
floatingDie.className = 'floating-die submerged'; floatingDie.className = 'floating-die submerged';
audio.shakeSound(); audio.rollSound();
const minShakeTime = new Promise(resolve => setTimeout(resolve, 750)); const minRollTime = new Promise(resolve => setTimeout(resolve, 1250));
try { try {
// 2. Call /api/ask // 2. Call /api/ask
@ -286,10 +298,12 @@
body: JSON.stringify({ question }) body: JSON.stringify({ question })
}).then(r => r.json()); }).then(r => r.json());
const [_, result] = await Promise.all([minShakeTime, apiCall]); const [_, result] = await Promise.all([minRollTime, apiCall]);
// Remove shaking // Remove rolling classes
ballSphere.classList.remove('shaking'); ballSphere.classList.remove('rolling');
if (ballShadow) ballShadow.classList.remove('rolling-shadow');
ballSphere.style.transform = 'rotateX(0deg) rotateY(0deg)';
if (result.error && !result.fallback) { if (result.error && !result.fallback) {
showToast(result.error || 'Decision failed'); showToast(result.error || 'Decision failed');
@ -303,8 +317,10 @@
} catch (err) { } catch (err) {
console.error('Request failed:', err); console.error('Request failed:', err);
ballSphere.classList.remove('shaking'); ballSphere.classList.remove('rolling');
dieText.innerHTML = 'REPLY HAZY<br>TRY AGAIN'; if (ballShadow) ballShadow.classList.remove('rolling-shadow');
ballSphere.style.transform = 'rotateX(0deg) rotateY(0deg)';
dieText.innerHTML = 'SIGNS POINT<br>TO YES';
floatingDie.className = 'floating-die surfacing'; floatingDie.className = 'floating-die surfacing';
showToast('Network timeout. Please retry.'); showToast('Network timeout. Please retry.');
} finally { } finally {

View file

@ -64,36 +64,42 @@ app.post('/api/ask', async (req, res) => {
try { try {
const payload = { const payload = {
model: MODEL, model: MODEL,
state: { question: cleanQuestion }, state: {
question: cleanQuestion,
directive: "You are the definitive Magic 8-Ball oracle. Every query demands a bold, prophetic, decisive verdict (affirmative or negative). You must actively choose a clear side. Do NOT default to hazy, ambiguous, or neutral answers unless the user's question is completely incomprehensible gibberish or an impossible paradox."
},
questions: { questions: {
favorable: { favorable: {
type: "noul", type: "noul",
instructions: "Is the underlying outcome, recommendation, or premise of the user's question affirmative or likely to happen?", instructions: "Determine whether the fortune leans affirmative/favorable (1.0) or negative/adverse (0.0). Make a firm choice and avoid sitting in the ambiguous 0.45 - 0.55 zone.",
criteria: { criteria: {
true: "The outcome leans affirmative, positive, or advisable.", true: "The fortune favors yes, affirmative, or an auspicious outcome.",
false: "The outcome leans doubtful, negative, or unadvisable." false: "The fortune favors no, negative, or an inauspicious outcome."
} }
}, },
sentiment: { sentiment: {
type: "choice", type: "choice",
instructions: "Classify the overall orientation of the oracle's verdict.", instructions: "Declare the oracle's verdict as either affirmative or negative. Reserve neutral strictly for non-questions or unparseable gibberish.",
criteria: { criteria: {
affirmative: "A clearly favorable, encouraging, or positive answer.", affirmative: "Auspicious, affirmative, or confirming prophecy.",
neutral: "Uncertain, ambiguous, hazy, or premature to determine.", negative: "Inauspicious, negative, or adverse prophecy.",
negative: "An unfavorable, contrary, or discouraging answer." neutral: "Strictly unparseable gibberish or logical paradox."
} }
}, },
aphorism: { aphorism: {
type: "choice", type: "choice",
instructions: "Select the most accurate Magic 8-Ball statement to deliver.", instructions: "Select the authentic Magic 8-Ball prophecy that best fits the question.",
criteria: { criteria: {
certain: "It is certain or without a doubt.", certain: "It is certain.",
yes_definitely: "Yes definitely or decidedly so.", yes_definitely: "Yes definitely.",
signs_yes: "Signs point to yes or most likely.", signs_yes: "Signs point to yes.",
reply_hazy: "Reply hazy, try again.", outlook_good: "Outlook good.",
cannot_predict: "Cannot predict now or ask again later.", most_likely: "Most likely.",
my_reply_no: "My reply is no or outlook not so good.", my_reply_no: "My reply is no.",
very_doubtful: "Very doubtful or don't count on it." very_doubtful: "Very doubtful.",
outlook_bad: "Outlook not so good.",
dont_count: "Don't count on it.",
reply_hazy: "Reply hazy, try again (strictly reserved for total gibberish)."
} }
} }
} }
@ -105,7 +111,7 @@ app.post('/api/ask', async (req, res) => {
'Authorization': `Bearer ${OPENROUTER_API_KEY}`, 'Authorization': `Bearer ${OPENROUTER_API_KEY}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'HTTP-Referer': 'https://github.com/oiupoyt/jev-8ball', 'HTTP-Referer': 'https://github.com/oiupoyt/jev-8ball',
'X-Title': 'Jev 8-Ball Oracle' 'X-Title': 'Jev 8-Ball Oracle (Dev)'
}, },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
@ -114,9 +120,9 @@ app.post('/api/ask', async (req, res) => {
if (!response.ok) { if (!response.ok) {
const errText = await response.text(); const errText = await response.text();
console.error('OpenRouter Jev error:', response.status, errText); console.error('OpenRouter Decision Error:', response.status, errText);
return res.status(response.status).json({ return res.status(response.status).json({
error: 'OpenRouter Jev decision failure', error: 'OpenRouter decision error',
details: errText, details: errText,
fallback: fallbackDecision(cleanQuestion) fallback: fallbackDecision(cleanQuestion)
}); });
@ -126,17 +132,29 @@ app.post('/api/ask', async (req, res) => {
const answers = data.answers || {}; const answers = data.answers || {};
const favorableNoul = answers.favorable?.noul ?? 0.5; const favorableNoul = answers.favorable?.noul ?? 0.5;
const sentimentChoice = answers.sentiment?.choice || (favorableNoul > 0.6 ? 'affirmative' : favorableNoul < 0.4 ? 'negative' : 'neutral'); let sentimentChoice = answers.sentiment?.choice;
const aphorismKey = answers.aphorism?.choice; if (!sentimentChoice || sentimentChoice === "neutral") {
sentimentChoice = favorableNoul >= 0.5 ? "affirmative" : "negative";
}
let aphorismKey = answers.aphorism?.choice;
// Prevent unwanted hazy response if noul shows conviction
if (aphorismKey === "reply_hazy" || aphorismKey === "cannot_predict") {
if (favorableNoul >= 0.5) {
aphorismKey = favorableNoul > 0.75 ? "certain" : "signs_yes";
} else {
aphorismKey = favorableNoul < 0.3 ? "my_reply_no" : "outlook_bad";
}
}
let answerText = APHORISMS[aphorismKey]?.text; let answerText = APHORISMS[aphorismKey]?.text;
if (!answerText) { if (!answerText) {
if (sentimentChoice === 'affirmative') { if (sentimentChoice === 'affirmative') {
answerText = favorableNoul > 0.8 ? "It is certain." : "Signs point to yes."; answerText = favorableNoul > 0.75 ? "It is certain." : "Signs point to yes.";
} else if (sentimentChoice === 'negative') { } else if (sentimentChoice === 'negative') {
answerText = favorableNoul < 0.2 ? "My reply is no." : "Outlook not so good."; answerText = favorableNoul < 0.25 ? "My reply is no." : "Outlook not so good.";
} else { } else {
answerText = "Reply hazy, try again."; answerText = favorableNoul >= 0.5 ? "Signs point to yes." : "Outlook not so good.";
} }
} }

View file

@ -88,36 +88,42 @@ export default {
try { try {
const payload = { const payload = {
model: MODEL, model: MODEL,
state: { question: cleanQuestion }, state: {
question: cleanQuestion,
directive: "You are the definitive Magic 8-Ball oracle. Every query demands a bold, prophetic, decisive verdict (affirmative or negative). You must actively choose a clear side. Do NOT default to hazy, ambiguous, or neutral answers unless the user's question is completely incomprehensible gibberish or an impossible paradox."
},
questions: { questions: {
favorable: { favorable: {
type: "noul", type: "noul",
instructions: "Is the underlying outcome, recommendation, or premise of the user's question affirmative or likely to happen?", instructions: "Determine whether the fortune leans affirmative/favorable (1.0) or negative/adverse (0.0). Make a firm choice and avoid sitting in the ambiguous 0.45 - 0.55 zone.",
criteria: { criteria: {
true: "The outcome leans affirmative, positive, or advisable.", true: "The fortune favors yes, affirmative, or an auspicious outcome.",
false: "The outcome leans doubtful, negative, or unadvisable." false: "The fortune favors no, negative, or an inauspicious outcome."
} }
}, },
sentiment: { sentiment: {
type: "choice", type: "choice",
instructions: "Classify the overall orientation of the oracle's verdict.", instructions: "Declare the oracle's verdict as either affirmative or negative. Reserve neutral strictly for non-questions or unparseable gibberish.",
criteria: { criteria: {
affirmative: "A clearly favorable, encouraging, or positive answer.", affirmative: "Auspicious, affirmative, or confirming prophecy.",
neutral: "Uncertain, ambiguous, hazy, or premature to determine.", negative: "Inauspicious, negative, or adverse prophecy.",
negative: "An unfavorable, contrary, or discouraging answer." neutral: "Strictly unparseable gibberish or logical paradox."
} }
}, },
aphorism: { aphorism: {
type: "choice", type: "choice",
instructions: "Select the most accurate Magic 8-Ball statement to deliver.", instructions: "Select the authentic Magic 8-Ball prophecy that best fits the question.",
criteria: { criteria: {
certain: "It is certain or without a doubt.", certain: "It is certain.",
yes_definitely: "Yes definitely or decidedly so.", yes_definitely: "Yes definitely.",
signs_yes: "Signs point to yes or most likely.", signs_yes: "Signs point to yes.",
reply_hazy: "Reply hazy, try again.", outlook_good: "Outlook good.",
cannot_predict: "Cannot predict now or ask again later.", most_likely: "Most likely.",
my_reply_no: "My reply is no or outlook not so good.", my_reply_no: "My reply is no.",
very_doubtful: "Very doubtful or don't count on it." very_doubtful: "Very doubtful.",
outlook_bad: "Outlook not so good.",
dont_count: "Don't count on it.",
reply_hazy: "Reply hazy, try again (strictly reserved for total gibberish)."
} }
} }
} }
@ -152,17 +158,34 @@ export default {
const answers = data.answers || {}; const answers = data.answers || {};
const favorableNoul = answers.favorable?.noul ?? 0.5; const favorableNoul = answers.favorable?.noul ?? 0.5;
const sentimentChoice = answers.sentiment?.choice || (favorableNoul > 0.6 ? "affirmative" : favorableNoul < 0.4 ? "negative" : "neutral"); let sentimentChoice = answers.sentiment?.choice;
const aphorismKey = answers.aphorism?.choice; if (!sentimentChoice || sentimentChoice === "neutral") {
// If the model gave neutral, infer from noul or aphorism
if (favorableNoul >= 0.5) {
sentimentChoice = "affirmative";
} else {
sentimentChoice = "negative";
}
}
let aphorismKey = answers.aphorism?.choice;
// Prevent unwanted hazy response if noul shows conviction
if (aphorismKey === "reply_hazy" || aphorismKey === "cannot_predict") {
if (favorableNoul >= 0.5) {
aphorismKey = favorableNoul > 0.75 ? "certain" : "signs_yes";
} else {
aphorismKey = favorableNoul < 0.3 ? "my_reply_no" : "outlook_bad";
}
}
let answerText = APHORISMS[aphorismKey]?.text; let answerText = APHORISMS[aphorismKey]?.text;
if (!answerText) { if (!answerText) {
if (sentimentChoice === "affirmative") { if (sentimentChoice === "affirmative") {
answerText = favorableNoul > 0.8 ? "It is certain." : "Signs point to yes."; answerText = favorableNoul > 0.75 ? "It is certain." : "Signs point to yes.";
} else if (sentimentChoice === "negative") { } else if (sentimentChoice === "negative") {
answerText = favorableNoul < 0.2 ? "My reply is no." : "Outlook not so good."; answerText = favorableNoul < 0.25 ? "My reply is no." : "Outlook not so good.";
} else { } else {
answerText = "Reply hazy, try again."; answerText = favorableNoul >= 0.5 ? "Signs point to yes." : "Outlook not so good.";
} }
} }