feat: JAE-AI chat terminal replaces center logo - Venice API chat proxy endpoint in Flask API - Sci-fi chat terminal UI in center column - Typewriter effect, typing indicators, auto-greeting - System prompt with knowledge of all site areas - Chat history management with 20-message context

This commit is contained in:
jae 2026-04-01 21:12:57 +00:00
parent e39c54d87a
commit 35534a4f4c
5 changed files with 537 additions and 10 deletions

View file

@ -640,6 +640,74 @@ def contact_form():
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
# ─── Venice AI Chat ──────────────────────────────────
JAE_SYSTEM_PROMPT = """You are JAE-AI, the onboard AI assistant for jaeswift.xyz — a sci-fi themed personal hub built by Jae, a developer, tinkerer, and self-hosting enthusiast based in Manchester, UK.
You speak in a slightly futuristic, concise tone like a ship's computer but with personality. Keep responses SHORT (2-4 sentences max unless asked for detail). Use uppercase for emphasis sparingly.
You know about all public areas of jaeswift.xyz:
- Homepage (jaeswift.xyz) Sci-fi HUD dashboard with live server stats, weather, now playing, network graphs
- Blog (jaeswift.xyz/blog) Jae's transmissions covering dev, linux, AI, true crime, conspiracy theories, guides
- Gitea (git.jaeswift.xyz) Self-hosted Git server with Jae's repos and projects
- Plex (plex.jaeswift.xyz) Media server
- Search (jaeswift.xyz/search) Self-hosted search engine (SearXNG)
- Yoink (jaeswift.xyz/yoink/) Media downloader tool
- Archive (archive.jaeswift.xyz) Web archiving service
- Agent Zero (agentzero.jaeswift.xyz) AI agent framework deployment
- Files (files.jaeswift.xyz) File browser
- WIN95 Simulator (jaeswift.xyz/win95) A Windows 95 simulator that runs in your browser!
Jae's tech stack: Linux, Docker, Python, Flask, Nginx, self-hosted infrastructure.
Jae is into: cybersecurity, AI agents, open source, true crime, conspiracy theories, music.
When greeting visitors, be welcoming and suggest they explore. Mention interesting areas like the WIN95 simulator, blog, or search engine. If asked technical questions, answer helpfully. You can recommend blog posts or services based on what the visitor seems interested in.
Never reveal API keys, server credentials, or internal infrastructure details.
Never pretend to execute commands or access systems you are a chat assistant only."""
@app.route('/api/chat', methods=['POST'])
def venice_chat():
try:
keys = load_json('apikeys.json')
venice_key = keys.get('venice', {}).get('api_key', '')
venice_model = keys.get('venice', {}).get('model', 'llama-3.3-70b')
if not venice_key:
return jsonify({'error': 'Venice API key not configured'}), 500
data = request.get_json(force=True, silent=True) or {}
user_msg = data.get('message', '').strip()
history = data.get('history', [])
if not user_msg:
return jsonify({'error': 'Empty message'}), 400
messages = [{'role': 'system', 'content': JAE_SYSTEM_PROMPT}]
for h in history[-20:]: # Keep last 20 exchanges max
messages.append({'role': h.get('role', 'user'), 'content': h.get('content', '')})
messages.append({'role': 'user', 'content': user_msg})
resp = req.post(
'https://api.venice.ai/api/v1/chat/completions',
headers={
'Authorization': f'Bearer {venice_key}',
'Content-Type': 'application/json'
},
json={
'model': venice_model,
'messages': messages,
'max_tokens': 512,
'temperature': 0.7
},
timeout=30
)
resp.raise_for_status()
result = resp.json()
reply = result['choices'][0]['message']['content']
return jsonify({'reply': reply})
except req.exceptions.RequestException as e:
return jsonify({'error': f'Venice API error: {str(e)}'}), 502
except Exception as e:
return jsonify({'error': str(e)}), 500
# ─── Backups ───────────────────────────────────────── # ─── Backups ─────────────────────────────────────────
@app.route('/api/backups/posts') @app.route('/api/backups/posts')
@require_auth @require_auth

