Spaces:
Running
Running
File size: 9,316 Bytes
3a1960e a7e7318 284c59f 3a1960e 3672af3 3a1960e 3672af3 3a1960e 3672af3 3a1960e 3672af3 3a1960e 3672af3 3a1960e 3672af3 3a1960e 3672af3 ab34076 3a1960e a424734 3a1960e ab34076 a7e7318 e59019b ab34076 3a1960e ab34076 3672af3 ab34076 3a1960e 3672af3 3a1960e ab34076 953a37d 3a1960e ab34076 3a1960e ab34076 3a1960e ab34076 3a1960e ab34076 3a1960e ab34076 3a1960e ab34076 3a1960e ab34076 3a1960e ab34076 3a1960e ab34076 3a1960e |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>University Assistant</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet"
href="{{ url_for('static', filename='style.css') }}">
<!-- <link rel="stylesheet" href="../static/style.css"> -->
<script>
tailwind.config = {
darkMode: 'class',
};
</script>
</head>
<body class="bg-zinc-100 text-black dark:bg-slate-950 dark:text-white flex items-center justify-center min-h-screen">
<div
class="flex w-full flex-col overflow-hidden border border-gray-300 dark:border-gray-700 shadow-lg rounded-lg h-screen relative">
<!-- Theme Toggle Icon -->
<button id="theme-toggle"
class="absolute top-2 right-2 z-20 flex h-9 w-9 items-center justify-center rounded-full bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-black dark:text-white shadow transition-all"
title="Toggle Theme">
π
</button>
<div class="flex flex-col flex-grow overflow-hidden">
<div id="chat-container" class="flex-grow overflow-y-auto p-3 space-y-3 bg-zinc-100 dark:bg-slate-950">
<!-- Chat messages will appear here -->
</div>
<div class="border-t p-2 border-gray-300 dark:border-slate-800">
<div class="flex gap-2">
<input id="chat-input"
class="flex-grow rounded-md border border-gray-300 dark:border-slate-800 bg-white dark:bg-slate-800 px-3 py-1 text-sm text-black dark:text-white focus:outline-none"
type="text" placeholder="Type your question here..." />
<button id="send-button" class="rounded-md bg-blue-600 px-3 py-1 text-white hover:bg-blue-700">
β€
</button>
</div>
</div>
</div>
</div>
<script>
const toggleButton = document.getElementById("theme-toggle");
toggleButton.addEventListener("click", () => {
document.documentElement.classList.toggle("dark");
// Optional: save preference
if (document.documentElement.classList.contains("dark")) {
localStorage.setItem("theme", "dark");
} else {
localStorage.setItem("theme", "light");
}
});
// Optional: load theme from localStorage
if (localStorage.getItem("theme") === "dark") {
document.documentElement.classList.add("dark");
}
const API_BASE_URL = window.location.origin;
const chatContainer = document.getElementById("chat-container");
const chatInput = document.getElementById("chat-input");
const sendButton = document.getElementById("send-button");
const initialMessage = {
id: "1",
text: "Hello! I'm the JUIT University Assistant. How can I help you today? !(The first message may take 10-15s to load)",
isUser: false,
};
let messages = [initialMessage];
let isTyping = false;
function renderMessages() {
chatContainer.innerHTML = "";
messages.forEach((msg) => {
const bubble = document.createElement("div");
bubble.className = `flex items-start gap-2 ${msg.isUser ? "flex-row-reverse" : "flex-row"}`;
const avatar = document.createElement("img");
avatar.className = "h-9 w-9 rounded-full object-cover";
// Use complete URLs for images
const baseUrl = window.location.origin;
avatar.src = msg.isUser ? `${baseUrl}/static/profile.png` : `${baseUrl}/static/JUIT_icon.png`;
// avatar.src = msg.isUser ? `../static/profile.png` : `../static/JUIT_icon.png`;
avatar.alt = msg.isUser ? "" : "";
avatar.onerror = function () {
// Fallback if image fails to load
this.src = msg.isUser ?
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='12' fill='%230ea5e9'/%3E%3C/svg%3E" :
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='12' fill='%23475569'/%3E%3C/svg%3E";
};
const msgDiv = document.createElement("div");
msgDiv.className = `max-w-[75%] break-words rounded-md px-3 py-1 text-sm ${
msg.isUser
? "bg-blue-600 text-white rounded-tr-none"
: "bg-gray-200 text-black light:bg-gray-700 light:text-white rounded-tl-none"
}`;
msgDiv.innerText = msg.text;
bubble.appendChild(avatar);
bubble.appendChild(msgDiv);
chatContainer.appendChild(bubble);
});
if (isTyping) {
const typing = document.createElement("div");
typing.className = "flex items-start gap-2";
typing.innerHTML = `
<img src="../static/JUIT_icon.png" alt="Bot" class="h-9 w-9 rounded-full object-cover" />
<div class="max-w-[75%] rounded-md rounded-tl-none bg-gray-200 light:bg-gray-700 px-3 py-1 text-sm">
<div class="dot-typing text-black light:text-white">
<span>.</span><span>.</span><span>.</span>
</div>
</div>
`;
chatContainer.appendChild(typing);
}
chatContainer.scrollTop = chatContainer.scrollHeight;
}
async function simulateBotResponse(userInput) {
try {
const response = await fetch(`${API_BASE_URL}/get`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
message: userInput
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API error (${response.status}): ${errorText}`);
}
const data = await response.json();
if (data.error) throw new Error(data.error);
return data.response;
} catch (error) {
console.error("Error fetching response:", error);
throw error;
}
}
async function handleSendMessage() {
const text = chatInput.value.trim();
if (!text || isTyping) return;
const userMessage = {
id: Date.now().toString(),
text,
isUser: true,
};
messages.push(userMessage);
chatInput.value = "";
isTyping = true;
// Render messages, including typing animation
renderMessages();
// Add typing animation (". . .")
const typingMessage = {
id: "typing",
text: ". . .", // Use animated dots here
isUser: false,
};
messages.push(typingMessage);
try {
const res = await simulateBotResponse(text);
// Remove typing animation once response is received
messages.pop(); // Removes the ". . ." animation message
const botMessage = {
id: (Date.now() + 1).toString(),
text: res.response,
isUser: false,
};
messages.push(botMessage);
} catch (error) {
// Remove typing animation if there's an error
messages.pop(); // Removes the ". . ." animation message
messages.push({
id: Date.now().toString(),
text: `Error: ${error.message || "Failed to get response. Please try again."}`,
isUser: false,
});
}
isTyping = false;
renderMessages();
}
// Check server health on page load
async function checkServerHealth() {
try {
const response = await fetch(`${API_BASE_URL}/health`);
if (!response.ok) {
console.warn("Server health check failed");
}
} catch (error) {
console.error("Server health check error:", error);
}
}
sendButton.addEventListener("click", handleSendMessage);
chatInput.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
});
// Initialize
renderMessages();
checkServerHealth();
</script>
</body>
</html>
|