Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions src/main/resources/static/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
const chatState = {
sessionId: crypto.randomUUID(),
isWaiting: false
};

const dom = {
personality: document.getElementById('personality-select'),
input: document.getElementById('user-input'),
button: document.getElementById('send-button'),
window: document.getElementById('chat-window'),
typing: document.getElementById('typing-indicator'),
chips: document.querySelectorAll('.god-chip')
};

// ── God chip selection ──────────────────────────────────────────

dom.chips.forEach(chip => {
chip.addEventListener('click', () => {
dom.chips.forEach(c => c.classList.remove('active'));
chip.classList.add('active');
dom.personality.value = chip.dataset.god;
});
});

// Set Heimdall as default active chip on load
const defaultChip = document.querySelector('.god-chip[data-god="HEIMDALL"]');
if (defaultChip) defaultChip.classList.add('active');
dom.personality.value = 'HEIMDALL';

// ── Chat ────────────────────────────────────────────────────────

async function sendMessage() {
const message = dom.input.value.trim();
if (!message || chatState.isWaiting) return;

const selectedPersonality = dom.personality.value;
const godName = getGodName(selectedPersonality);

appendMessage('user', null, message);
dom.input.value = '';
setLoading(true);

try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 15000);
let response;

try {
response = await fetch('/api/v1/chat', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
personality: selectedPersonality,
message: message,
sessionId: chatState.sessionId
}),
signal: controller.signal
});
} finally {
clearTimeout(timeoutId);
}

if (!response.ok) throw new Error("Divine connection lost.");

const aiText = await response.text();
appendMessage('assistant', godName, aiText);
} catch (error) {
appendMessage('assistant', 'System', "Error: " + error.message);
} finally {
setLoading(false);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

function appendMessage(role, name, text) {
const msgDiv = document.createElement('div');
msgDiv.className = `message ${role}`;

if (role === 'assistant') {
const label = document.createElement('div');
label.className = 'god-label';
label.textContent = name;
msgDiv.appendChild(label);
msgDiv.appendChild(document.createTextNode(text));
} else {
msgDiv.textContent = text;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

dom.window.appendChild(msgDiv);
dom.window.scrollTop = dom.window.scrollHeight;
}

function setLoading(active) {
chatState.isWaiting = active;
dom.typing.classList.toggle('hidden', !active);
dom.button.disabled = active;
}

function getGodName(value) {
const names = {
ODIN: 'Odin',
LOKI: 'Loki',
THOR: 'Thor',
FREYJA: 'Freyja',
HEIMDALL: 'Heimdall'
};
return names[value] || value;
}

// ── Event listeners ─────────────────────────────────────────────

dom.button.addEventListener('click', sendMessage);
dom.input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage().then(r => {
}).catch(err => console.error(err));
}
});
67 changes: 67 additions & 0 deletions src/main/resources/static/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bifrost AI | Mythological Gateway</title>
<link rel="stylesheet" href="style.css">
</head>
<body>

<div class="rune-border"></div>

<div class="app-container">
<header>
<div class="header-ornament">
<div class="ornament-line"></div>
<svg class="valknut" viewBox="0 0 40 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<polygon points="20,2 10,20 30,20" stroke="#c8973a" stroke-width="1.5"/>
<polygon points="9,21 2,34 21,34" stroke="#c8973a" stroke-width="1.5"/>
<polygon points="31,21 19,34 38,34" stroke="#c8973a" stroke-width="1.5"/>
</svg>
<div class="ornament-line"></div>
</div>
<h1>Asgard AI</h1>
<p>SPEAK TO THE GODS</p>
</header>

<select id="personality-select" class="hidden-select">
<option value="ODIN">Odin</option>
<option value="LOKI">Loki</option>
<option value="THOR">Thor</option>
<option value="FREYJA">Freyja</option>
<option value="HEIMDALL">Heimdall</option>
</select>

<main>
<nav class="god-selector">
<button type="button" class="god-chip" data-god="THOR" aria-pressed="false">⚡ Thor</button>
<button type="button" class="god-chip" data-god="FREYJA" aria-pressed="false">🌿 Freyja</button>
<button type="button" class="god-chip" data-god="ODIN" aria-pressed="false">👁 Odin</button>
<button type="button" class="god-chip" data-god="LOKI" aria-pressed="false">🔥 Loki</button>
<button type="button" class="god-chip active" data-god="HEIMDALL" aria-pressed="true">⚓ Heimdall</button>
</nav>

<section id="chat-container">
<div id="chat-window" aria-live="polite" aria-relevant="additions">
<div class="message assistant">
<div class="god-label">Heimdall</div>
I see all who approach the bridge. Who do you wish to speak with, mortal?
</div>
</div>
<div id="typing-indicator" class="hidden" role="status" aria-live="polite">The Gods are contemplating...
</div>
</section>

<section class="input-area">
<textarea id="user-input" placeholder="Type your message to the Gods..." rows="1"></textarea>
<button id="send-button">Send ᚱ</button>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</section>
</main>

<div class="rune-footer">ᚠ ᚢ ᚦ ᚨ ᚱ ᚲ ᚷ ᚹ ᚺ ᚾ ᛁ ᛃ ᛇ ᛈ ᛉ ᛊ ᛏ ᛒ ᛖ ᛗ ᛚ ᛜ ᛞ ᛟ</div>
</div>

<script src="app.js"></script>
</body>
</html>
Loading