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
+
+
-
+
+
+
+
+
\ 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