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:
parent
e39c54d87a
commit
35534a4f4c
5 changed files with 537 additions and 10 deletions
68
api/app.py
68
api/app.py
|
|
@ -640,6 +640,74 @@ def contact_form():
|
|||
except Exception as e:
|
||||
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 ─────────────────────────────────────────
|
||||
@app.route('/api/backups/posts')
|
||||
@require_auth
|
||||
|
|
|
|||
|
|
@ -33,5 +33,9 @@
|
|||
"name": "",
|
||||
"key": "",
|
||||
"url": ""
|
||||
},
|
||||
"venice": {
|
||||
"api_key": "VENICE_ADMIN_KEY_xSnxLlHxPGk4BUPzSmHVpLO3xLvKD-MadtwH-xZuUG",
|
||||
"model": "llama-3.3-70b"
|
||||
}
|
||||
}
|
||||
271
css/style.css
271
css/style.css
|
|
@ -1807,3 +1807,274 @@ a:hover { color: #fff; text-shadow: none; }
|
|||
background: #3a1a1a;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
32
index.html
32
index.html
|
|
@ -150,17 +150,28 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Middle Column: Identity -->
|
||||
<!-- Middle Column: AI Chat Terminal -->
|
||||
<div class="hud-col-center">
|
||||
<div class="hud-identity">
|
||||
<div class="hero-label">SYSTEM IDENTIFICATION</div>
|
||||
<h1 class="hero-title">
|
||||
<span class="glitch" data-text="JAESWIFT">JAESWIFT</span>
|
||||
</h1>
|
||||
<div class="hero-subtitle">
|
||||
<span class="typing-prefix">> </span>
|
||||
<span class="typing-text" id="typingText"></span>
|
||||
<span class="typing-cursor">█</span>
|
||||
<div class="panel chat-terminal">
|
||||
<div class="panel-header chat-header">
|
||||
<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>
|
||||
</div>
|
||||
<div class="chat-welcome-sub">SYSTEM IDENTIFICATION // AI TERMINAL</div>
|
||||
<div class="chat-welcome-hint">Type a message to interact with JAE-AI</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-input-area">
|
||||
<span class="chat-prompt-icon">>_</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>
|
||||
|
|
@ -562,5 +573,6 @@
|
|||
|
||||
<script src="/js/main.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script src="/js/chat.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
172
js/chat.js
Normal file
172
js/chat.js
Normal 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);
|
||||
|
||||
})();
|
||||
Loading…
Add table
Reference in a new issue