rheoneiro2 / index.html
OllieCentral's picture
Update index.html
8ec31ba verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Rhe-Oneiro · 🔮 Dream Intelligence</title>
<!-- Fonts and Icons -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Poppins:wght@600;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/@phosphor-icons/[email protected]/src/index.css">
<!-- Tailwind config must be defined BEFORE the CDN script to avoid runtime mutation issues -->
<script>
tailwind = {
config: {
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'ui-sans-serif', 'system-ui'],
display: ['Poppins', 'ui-sans-serif', 'system-ui']
},
colors: {
bgdeep: '#1a202c',
bgslate: '#4a5568',
neon: '#ff00a9',
ink: '#e2e8f0',
mist: '#a0aec0',
tealish: '#00f5d4',
amberish: '#ffc700'
},
boxShadow: {
neon: '0 0 10px #ff00a9, 0 0 30px #ff00a9',
glory: 'inset 0 0 60px rgba(255,0,169,.2), 0 10px 40px rgba(0,0,0,.4)'
},
keyframes: {
floaty: { '0%,100%': { transform: 'translateY(0px)' }, '50%': { transform: 'translateY(-6px)' } },
glowPulse: { '0%,100%': { boxShadow: '0 0 6px #ff00a9, 0 0 24px rgba(255,0,169,.5)' }, '50%': { boxShadow: '0 0 20px #ff00a9, 0 0 50px rgba(255,0,169,.9)' } },
turn: { '0%': { transform: 'rotate(0deg)' }, '100%': { transform: 'rotate(360deg)' } },
wiggle: { '0%,100%': { transform: 'rotate(-1deg)' }, '50%': { transform: 'rotate(1deg)' } }
},
animation: {
floaty: 'floaty 6s ease-in-out infinite',
glowPulse: 'glowPulse 2.6s ease-in-out infinite',
turn: 'turn 14s linear infinite',
wiggle: 'wiggle 6s ease-in-out infinite'
}
}
}
}
};
</script>
<!-- Load Tailwind with the Typography plugin so prose classes work -->
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
<style>
:root { --bg1: #1a202c; --bg2: #4a5568; --neon: #ff00a9; --ink: #e2e8f0; --mist: #a0aec0; --teal: #00f5d4; --amber: #ffc700; }
body { background: radial-gradient(1200px 800px at 20% 10%, rgba(255,0,169,.18), transparent 40%), linear-gradient(135deg, var(--bg1), var(--bg2)); }
.noise { position: fixed; inset: 0; pointer-events: none; opacity: .09; background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" viewBox="0 0 400 400"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.95" numOctaves="2" stitchTiles="stitch"/></filter><rect width="100%" height="100%" filter="url(%23n)" opacity="0.6"/></svg>'); mix-blend-mode: overlay; }
.portal { position: absolute; inset: 10% 4%; border-radius: 18px; border: 2px solid rgba(255,0,169,.6); box-shadow: 0 0 30px rgba(255,0,169,.5), 0 0 90px rgba(255,0,169,.3); filter: drop-shadow(0 0 12px rgba(255,0,169,.8)); }
.portal::before { content: ""; position: absolute; inset: -12px; border-radius: 26px; border: 2px solid rgba(255,0,169,.25); }
.glass { backdrop-filter: blur(14px) saturate(120%); background: linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.04)); border: 1px solid rgba(255,255,255,.12); }
.neontext { text-shadow: 0 0 12px rgba(255,0,169,.9), 0 0 32px rgba(255,0,169,.5); }
.gradientBorder { position: relative; }
.gradientBorder:before { content: ""; position: absolute; inset: -1px; border-radius: 16px; padding: 1px; background: linear-gradient(135deg, rgba(255,0,169,1), rgba(0,245,212,1)); -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor; mask-composite: exclude; }
.btn-neon { box-shadow: 0 0 12px rgba(255,0,169,.8), inset 0 0 12px rgba(255,0,169,.2); }
.emoji-badge { filter: drop-shadow(0 6px 20px rgba(255,0,169,.5)); }
.route-hidden { display: none; }
.route-active { display: block; }
@media (min-width: 768px) {
.portal {
inset: 10% 35%;
}
}
</style>
</head>
<body class="text-ink min-h-screen antialiased">
<div class="noise"></div>
<div class="pointer-events-none fixed -z-10 inset-0">
<div class="portal animate-glowPulse"></div>
<div class="absolute -right-10 top-10 w-48 h-48 rounded-full bg-[radial-gradient(circle_at_center,rgba(0,245,212,.45),transparent_60%)] blur-2xl opacity-70 animate-floaty"></div>
<div class="absolute -left-16 bottom-16 w-64 h-64 rounded-full bg-[radial-gradient(circle_at_center,rgba(255,0,169,.45),transparent_60%)] blur-2xl opacity-60 animate-floaty"></div>
</div>
<div id="app"></div>
<!-- React UMD builds -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- Enable JSX in the browser for this single-file app -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- Application script. Use JSX compiled by Babel. -->
<script type="text/babel">
const { useState, useEffect, useRef, Fragment } = React;
// --- App Configuration ---
const HF_MODEL = 'deepseek-ai/DeepSeek-V3.1-Terminus';
const HF_API_URL = 'https://router.huggingface.co/v1/chat/completions';
const HfTokenManager = {
get() { return localStorage.getItem('hf_token') || ''; },
set(token) { localStorage.setItem('hf_token', token || ''); },
};
const useHashRoute = () => {
const [route, setRoute] = useState(location.hash.replace('#', '') || 'analyze');
useEffect(() => {
const onHash = () => setRoute(location.hash.replace('#', '') || 'analyze');
window.addEventListener('hashchange', onHash);
return () => window.removeEventListener('hashchange', onHash);
}, []);
return [route, r => location.hash = r];
};
const Badge = ({ children }) => (
<span className="emoji-badge align-middle ml-2">{children}</span>
);
const TopBar = ({ onOpenSettings }) => (
<header className="sticky top-0 z-40">
<div className="glass backdrop-blur-xl border-b border-white/10">
<div className="max-w-6xl mx-auto px-4 py-3 flex flex-wrap items-center justify-between gap-y-2">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-black/30 flex items-center justify-center shadow-neon">
<span className="text-2xl neontext">O</span>
</div>
<div>
<h1 className="font-display text-lg md:text-xl tracking-wide">Rhe-Oneiro <span className="text-neon">🔮</span></h1>
<p className="text-mist text-xs hidden sm:block">Dream Intelligence. Elegant by design.</p>
</div>
</div>
<nav className="flex items-center gap-2">
<a href="#analyze" className="px-2 md:px-3 py-1.5 rounded-lg hover:bg-white/10 transition"><span className="hidden md:inline">Analysis </span>📊</a>
<a href="#new" className="px-2 md:px-3 py-1.5 rounded-lg hover:bg-white/10 transition"><span className="hidden md:inline">New Dream </span></a>
<button title="Hugging Face Token" className="p-2 rounded-lg hover:bg-white/10 transition" onClick={onOpenSettings}>
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.096 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
</nav>
</div>
</div>
</header>
);
const Chip = ({ text, icon }) => (
<span className="glass px-2.5 py-1 rounded-lg text-sm text-mist border border-white/10 inline-flex items-center gap-1">
{icon && <i className={`ph-${icon} text-sm`}></i>}<span>{text}</span>
</span>
);
const SectionCard = ({ title, subtitle, children, icon }) => (
<section className="gradientBorder rounded-2xl relative h-full">
<div className="glass rounded-2xl p-5 md:p-7 shadow-glory h-full flex flex-col">
<div className="flex items-center gap-2 mb-3">
{icon && <i className={`ph-${icon} text-2xl text-neon animate-wiggle`}></i>}
<h2 className="font-display text-lg md:text-xl">{title}</h2>
</div>
{subtitle && <p className="text-mist mb-4 text-sm">{subtitle}</p>}
<div className="flex-grow flex flex-col">{children}</div>
</div>
</section>
);
const TokenModal = ({ open, onClose }) => {
const [token, setToken] = useState(HfTokenManager.get());
const save = () => {
HfTokenManager.set(token);
onClose();
window.location.reload(); // Reload to ensure app state is fresh
};
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-black/60" onClick={onClose}></div>
<div className="glass w-[92%] max-w-lg rounded-2xl p-6 border border-white/10 relative">
<h3 className="font-display text-xl mb-2">Hugging Face API Token <Badge>🔑</Badge></h3>
<p className="text-mist text-sm mb-4">
This app requires a Hugging Face API token to function. Your token is saved in your browser and never sent to our servers. Get your token from your <a href="https://huggingface.co/settings/tokens" target="_blank" rel="noopener noreferrer" className="text-tealish underline hover:text-neon">Hugging Face account settings</a>.
</p>
<label className="text-sm">API Token</label>
<input
type="password"
className="w-full mt-1 mb-6 px-3 py-2 rounded-xl bg-black/40 border border-white/15 focus:outline-none focus:ring-2 focus:ring-neon"
placeholder="hf_..."
value={token}
onChange={e => setToken(e.target.value)}
/>
<div className="flex justify-end gap-2">
<button className="px-4 py-2 rounded-xl bg-white/10 hover:bg-white/20" onClick={onClose}>Cancel</button>
<button className="px-4 py-2 rounded-xl bg-neon/20 text-ink border border-neon btn-neon" onClick={save}>Save & Reload</button>
</div>
</div>
</div>
);
};
const streamHuggingFace = async ({ prompt }) => {
const apiKey = HfTokenManager.get();
if (!apiKey) throw new Error('Missing Hugging Face API token. Please set it in the settings.');
const sys = `You are Rhe-Oneiro. Analyze a detailed dream description. Follow this structure strictly.
Steps: Identify Key Elements. Symbolic Analysis. Emotional Significance. Psychological Themes. Synthesize Relationships.
Output Format: Symbol Interpretation. Emotional Context. Psychological Analysis.
Avoid em dashes. Use colons, semicolons, or periods.`;
const body = {
model: HF_MODEL,
messages: [
{ role: 'system', content: sys },
{ role: 'user', content: prompt }
],
stream: true,
temperature: 0.6,
max_tokens: 1200
};
const resp = await fetch(HF_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify(body)
});
if (!resp.ok) {
const t = await resp.text();
throw new Error('Hugging Face API error: ' + t);
}
const reader = resp.body.getReader();
const decoder = new TextDecoder();
return new ReadableStream({
start(controller) {
const readChunk = () => reader.read().then(({ value, done }) => {
if (done) { controller.close(); return; }
const chunk = decoder.decode(value, { stream: true });
chunk.split('\n').forEach(line => {
if (!line.startsWith('data: ')) return;
const data = line.slice(6).trim();
if (!data || data === '[DONE]') return;
try {
const json = JSON.parse(data);
const token = json.choices?.[0]?.delta?.content || '';
if (token) controller.enqueue(token);
} catch {}
});
readChunk();
}).catch(err => controller.error(err));
readChunk();
}
});
};
const getHuggingFaceCompletion = async ({ prompt }) => {
const apiKey = HfTokenManager.get();
if (!apiKey) throw new Error('Missing Hugging Face API token. Please set it in the settings.');
const body = {
model: HF_MODEL,
messages: [{ role: 'user', content: prompt }],
stream: false,
temperature: 0.5,
max_tokens: 250
};
const resp = await fetch(HF_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify(body)
});
if (!resp.ok) {
const t = await resp.text();
throw new Error('Hugging Face API error: ' + t);
}
const json = await resp.json();
return json.choices?.[0]?.message?.content || 'Failed to generate summary.';
};
const Markdown = ({ text }) => {
const html = marked.parse(text || '');
return <div className="prose prose-invert max-w-none"><div dangerouslySetInnerHTML={{ __html: html }} /></div>;
};
const Analyzer = () => {
const [input, setInput] = useState('I was climbing a glass staircase inside a neon museum. A pink portal hummed while a calm sea rose in the lobby. A stray black cat watched. I could not find my voice. My late grandmother touched my shoulder. I woke up relieved and unsettled.');
const [out, setOut] = useState('');
const [busy, setBusy] = useState(false);
const outRef = useRef(null);
const run = async () => {
try {
setOut('');
setBusy(true);
let fullAnalysis = '';
const stream = await streamHuggingFace({ prompt: input });
const reader = stream.getReader();
const pump = () => reader.read().then(async ({ value, done }) => {
if (done) {
setOut(prev => prev + '\n\n---\n\n**TL;DR**\n\n*Generating summary...*');
const tldrPrompt = `Summarize the following dream analysis. Provide only the summary text, keeping it concise (around 100-120 words). Do not include a "TL;DR" heading, do not mention the word count, and do not add any conversational fluff. Just provide the summary paragraph.\n\nAnalysis:\n${fullAnalysis}`;
const summary = await getHuggingFaceCompletion({ prompt: tldrPrompt });
setOut(prev => {
const newText = prev.replace('*Generating summary...*', summary);
setTimeout(() => {
if(outRef.current) {
outRef.current.scrollTop = outRef.current.scrollHeight;
}
}, 0);
return newText;
});
setBusy(false);
return;
}
const token = value; // Value is already a string
fullAnalysis += token;
setOut(prev => prev + (token || ''));
if(outRef.current) {
outRef.current.scrollTop = outRef.current.scrollHeight;
}
return pump();
});
pump();
} catch(err) {
setBusy(false);
setOut('Error: ' + err.message);
}
};
const copy = () => {
const tempTextArea = document.createElement('textarea');
tempTextArea.value = out;
document.body.appendChild(tempTextArea);
tempTextArea.select();
try {
document.execCommand('copy');
} catch (err) {
console.error('Fallback: Oops, unable to copy', err);
}
document.body.removeChild(tempTextArea);
};
const downloadTxt = () => {
const blob = new Blob([out], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = Object.assign(document.createElement('a'), { href: url, download: 'rhe-oneiro-analysis.txt' });
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url);
document.body.removeChild(a);
};
return (
<div className="max-w-6xl mx-auto px-4 pb-24 pt-6">
<div className="grid md:grid-cols-2 gap-6 items-stretch">
<SectionCard title="Describe your dream" subtitle="Write in vivid detail. The analyzer organizes the result under the required headings." icon="sparkle">
<div className="flex flex-col gap-3 h-full">
<div className="flex flex-wrap gap-2">
<Chip text="#analysis" icon="brain" />
<Chip text="🔮 symbolism" />
<Chip text="✨ archetypes" />
<Chip text="🪞 emotions" />
</div>
<textarea className="w-full flex-grow p-4 rounded-2xl bg-black/40 border border-white/15 focus:outline-none focus:ring-2 focus:ring-neon" value={input} onChange={e => setInput(e.target.value)} placeholder="Type or paste a dream in detail..."></textarea>
<div className="flex items-center gap-3">
<button onClick={run} disabled={busy || !input} className="px-5 py-2.5 rounded-2xl bg-neon/20 border border-neon btn-neon hover:shadow-neon transition flex items-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"><i className="ph-magic-wand"></i> {busy ? 'Analyzing…' : 'Analyze'}</button>
<button onClick={() => setInput('')} className="px-4 py-2 rounded-2xl bg-white/10">Clear</button>
</div>
<p className="text-sm text-mist">This app uses your Hugging Face API key client-side. Nothing is stored on our servers.</p>
</div>
</SectionCard>
<SectionCard title="Structured interpretation" subtitle="Live streaming result in three sections" icon="chart-bar">
<div className="flex flex-col gap-3 h-full">
<div ref={outRef} className="flex-grow overflow-y-auto rounded-2xl glass p-4 border border-white/10">
{busy && !out && <div className="flex justify-center items-center h-full"><div className="animate-spin rounded-full h-8 w-8 border-b-2 border-neon"></div></div>}
{out ? <Markdown text={out} /> : !busy && <p className="text-mist">Results will appear here.</p>}
</div>
<div className="flex items-center gap-2">
<button onClick={copy} disabled={!out} className="px-3 py-2 rounded-xl bg-white/10 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"><i className="ph-copy"></i> Copy</button>
<button onClick={downloadTxt} disabled={!out} className="px-3 py-2 rounded-xl bg-white/10 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"><i className="ph-download"></i> Save</button>
</div>
</div>
</SectionCard>
</div>
<div className="mt-8 grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="md:col-span-2">
<SectionCard title="Inspiration" icon="video">
<div className="rounded-xl overflow-hidden shadow-neon">
<div className="aspect-video bg-black">
<iframe
className="w-full h-full"
src="https://www.youtube.com/embed/tMN6mRkGFeE?loop=1&playlist=tMN6mRkGFeE&controls=1"
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen>
</iframe>
</div>
</div>
</SectionCard>
</div>
<SectionCard title="Quick tips" icon="lightbulb-filament">
<ul className="list-disc pl-5 text-sm text-mist space-y-2">
<li>Write sensory details for richer symbolism.</li>
<li>Note people and animals. They can map to archetypes or projections.</li>
<li>End with your waking feeling. It guides the emotional thread.</li>
</ul>
</SectionCard>
</div>
</div>
);
};
const App = () => {
const [route, setRoute] = useHashRoute();
const [tokenModalOpen, setTokenModalOpen] = useState(false);
useEffect(() => {
document.title = 'Rhe-Oneiro · 🔮 Dream Intelligence';
if (!HfTokenManager.get()) {
setTokenModalOpen(true);
}
}, []);
const handleCloseTokenModal = () => {
setTokenModalOpen(false);
};
return (
<Fragment>
<TopBar onOpenSettings={() => setTokenModalOpen(true)} />
<main>
<div className={(route === 'analyze' || route === 'new' || route === '') ? 'route-active' : 'route-hidden'}>
<Analyzer />
</div>
</main>
<footer className="text-center text-mist text-sm pb-8">
<div>© 2025 Rhe-Oneiro. Crafted with OLLIESENSE✨</div>
</footer>
<TokenModal open={tokenModalOpen} onClose={handleCloseTokenModal} />
</Fragment>
);
};
const root = ReactDOM.createRoot(document.getElementById('app'));
root.render(<App />);
</script>
</body>
</html>