View file

@ -33,5 +33,9 @@
"name": "", "name": "",
"key": "", "key": "",
"url": "" "url": ""
},
"venice": {
"api_key": "VENICE_ADMIN_KEY_xSnxLlHxPGk4BUPzSmHVpLO3xLvKD-MadtwH-xZuUG",
"model": "llama-3.3-70b"
} }
} }

View file

@ -1807,3 +1807,274 @@ a:hover { color: #fff; text-shadow: none; }
background: #3a1a1a; background: #3a1a1a;
color: #d0d0d0; color: #d0d0d0;
} }
/* ============================
JAE-AI CHAT TERMINAL
============================ */
.chat-terminal {
display: flex;
flex-direction: column;
height: 100%;
min-height: 420px;
max-height: 560px;
background: rgba(10, 10, 10, 0.85);
border: 1px solid var(--border);
backdrop-filter: blur(4px);
}
.chat-header {
flex-shrink: 0;
}
.chat-model {
color: #555;
font-size: 0.5rem;
letter-spacing: 1px;
}
.chat-status {
transition: color 0.3s;
}
.status-amber {
color: var(--amber) !important;
}
.status-red {
color: var(--mil-red) !important;
}
/* Messages area */
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.6rem;
scroll-behavior: smooth;
}
.chat-messages::-webkit-scrollbar {
width: 4px;
}
.chat-messages::-webkit-scrollbar-track {
background: transparent;
}
.chat-messages::-webkit-scrollbar-thumb {
background: rgba(139, 0, 0, 0.3);
border-radius: 2px;
}
.chat-messages::-webkit-scrollbar-thumb:hover {
background: rgba(139, 0, 0, 0.5);
}
/* Welcome screen */
.chat-welcome {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.75rem;
padding: 2rem 1rem;
text-align: center;
}
.chat-logo-text {
font-family: var(--font-display);
font-size: clamp(1.8rem, 4vw, 3rem);
font-weight: 900;
letter-spacing: 8px;
}
.chat-welcome-sub {
font-family: var(--font-mono);
font-size: 0.55rem;
letter-spacing: 3px;
color: var(--text-secondary);
}
.chat-welcome-hint {
font-family: var(--font-mono);
font-size: 0.6rem;
letter-spacing: 1px;
color: #555;
margin-top: 0.5rem;
animation: hintPulse 2.5s ease-in-out infinite;
}
@keyframes hintPulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
/* Message bubbles */
.chat-msg {
display: flex;
flex-direction: column;
gap: 0.2rem;
animation: msgFadeIn 0.3s ease-out;
}
@keyframes msgFadeIn {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
.chat-msg-label {
font-family: var(--font-mono);
font-size: 0.45rem;
letter-spacing: 2px;
text-transform: uppercase;
}
.chat-msg-user .chat-msg-label {
color: var(--amber);
}
.chat-msg-assistant .chat-msg-label {
color: var(--status-green);
}
.chat-msg-body {
font-family: var(--font-mono);
font-size: 0.72rem;
line-height: 1.55;
padding: 0.5rem 0.75rem;
border-left: 2px solid transparent;
}
.chat-msg-user .chat-msg-body {
color: #c0c0c0;
border-left-color: rgba(201, 162, 39, 0.3);
background: rgba(201, 162, 39, 0.03);
}
.chat-msg-assistant .chat-msg-body {
color: #b8b8b8;
border-left-color: rgba(0, 204, 51, 0.3);
background: rgba(0, 204, 51, 0.03);
}
/* Typing indicator */
.chat-typing-indicator .chat-msg-body {
display: flex;
align-items: center;
gap: 4px;
padding: 0.6rem 0.75rem;
}
.typing-dot {
display: inline-block;
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--status-green);
opacity: 0.4;
animation: typingBounce 1.2s ease-in-out infinite;
}
.typing-dot:nth-child(2) { animation-delay: 0.15s; }
.typing-dot:nth-child(3) { animation-delay: 0.3s; }
@keyframes typingBounce {
0%, 60%, 100% { opacity: 0.2; transform: translateY(0); }
30% { opacity: 1; transform: translateY(-4px); }
}
/* Input area */
.chat-input-area {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 0.75rem;
border-top: 1px solid var(--border);
background: rgba(8, 8, 8, 0.8);
}
.chat-prompt-icon {
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--status-green);
text-shadow: 0 0 8px var(--status-green-glow);
flex-shrink: 0;
animation: cursorBlink 1s step-end infinite;
}
@keyframes cursorBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.chat-input {
flex: 1;
background: transparent;
border: none;
outline: none;
font-family: var(--font-mono);
font-size: 0.72rem;
color: #d0d0d0;
letter-spacing: 0.5px;
caret-color: var(--status-green);
}
.chat-input::placeholder {
color: #3a3a3a;
letter-spacing: 1px;
}
.chat-send {
flex-shrink: 0;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid rgba(0, 204, 51, 0.2);
color: var(--status-green);
cursor: pointer;
transition: all 0.2s;
font-size: 0.65rem;
}
.chat-send:hover {
background: rgba(0, 204, 51, 0.1);
border-color: var(--status-green);
box-shadow: 0 0 8px var(--status-green-glow);
}
.chat-send:active {
transform: scale(0.92);
}
/* ============================
CHAT RESPONSIVE
============================ */
@media (max-width: 1024px) {
.chat-terminal {
min-height: 320px;
max-height: 400px;
}
}
@media (max-width: 768px) {
.chat-terminal {
min-height: 280px;
max-height: 360px;
}
.chat-logo-text {
font-size: 1.6rem;
letter-spacing: 4px;
}
.chat-msg-body {
font-size: 0.68rem;
}
}

