feat: 3D hexagonal figure with roll-in-place animation and decisive Jev oracle prompting
This commit is contained in:
parent
f7f14e37e9
commit
fc464cb68b
6 changed files with 336 additions and 146 deletions
|
|
@ -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.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 ─── */
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
66
server.js
66
server.js
|
|
@ -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.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
65
worker.mjs
65
worker.mjs
|
|
@ -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.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue