mirror of
https://github.com/microsoft/Web-Dev-For-Beginners.git
synced 2025-09-03 19:52:52 +02:00
a first chat app
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
venv
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
@@ -1 +1,26 @@
|
||||
# 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)
|
@@ -8,21 +8,22 @@ client = OpenAI(
|
||||
api_key=os.environ["GITHUB_TOKEN"],
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What is the capital of France?",
|
||||
}
|
||||
],
|
||||
model="openai/gpt-4o-mini",
|
||||
temperature=1,
|
||||
max_tokens=4096,
|
||||
top_p=1
|
||||
)
|
||||
def call_llm(prompt: str, system_message: str):
|
||||
response = client.chat.completions.create(
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": system_message,
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt,
|
||||
}
|
||||
],
|
||||
model="openai/gpt-4o-mini",
|
||||
temperature=1,
|
||||
max_tokens=4096,
|
||||
top_p=1
|
||||
)
|
||||
|
||||
print(response.choices[0].message.content)
|
||||
return response.choices[0].message.content
|
@@ -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>
|
||||
<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>
|
||||
<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>
|
@@ -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}
|
||||
}
|
Reference in New Issue
Block a user