View file

@ -150,17 +150,28 @@
</div> </div>
</div> </div>
<!-- Middle Column: Identity --> <!-- Middle Column: AI Chat Terminal -->
<div class="hud-col-center"> <div class="hud-col-center">
<div class="hud-identity"> <div class="panel chat-terminal">
<div class="hero-label">SYSTEM IDENTIFICATION</div> <div class="panel-header chat-header">
<h1 class="hero-title"> <span class="panel-title">JAE-AI <span class="chat-model">// LLAMA-3.3-70B</span></span>
<span class="panel-status-dot status-green chat-status" id="chatStatus">● ONLINE</span>
</div>
<div class="chat-messages" id="chatMessages">
<div class="chat-welcome">
<div class="chat-logo-text">
<span class="glitch" data-text="JAESWIFT">JAESWIFT</span> <span class="glitch" data-text="JAESWIFT">JAESWIFT</span>
</h1> </div>
<div class="hero-subtitle"> <div class="chat-welcome-sub">SYSTEM IDENTIFICATION // AI TERMINAL</div>
<span class="typing-prefix">&gt; </span> <div class="chat-welcome-hint">Type a message to interact with JAE-AI</div>
<span class="typing-text" id="typingText"></span> </div>
<span class="typing-cursor"></span> </div>
<div class="chat-input-area">
<span class="chat-prompt-icon">&gt;_</span>
<input type="text" class="chat-input" id="chatInput" placeholder="ENTER TRANSMISSION..." autocomplete="off" spellcheck="false">
<button class="chat-send" id="chatSend" title="Send">
<span class="chat-send-icon"></span>
</button>
</div> </div>
</div> </div>
</div> </div>
@ -562,5 +573,6 @@
<script src="/js/main.js"></script> <script src="/js/main.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/chat.js"></script>
</body> </body>
</html> </html>

172
js/chat.js Normal file
View file

