Spaces:
Running
Running
| <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> |