diff --git a/.gitignore b/.gitignore index a0625636..08d00376 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.user *.userosscache *.sln.docstates +venv # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/09-chat-project/backend/api.py b/09-chat-project/backend/api.py index e84f94cf..bad25776 100644 --- a/09-chat-project/backend/api.py +++ b/09-chat-project/backend/api.py @@ -1 +1,26 @@ -# api \ No newline at end of file +# 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) \ No newline at end of file diff --git a/09-chat-project/backend/llm.py b/09-chat-project/backend/llm.py index d9d42cb5..609516bf 100644 --- a/09-chat-project/backend/llm.py +++ b/09-chat-project/backend/llm.py @@ -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) \ No newline at end of file + return response.choices[0].message.content \ No newline at end of file diff --git a/09-chat-project/frontend/app.js b/09-chat-project/frontend/app.js index 385b5be0..8f9efee9 100644 --- a/09-chat-project/frontend/app.js +++ b/09-chat-project/frontend/app.js @@ -1 +1,94 @@ -// js code \ No newline at end of file +// 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,'''); + } + + function formatText(text){ + return escapeHtml(text).replace(/\n/g,'
'); + } + + function scrollToBottom(){ + messagesEl.scrollTop = messagesEl.scrollHeight; + } + + function appendMessage(role, text){ + const el = document.createElement('div'); + el.className = 'message ' + role; + el.innerHTML = `
${formatText(text)}
${new Date().toLocaleTimeString()}`; + 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.'); +})(); \ No newline at end of file diff --git a/09-chat-project/frontend/index.html b/09-chat-project/frontend/index.html index 52125560..8504d3b7 100644 --- a/09-chat-project/frontend/index.html +++ b/09-chat-project/frontend/index.html @@ -1,6 +1,35 @@ - + - Chat app + + + Stellar AI Chat + + - + +
+
+
+ +
+

Stellar AI Chat

+

Dark-mode chat UI — powered by the backend AI

+
+
+
+ +
+
+ +
+ + +
+
+ + +
+ + + \ No newline at end of file diff --git a/09-chat-project/frontend/styles.css b/09-chat-project/frontend/styles.css index 1ce2089e..1b5cdb55 100644 --- a/09-chat-project/frontend/styles.css +++ b/09-chat-project/frontend/styles.css @@ -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; +} -} */ \ No newline at end of file +*{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} +} \ No newline at end of file