parent
9e2a5e5e8f
commit
07cdbf1979
@ -1 +1,26 @@
|
|||||||
# api
|
# api
|
||||||
|
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
from llm import call_llm
|
||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app)
|
||||||
|
|
||||||
|
@app.route("/", methods=["GET"])
|
||||||
|
def index():
|
||||||
|
return "Welcome to the Chat API!"
|
||||||
|
|
||||||
|
@app.route("/hello", methods=["POST"])
|
||||||
|
def hello():
|
||||||
|
# get message from request body
|
||||||
|
data = request.get_json()
|
||||||
|
message = data.get("message", "")
|
||||||
|
|
||||||
|
response = call_llm(message, "You are a helpful assistant.")
|
||||||
|
return jsonify({
|
||||||
|
"response": response
|
||||||
|
})
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host="0.0.0.0", port=5000)
|
@ -1 +1,94 @@
|
|||||||
// js code
|
// Replace placeholder JS with chat UI client logic
|
||||||
|
// Handles sending messages to backend and updating the UI
|
||||||
|
|
||||||
|
(function(){
|
||||||
|
const messagesEl = document.getElementById('messages');
|
||||||
|
const form = document.getElementById('composer');
|
||||||
|
const input = document.getElementById('input');
|
||||||
|
const sendBtn = document.getElementById('send');
|
||||||
|
const BASE_URL = "https://automatic-space-funicular-954qxp96rgcqjq-5000.app.github.dev/";
|
||||||
|
const API_ENDPOINT = `${BASE_URL}/hello`; // adjust if your backend runs elsewhere
|
||||||
|
|
||||||
|
function escapeHtml(str){
|
||||||
|
if(!str) return '';
|
||||||
|
return str.replace(/&/g,'&')
|
||||||
|
.replace(/</g,'<')
|
||||||
|
.replace(/>/g,'>')
|
||||||
|
.replace(/"/g,'"')
|
||||||
|
.replace(/'/g,''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatText(text){
|
||||||
|
return escapeHtml(text).replace(/\n/g,'<br>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToBottom(){
|
||||||
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendMessage(role, text){
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'message ' + role;
|
||||||
|
el.innerHTML = `<div class="content">${formatText(text)}</div><small>${new Date().toLocaleTimeString()}</small>`;
|
||||||
|
messagesEl.appendChild(el);
|
||||||
|
scrollToBottom();
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTyping(){
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'message ai';
|
||||||
|
const typing = document.createElement('div');
|
||||||
|
typing.className = 'typing';
|
||||||
|
for(let i=0;i<3;i++){ const d = document.createElement('span'); d.className = 'dot'; typing.appendChild(d); }
|
||||||
|
el.appendChild(typing);
|
||||||
|
messagesEl.appendChild(el);
|
||||||
|
scrollToBottom();
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendToApi(text){
|
||||||
|
const res = await fetch(API_ENDPOINT, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ message: text })
|
||||||
|
});
|
||||||
|
if(!res.ok) throw new Error('Network response was not ok');
|
||||||
|
let json = await res.json();
|
||||||
|
return json.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const text = input.value.trim();
|
||||||
|
if(!text) return;
|
||||||
|
appendMessage('user', text);
|
||||||
|
input.value = '';
|
||||||
|
input.focus();
|
||||||
|
sendBtn.disabled = true;
|
||||||
|
|
||||||
|
const typingEl = createTyping();
|
||||||
|
try{
|
||||||
|
const reply = await sendToApi(text);
|
||||||
|
typingEl.remove();
|
||||||
|
appendMessage('ai', reply || '(no response)');
|
||||||
|
}catch(err){
|
||||||
|
typingEl.remove();
|
||||||
|
appendMessage('ai', 'Error: ' + err.message);
|
||||||
|
console.error(err);
|
||||||
|
}finally{
|
||||||
|
sendBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enter to send, Shift+Enter for newline
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if(e.key === 'Enter' && !e.shiftKey){
|
||||||
|
e.preventDefault();
|
||||||
|
form.dispatchEvent(new Event('submit', { cancelable: true }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Small welcome message
|
||||||
|
appendMessage('ai', 'Hello! I\'m your AI assistant. Ask me anything.');
|
||||||
|
})();
|
@ -1,6 +1,35 @@
|
|||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Chat app</title>
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Stellar AI Chat</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
</head>
|
</head>
|
||||||
<body></body>
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<header class="header">
|
||||||
|
<div style="display:flex;align-items:center;gap:12px;">
|
||||||
|
<div class="logo">🤖</div>
|
||||||
|
<div>
|
||||||
|
<h1>Stellar AI Chat</h1>
|
||||||
|
<p class="subtitle">Dark-mode chat UI — powered by the backend AI</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="chat" id="chat">
|
||||||
|
<div class="messages" id="messages" aria-live="polite"></div>
|
||||||
|
|
||||||
|
<form id="composer" class="composer" action="#">
|
||||||
|
<textarea id="input" placeholder="Say hello — press Enter to send (Shift+Enter for newline)" rows="2"></textarea>
|
||||||
|
<button id="send" type="submit">Send</button>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">Running with backend at <code>http://127.0.0.1:5000/hello</code></footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
@ -1,3 +1,155 @@
|
|||||||
/* body {
|
/* Dark, modern chat styles for the AI chat page */
|
||||||
|
:root{
|
||||||
|
--bg-1: #0f1724;
|
||||||
|
--bg-2: #071226;
|
||||||
|
--panel: rgba(255,255,255,0.03);
|
||||||
|
--glass: rgba(255,255,255,0.04);
|
||||||
|
--accent: #7c3aed; /* purple */
|
||||||
|
--accent-2: #06b6d4; /* cyan */
|
||||||
|
--muted: rgba(255,255,255,0.55);
|
||||||
|
--user-bg: linear-gradient(135deg,#0ea5a4 0%, #06b6d4 100%);
|
||||||
|
--ai-bg: linear-gradient(135deg,#111827 0%, #0b1220 100%);
|
||||||
|
--radius: 14px;
|
||||||
|
--max-width: 900px;
|
||||||
|
}
|
||||||
|
|
||||||
} */
|
*{box-sizing:border-box}
|
||||||
|
html,body{height:100%}
|
||||||
|
body{
|
||||||
|
margin:0;
|
||||||
|
font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
|
||||||
|
background: radial-gradient(1000px 500px at 10% 10%, rgba(124,58,237,0.12), transparent),
|
||||||
|
radial-gradient(800px 400px at 90% 90%, rgba(6,182,212,0.06), transparent),
|
||||||
|
linear-gradient(180deg,var(--bg-1), var(--bg-2));
|
||||||
|
color: #e6eef8;
|
||||||
|
-webkit-font-smoothing:antialiased;
|
||||||
|
-moz-osx-font-smoothing:grayscale;
|
||||||
|
padding:32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app{
|
||||||
|
max-width:var(--max-width);
|
||||||
|
margin:0 auto;
|
||||||
|
height:calc(100vh - 64px);
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
gap:16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header{
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:16px;
|
||||||
|
padding:16px 20px;
|
||||||
|
border-radius:12px;
|
||||||
|
background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));
|
||||||
|
box-shadow: 0 6px 18px rgba(2,6,23,0.6);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
}
|
||||||
|
.header .logo{
|
||||||
|
font-size:28px;
|
||||||
|
width:56px;height:56px;
|
||||||
|
display:flex;align-items:center;justify-content:center;
|
||||||
|
border-radius:12px;
|
||||||
|
background: linear-gradient(135deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01));
|
||||||
|
}
|
||||||
|
.header h1{margin:0;font-size:18px}
|
||||||
|
.header .subtitle{margin:0;font-size:12px;color:var(--muted)}
|
||||||
|
|
||||||
|
.chat{
|
||||||
|
background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));
|
||||||
|
padding:18px;
|
||||||
|
border-radius:16px;
|
||||||
|
flex:1 1 auto;
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
overflow:hidden;
|
||||||
|
box-shadow: 0 20px 40px rgba(2,6,23,0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages{
|
||||||
|
overflow:auto;
|
||||||
|
padding:8px;
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
gap:12px;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message bubble */
|
||||||
|
.message{
|
||||||
|
max-width:85%;
|
||||||
|
display:inline-block;
|
||||||
|
padding:12px 14px;
|
||||||
|
border-radius:12px;
|
||||||
|
color: #e6eef8;
|
||||||
|
line-height:1.4;
|
||||||
|
box-shadow: 0 6px 18px rgba(2,6,23,0.45);
|
||||||
|
}
|
||||||
|
.message.user{
|
||||||
|
margin-left:auto;
|
||||||
|
background: var(--user-bg);
|
||||||
|
border-radius: 16px 16px 6px 16px;
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
.message.ai{
|
||||||
|
margin-right:auto;
|
||||||
|
background: linear-gradient(135deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));
|
||||||
|
border: 1px solid rgba(255,255,255,0.03);
|
||||||
|
color: #cfe6ff;
|
||||||
|
border-radius: 16px 16px 16px 6px;
|
||||||
|
}
|
||||||
|
.message small{display:block;color:var(--muted);font-size:11px;margin-top:6px}
|
||||||
|
|
||||||
|
/* Typing indicator (dots) */
|
||||||
|
.typing{
|
||||||
|
display:inline-flex;gap:6px;align-items:center;padding:8px 12px;border-radius:10px;background:rgba(255,255,255,0.02)
|
||||||
|
}
|
||||||
|
.typing .dot{width:8px;height:8px;border-radius:50%;background:var(--muted);opacity:0.9}
|
||||||
|
@keyframes blink{0%{transform:translateY(0);opacity:0.25}50%{transform:translateY(-4px);opacity:1}100%{transform:translateY(0);opacity:0.25}}
|
||||||
|
.typing .dot:nth-child(1){animation:blink 1s infinite 0s}
|
||||||
|
.typing .dot:nth-child(2){animation:blink 1s infinite 0.15s}
|
||||||
|
.typing .dot:nth-child(3){animation:blink 1s infinite 0.3s}
|
||||||
|
|
||||||
|
/* Composer */
|
||||||
|
.composer{
|
||||||
|
display:flex;
|
||||||
|
gap:12px;
|
||||||
|
align-items:center;
|
||||||
|
padding-top:12px;
|
||||||
|
border-top:1px dashed rgba(255,255,255,0.02);
|
||||||
|
}
|
||||||
|
.composer textarea{
|
||||||
|
resize:none;
|
||||||
|
min-height:44px;
|
||||||
|
max-height:160px;
|
||||||
|
padding:12px 14px;
|
||||||
|
border-radius:12px;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: rgba(255,255,255,0.02);
|
||||||
|
color: #e6eef8;
|
||||||
|
flex:1 1 auto;
|
||||||
|
font-size:14px;
|
||||||
|
}
|
||||||
|
.composer button{
|
||||||
|
background: linear-gradient(135deg,var(--accent),var(--accent-2));
|
||||||
|
color:white;
|
||||||
|
border:none;
|
||||||
|
padding:12px 16px;
|
||||||
|
border-radius:12px;
|
||||||
|
cursor:pointer;
|
||||||
|
font-weight:600;
|
||||||
|
box-shadow: 0 8px 24px rgba(12,6,40,0.5);
|
||||||
|
transition: transform .12s ease, box-shadow .12s ease;
|
||||||
|
}
|
||||||
|
.composer button:active{transform:translateY(1px)}
|
||||||
|
|
||||||
|
.footer{color:var(--muted);font-size:12px;text-align:center}
|
||||||
|
|
||||||
|
/* small screens */
|
||||||
|
@media (max-width:640px){
|
||||||
|
body{padding:16px}
|
||||||
|
.app{height:calc(100vh - 32px)}
|
||||||
|
.header h1{font-size:16px}
|
||||||
|
}
|
Loading…
Reference in new issue