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 {
const payload = {
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: {
favorable: {
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: {
true: "The outcome leans affirmative, positive, or advisable.",
false: "The outcome leans doubtful, negative, or unadvisable."
true: "The fortune favors yes, affirmative, or an auspicious outcome.",
false: "The fortune favors no, negative, or an inauspicious outcome."
}
},
sentiment: {
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: {
affirmative: "A clearly favorable, encouraging, or positive answer.",
neutral: "Uncertain, ambiguous, hazy, or premature to determine.",
negative: "An unfavorable, contrary, or discouraging answer."
affirmative: "Auspicious, affirmative, or confirming prophecy.",
negative: "Inauspicious, negative, or adverse prophecy.",
neutral: "Strictly unparseable gibberish or logical paradox."
}
},
aphorism: {
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: {
certain: "It is certain or without a doubt.",
yes_definitely: "Yes definitely or decidedly so.",
signs_yes: "Signs point to yes or most likely.",
reply_hazy: "Reply hazy, try again.",
cannot_predict: "Cannot predict now or ask again later.",
my_reply_no: "My reply is no or outlook not so good.",
very_doubtful: "Very doubtful or don't count on it."
certain: "It is certain.",
yes_definitely: "Yes definitely.",
signs_yes: "Signs point to yes.",
outlook_good: "Outlook good.",
most_likely: "Most likely.",
my_reply_no: "My reply is no.",
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 favorableNoul = answers.favorable?.noul ?? 0.5;
const sentimentChoice = answers.sentiment?.choice || (favorableNoul > 0.6 ? "affirmative" : favorableNoul < 0.4 ? "negative" : "neutral");
const aphorismKey = answers.aphorism?.choice;
let sentimentChoice = answers.sentiment?.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;
if (!answerText) {
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") {
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 {
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);
}
/* ─── 8-BALL STAGE & VESSEL ─── */
/* ─── HEXAGONAL 3D FIGURE STAGE & VESSEL ─── */
.ball-stage {
display: flex;
justify-content: center;
align-items: center;
margin: 10px 0 30px;
perspective: 900px;
margin: 14px 0 32px;
perspective: 1000px;
}
.ball-wrapper {
@ -162,30 +162,78 @@ h1 {
flex-direction: column;
align-items: center;
cursor: pointer;
perspective: 1000px;
}
.ball-sphere {
width: 230px;
height: 230px;
border-radius: 50% !important; /* Keep ball spherical */
background: radial-gradient(circle at 35% 30%, #2e2e2e 0%, #171717 40%, #090909 75%, #000000 100%);
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);
/* Hexagonal 3D Prism Figure */
.hex-figure-3d {
width: 210px;
height: 210px;
position: relative;
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;
align-items: 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 {
width: 140px;
height: 140px;
border-radius: 50% !important; /* Inner circular liquid reservoir */
background: radial-gradient(circle, #081226 0%, #040916 65%, #010308 100%);
border: 2px solid rgba(255, 255, 255, 0.08);
box-shadow: inset 0 4px 18px rgba(0, 0, 0, 0.95), 0 0 10px rgba(0, 0, 0, 0.5);
width: 142px;
height: 142px;
clip-path: polygon(50% 0%, 93.3% 25%, 93.3% 75%, 50% 100%, 6.7% 75%, 6.7% 25%);
background: radial-gradient(circle, #08142a 0%, #040916 65%, #010308 100%);
border: 1px solid rgba(56, 189, 248, 0.15);
box-shadow: inset 0 4px 22px rgba(0, 0, 0, 0.95), 0 0 14px rgba(0, 0, 0, 0.8);
position: relative;
overflow: hidden;
display: flex;
@ -196,11 +244,11 @@ h1 {
.fluid-ambient {
position: absolute;
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;
}
/* Floating Triangle Die */
/* Floating Hexagonal Die Plate */
.floating-die {
position: relative;
width: 120px;
@ -231,14 +279,21 @@ h1 {
}
.die-inner {
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 {
position: relative;
z-index: 2;
width: 78px;
width: 82px;
text-align: center;
margin-top: 14px;
margin-top: 0;
}
.die-text {
@ -247,17 +302,17 @@ h1 {
letter-spacing: 0.04em;
color: #bae6fd;
text-transform: uppercase;
line-height: 1.25;
line-height: 1.28;
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;
}
/* Submerged state */
.floating-die.submerged {
transform: scale(0.35) translateY(18px) rotate(15deg) !important;
transform: scale(0.28) translateY(24px) rotate(35deg) !important;
opacity: 0;
filter: blur(4px);
filter: blur(5px);
animation: none;
}
@ -266,30 +321,71 @@ h1 {
animation: dieSurface 1.1s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
@keyframes dieSurface {
0% { transform: scale(0.4) translateY(20px) rotate(-20deg); opacity: 0; filter: blur(4px); }
60% { transform: scale(1.06) translateY(-2px) rotate(2deg); opacity: 0.9; filter: blur(0); }
0% { transform: scale(0.35) translateY(24px) rotate(-30deg); opacity: 0; filter: blur(5px); }
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); }
}
/* Shake Animation on Ball */
.ball-sphere.shaking {
animation: eightBallShake 0.75s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
/* ─── 3D ROLL-IN-PLACE ANIMATION ─── */
.hex-figure-3d.rolling {
animation: hexRollInPlace 1.25s cubic-bezier(0.45, 0.05, 0.25, 1) forwards;
}
@keyframes eightBallShake {
10%, 90% { transform: translate3d(-3px, 0, 0) rotate(-2deg); }
20%, 80% { transform: translate3d(4px, 0, 0) rotate(2deg); }
30%, 50%, 70% { transform: translate3d(-6px, 0, 0) rotate(-3deg); }
40%, 60% { transform: translate3d(6px, 0, 0) rotate(3deg); }
@keyframes hexRollInPlace {
0% {
transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg) scale(1);
}
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 {
width: 170px;
height: 20px;
width: 180px;
height: 22px;
border-radius: 50% !important;
background: radial-gradient(ellipse, rgba(0, 0, 0, 0.6) 0%, transparent 70%);
margin-top: 14px;
background: radial-gradient(ellipse, rgba(0, 0, 0, 0.75) 0%, transparent 70%);
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 ─── */

View file

@ -35,30 +35,49 @@
</div>
</header>
<!-- 8-Ball Interactive Stage -->
<!-- Hexagonal 8-Ball Interactive Stage -->
<section class="ball-stage">
<div class="ball-wrapper" id="ballWrapper" title="Click to shake the oracle">
<div class="ball-sphere" id="ballSphere">
<!-- Fluid window / Viewport -->
<div class="fluid-window">
<div class="fluid-ambient"></div>
<!-- Floating Icosahedron Triangle Die -->
<div class="floating-die" id="floatingDie">
<svg class="die-svg" viewBox="0 0 160 160">
<!-- Triangle body -->
<polygon points="80,18 145,134 15,134" class="die-face" />
<polygon points="80,24 140,130 20,130" class="die-inner" />
</svg>
<div class="die-text-wrap">
<span class="die-text" id="dieText">CONCENTRATE<br>& ASK</span>
<div class="ball-wrapper" id="ballWrapper" title="Click to roll the oracle">
<!-- 3D Hexagonal Prism Figure -->
<div class="hex-figure-3d" id="ballSphere">
<!-- Hex Face Top / Cap -->
<div class="hex-cap hex-cap-top"></div>
<!-- Hex Side Faces (6 sides) -->
<div class="hex-facet hex-facet-0"></div>
<div class="hex-facet hex-facet-1"></div>
<div class="hex-facet hex-facet-2"></div>
<div class="hex-facet hex-facet-3"></div>
<div class="hex-facet hex-facet-4"></div>
<div class="hex-facet hex-facet-5"></div>
<!-- Hex Front Chamber Face with Viewport Window -->
<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 class="fluid-ripple" id="fluidRipple"></div>
</div>
<!-- Fluid bubbles / ripples -->
<div class="fluid-ripple" id="fluidRipple"></div>
</div>
</div>
<!-- Floor Shadow -->
<div class="ball-shadow"></div>
<div class="ball-shadow" id="ballShadow"></div>
</div>
</section>

View file

@ -93,24 +93,34 @@
osc.stop(this.ctx.currentTime + duration);
} catch (e) {}
}
shakeSound() {
rollSound() {
if (this.muted) return;
try {
this.init();
if (!this.ctx) return;
// Fluid swish pulse
// Whirring resonant gyroscopic roll sweep
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(90, this.ctx.currentTime);
osc.frequency.linearRampToValueAtTime(160, this.ctx.currentTime + 0.2);
osc.frequency.linearRampToValueAtTime(80, this.ctx.currentTime + 0.4);
gain.gain.setValueAtTime(0.12, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.45);
osc.connect(gain);
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(60, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(240, this.ctx.currentTime + 0.4);
osc.frequency.exponentialRampToValueAtTime(95, this.ctx.currentTime + 1.1);
const filter = this.ctx.createBiquadFilter();
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);
osc.start();
osc.stop(this.ctx.currentTime + 0.45);
osc.stop(this.ctx.currentTime + 1.15);
} catch (e) {}
}
revealChime(sentiment = 'affirmative') {
@ -133,6 +143,7 @@
// ─── DOM ELEMENTS ───
const ballWrapper = document.getElementById('ballWrapper');
const ballSphere = document.getElementById('ballSphere');
const ballShadow = document.getElementById('ballShadow');
const floatingDie = document.getElementById('floatingDie');
const dieText = document.getElementById('dieText');
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', () => {
if (isSubmitting) return;
if (questionInput.value.trim()) {
@ -271,12 +282,13 @@
btnText.style.display = 'none';
btnSpinner.style.display = 'inline-block';
// 1. Shake Ball & Submerge Die
ballSphere.classList.add('shaking');
// 1. Roll Hexagonal Figure in Place & Submerge Die
ballSphere.classList.add('rolling');
if (ballShadow) ballShadow.classList.add('rolling-shadow');
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 {
// 2. Call /api/ask
@ -286,10 +298,12 @@
body: JSON.stringify({ question })
}).then(r => r.json());
const [_, result] = await Promise.all([minShakeTime, apiCall]);
const [_, result] = await Promise.all([minRollTime, apiCall]);
// Remove shaking
ballSphere.classList.remove('shaking');
// Remove rolling classes
ballSphere.classList.remove('rolling');
if (ballShadow) ballShadow.classList.remove('rolling-shadow');
ballSphere.style.transform = 'rotateX(0deg) rotateY(0deg)';
if (result.error && !result.fallback) {
showToast(result.error || 'Decision failed');
@ -303,8 +317,10 @@
} catch (err) {
console.error('Request failed:', err);
ballSphere.classList.remove('shaking');
dieText.innerHTML = 'REPLY HAZY<br>TRY AGAIN';
ballSphere.classList.remove('rolling');
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';
showToast('Network timeout. Please retry.');
} finally {

View file

@ -64,36 +64,42 @@ app.post('/api/ask', async (req, res) => {
try {
const payload = {
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: {
favorable: {
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: {
true: "The outcome leans affirmative, positive, or advisable.",
false: "The outcome leans doubtful, negative, or unadvisable."
true: "The fortune favors yes, affirmative, or an auspicious outcome.",
false: "The fortune favors no, negative, or an inauspicious outcome."
}
},
sentiment: {
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: {
affirmative: "A clearly favorable, encouraging, or positive answer.",
neutral: "Uncertain, ambiguous, hazy, or premature to determine.",
negative: "An unfavorable, contrary, or discouraging answer."
affirmative: "Auspicious, affirmative, or confirming prophecy.",
negative: "Inauspicious, negative, or adverse prophecy.",
neutral: "Strictly unparseable gibberish or logical paradox."
}
},
aphorism: {
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: {
certain: "It is certain or without a doubt.",
yes_definitely: "Yes definitely or decidedly so.",
signs_yes: "Signs point to yes or most likely.",
reply_hazy: "Reply hazy, try again.",
cannot_predict: "Cannot predict now or ask again later.",
my_reply_no: "My reply is no or outlook not so good.",
very_doubtful: "Very doubtful or don't count on it."
certain: "It is certain.",
yes_definitely: "Yes definitely.",
signs_yes: "Signs point to yes.",
outlook_good: "Outlook good.",
most_likely: "Most likely.",
my_reply_no: "My reply is no.",
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}`,
'Content-Type': 'application/json',
'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)
});
@ -114,9 +120,9 @@ app.post('/api/ask', async (req, res) => {
if (!response.ok) {
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({
error: 'OpenRouter Jev decision failure',
error: 'OpenRouter decision error',
details: errText,
fallback: fallbackDecision(cleanQuestion)
});
@ -126,17 +132,29 @@ app.post('/api/ask', async (req, res) => {
const answers = data.answers || {};
const favorableNoul = answers.favorable?.noul ?? 0.5;
const sentimentChoice = answers.sentiment?.choice || (favorableNoul > 0.6 ? 'affirmative' : favorableNoul < 0.4 ? 'negative' : 'neutral');
const aphorismKey = answers.aphorism?.choice;
let sentimentChoice = answers.sentiment?.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;
if (!answerText) {
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') {
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 {
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 {
const payload = {
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: {
favorable: {
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: {
true: "The outcome leans affirmative, positive, or advisable.",
false: "The outcome leans doubtful, negative, or unadvisable."
true: "The fortune favors yes, affirmative, or an auspicious outcome.",
false: "The fortune favors no, negative, or an inauspicious outcome."
}
},
sentiment: {
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: {
affirmative: "A clearly favorable, encouraging, or positive answer.",
neutral: "Uncertain, ambiguous, hazy, or premature to determine.",
negative: "An unfavorable, contrary, or discouraging answer."
affirmative: "Auspicious, affirmative, or confirming prophecy.",
negative: "Inauspicious, negative, or adverse prophecy.",
neutral: "Strictly unparseable gibberish or logical paradox."
}
},
aphorism: {
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: {
certain: "It is certain or without a doubt.",
yes_definitely: "Yes definitely or decidedly so.",
signs_yes: "Signs point to yes or most likely.",
reply_hazy: "Reply hazy, try again.",
cannot_predict: "Cannot predict now or ask again later.",
my_reply_no: "My reply is no or outlook not so good.",
very_doubtful: "Very doubtful or don't count on it."
certain: "It is certain.",
yes_definitely: "Yes definitely.",
signs_yes: "Signs point to yes.",
outlook_good: "Outlook good.",
most_likely: "Most likely.",
my_reply_no: "My reply is no.",
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 favorableNoul = answers.favorable?.noul ?? 0.5;
const sentimentChoice = answers.sentiment?.choice || (favorableNoul > 0.6 ? "affirmative" : favorableNoul < 0.4 ? "negative" : "neutral");
const aphorismKey = answers.aphorism?.choice;
let sentimentChoice = answers.sentiment?.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;
if (!answerText) {
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") {
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 {
answerText = "Reply hazy, try again.";
answerText = favorableNoul >= 0.5 ? "Signs point to yes." : "Outlook not so good.";
}
}