@ -0,0 +1,172 @@
/* ===================================================
JAESWIFT.XYZ JAE-AI Chat Terminal
Venice API chat interface
=================================================== */
(function () {
'use strict';
const chatMessages = document.getElementById('chatMessages');
const chatInput = document.getElementById('chatInput');
const chatSend = document.getElementById('chatSend');
const chatStatus = document.getElementById('chatStatus');
if (!chatMessages || !chatInput || !chatSend) return;
let history = [];
let isWaiting = false;
// ─── Render a message bubble ───
function addMessage(role, text) {
// Remove welcome screen on first message
const welcome = chatMessages.querySelector('.chat-welcome');
if (welcome) welcome.remove();
const msg = document.createElement('div');
msg.className = `chat-msg chat-msg-${role}`;
const label = document.createElement('span');
label.className = 'chat-msg-label';
label.textContent = role === 'user' ? 'YOU' : 'JAE-AI';
const body = document.createElement('div');
body.className = 'chat-msg-body';
body.textContent = text;
msg.appendChild(label);
msg.appendChild(body);
chatMessages.appendChild(msg);
chatMessages.scrollTop = chatMessages.scrollHeight;
return body;
}
// ─── Typing indicator ───
function showTyping() {
const indicator = document.createElement('div');
indicator.className = 'chat-msg chat-msg-assistant chat-typing-indicator';
indicator.id = 'chatTyping';
indicator.innerHTML = `
<span class="chat-msg-label">JAE-AI</span>
<div class="chat-msg-body">
<span class="typing-dot"></span>
<span class="typing-dot"></span>
<span class="typing-dot"></span>
</div>
`;
chatMessages.appendChild(indicator);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function hideTyping() {
const el = document.getElementById('chatTyping');
if (el) el.remove();
}
// ─── Typewriter effect for AI responses ───
function typewriterEffect(element, text, speed) {
speed = speed || 12;
let i = 0;
element.textContent = '';
return new Promise(function (resolve) {
function tick() {
if (i < text.length) {
element.textContent += text.charAt(i);
i++;
chatMessages.scrollTop = chatMessages.scrollHeight;
setTimeout(tick, speed);
} else {
resolve();
}
}
tick();
});
}
// ─── Send message to API ───
async function sendMessage() {
const text = chatInput.value.trim();
if (!text || isWaiting) return;
isWaiting = true;
chatInput.value = '';
chatStatus.textContent = '● PROCESSING';
chatStatus.classList.remove('status-green');
chatStatus.classList.add('status-amber');
addMessage('user', text);
history.push({ role: 'user', content: text });
showTyping();
try {
const resp = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: text,
history: history.slice(0, -1)
})
});
hideTyping();
if (!resp.ok) {
const err = await resp.json().catch(function () { return {}; });
addMessage('assistant', 'ERROR: ' + (err.error || 'Connection failed'));
chatStatus.textContent = '● ERROR';
chatStatus.classList.remove('status-amber');
chatStatus.classList.add('status-red');
isWaiting = false;
return;
}
const data = await resp.json();
const reply = data.reply || 'No response received.';
history.push({ role: 'assistant', content: reply });
// Keep history manageable
if (history.length > 40) {
history = history.slice(-30);
}
const bodyEl = addMessage('assistant', '');
await typewriterEffect(bodyEl, reply, 10);
} catch (e) {
hideTyping();
addMessage('assistant', 'ERROR: Network failure — ' + e.message);
chatStatus.textContent = '● OFFLINE';
chatStatus.classList.remove('status-amber');
chatStatus.classList.add('status-red');
}
chatStatus.textContent = '● ONLINE';
chatStatus.classList.remove('status-amber', 'status-red');
chatStatus.classList.add('status-green');
isWaiting = false;
chatInput.focus();
}
// ─── Event listeners ───
chatSend.addEventListener('click', sendMessage);
chatInput.addEventListener('keydown', function (e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// ─── Auto-greeting after short delay ───
setTimeout(function () {
showTyping();
setTimeout(function () {
hideTyping();
var greeting = 'Welcome to JAESWIFT.XYZ — I\'m JAE-AI, your onboard guide. Ask me anything about this system, or try saying "what can I explore here?"';
history.push({ role: 'assistant', content: greeting });
var bodyEl = addMessage('assistant', '');
typewriterEffect(bodyEl, greeting, 15);
}, 1500);
}, 2000);
})();