Spaces:
Running
Running
can you add a sidebar for more features such as cube root, greatest common factor and a lot more scientific features? and make it so that even pressing the sceietific feature like squre root not just directly becore like equal cause it will just perform the action right away. add peranthese too - Follow Up Deployment
Browse files- index.html +1028 -563
index.html
CHANGED
|
@@ -3,668 +3,1133 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
<style>
|
| 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 |
</style>
|
| 36 |
</head>
|
| 37 |
-
<body class="bg-gray-900 text-
|
| 38 |
-
<div class="
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
<div class="p-4">
|
| 42 |
-
<
|
| 43 |
-
<
|
| 44 |
-
<
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
</div>
|
|
|
|
| 52 |
</div>
|
| 53 |
|
| 54 |
-
<div class="bg-gray-700 rounded-lg
|
| 55 |
-
<div
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
</div>
|
| 58 |
|
| 59 |
-
<div class="flex
|
| 60 |
-
<
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
</button>
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
</div>
|
| 67 |
</div>
|
| 68 |
|
| 69 |
-
<!--
|
| 70 |
-
<div class="
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
<
|
| 92 |
-
<button id="
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
<!-- Scientific Functions Row -->
|
| 96 |
-
<button id="power" class="bg-purple-500 hover:bg-purple-600 text-white text-xl p-3 rounded-lg calculator-btn">x^y</button>
|
| 97 |
-
<button id="sin" class="bg-purple-500 hover:bg-purple-600 text-white text-xl p-3 rounded-lg calculator-btn">sin</button>
|
| 98 |
-
<button id="cos" class="bg-purple-500 hover:bg-purple-600 text-white text-xl p-3 rounded-lg calculator-btn">cos</button>
|
| 99 |
-
<button id="tan" class="bg-purple-500 hover:bg-purple-600 text-white text-xl p-3 rounded-lg calculator-btn">tan</button>
|
| 100 |
-
|
| 101 |
-
<!-- More Functions Row -->
|
| 102 |
-
<button id="log" class="bg-purple-500 hover:bg-purple-600 text-white text-xl p-3 rounded-lg calculator-btn">log</button>
|
| 103 |
-
<button id="ln" class="bg-purple-500 hover:bg-purple-600 text-white text-xl p-3 rounded-lg calculator-btn">ln</button>
|
| 104 |
-
<button id="pi" class="bg-purple-500 hover:bg-purple-600 text-white text-xl p-3 rounded-lg calculator-btn">π</button>
|
| 105 |
-
<button id="factorial" class="bg-purple-500 hover:bg-purple-600 text-white text-xl p-3 rounded-lg calculator-btn">n!</button>
|
| 106 |
</div>
|
| 107 |
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
<
|
| 112 |
-
<button id="close-sidebar" class="text-gray-300 hover:text-white">
|
| 113 |
-
<i class="fas fa-times"></i>
|
| 114 |
-
</button>
|
| 115 |
</div>
|
| 116 |
|
| 117 |
-
<div class="
|
| 118 |
-
<
|
| 119 |
-
<
|
| 120 |
-
<button id="gcd" class="bg-purple-500 hover:bg-purple-600 text-white p-2 rounded calculator-btn">GCD</button>
|
| 121 |
-
<button id="lcm" class="bg-purple-500 hover:bg-purple-600 text-white p-2 rounded calculator-btn">LCM</button>
|
| 122 |
-
<button id="abs" class="bg-purple-500 hover:bg-purple-600 text-white p-2 rounded calculator-btn">|x|</button>
|
| 123 |
-
<button id="exp" class="bg-purple-500 hover:bg-purple-600 text-white p-2 rounded calculator-btn">e^x</button>
|
| 124 |
-
<button id="mod" class="bg-purple-500 hover:bg-purple-600 text-white p-2 rounded calculator-btn">mod</button>
|
| 125 |
-
<button id="rand" class="bg-purple-500 hover:bg-purple-600 text-white p-2 rounded calculator-btn">Rand</button>
|
| 126 |
</div>
|
| 127 |
|
| 128 |
<div class="mb-4">
|
| 129 |
-
<
|
| 130 |
-
<
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
| 134 |
</div>
|
|
|
|
| 135 |
|
| 136 |
-
<div class="
|
| 137 |
-
<
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
| 142 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
</div>
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
<
|
| 150 |
</div>
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
</div>
|
| 153 |
</div>
|
| 154 |
</div>
|
| 155 |
|
| 156 |
<script>
|
| 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 |
-
// Clear history
|
| 204 |
-
clearHistoryBtn.addEventListener('click', clearHistory);
|
| 205 |
-
|
| 206 |
-
// Number buttons
|
| 207 |
-
numberButtons.forEach(button => {
|
| 208 |
-
button.addEventListener('click', () => {
|
| 209 |
-
appendNumber(button.getAttribute('data-value'));
|
| 210 |
-
});
|
| 211 |
-
});
|
| 212 |
-
|
| 213 |
-
// Operation buttons
|
| 214 |
-
addButton.addEventListener('click', () => setOperation('+'));
|
| 215 |
-
subtractButton.addEventListener('click', () => setOperation('-'));
|
| 216 |
-
multiplyButton.addEventListener('click', () => setOperation('×'));
|
| 217 |
-
divideButton.addEventListener('click', () => setOperation('÷'));
|
| 218 |
-
equalsButton.addEventListener('click', compute);
|
| 219 |
-
clearButton.addEventListener('click', clear);
|
| 220 |
-
backspaceButton.addEventListener('click', backspace);
|
| 221 |
-
decimalButton.addEventListener('click', appendDecimal);
|
| 222 |
-
percentageButton.addEventListener('click', percentage);
|
| 223 |
-
|
| 224 |
-
// Sidebar toggle
|
| 225 |
-
const sidebarToggle = document.getElementById('sidebar-toggle');
|
| 226 |
-
const closeSidebar = document.getElementById('close-sidebar');
|
| 227 |
-
const sidebar = document.getElementById('sidebar');
|
| 228 |
-
|
| 229 |
-
sidebarToggle.addEventListener('click', () => {
|
| 230 |
-
sidebar.classList.add('open');
|
| 231 |
});
|
| 232 |
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
});
|
| 236 |
-
|
| 237 |
-
// Scientific functions
|
| 238 |
-
sqrtButton.addEventListener('click', () => appendFunction('√('));
|
| 239 |
-
powerButton.addEventListener('click', () => setOperation('^'));
|
| 240 |
-
sinButton.addEventListener('click', () => applyFunction('sin'));
|
| 241 |
-
cosButton.addEventListener('click', () => applyFunction('cos'));
|
| 242 |
-
tanButton.addEventListener('click', () => applyFunction('tan'));
|
| 243 |
-
logButton.addEventListener('click', () => applyFunction('log'));
|
| 244 |
-
lnButton.addEventListener('click', () => applyFunction('ln'));
|
| 245 |
-
piButton.addEventListener('click', appendPi);
|
| 246 |
-
factorialButton.addEventListener('click', factorial);
|
| 247 |
-
|
| 248 |
-
// Keyboard support
|
| 249 |
-
document.addEventListener('keydown', handleKeyboardInput);
|
| 250 |
-
|
| 251 |
-
// Functions
|
| 252 |
-
function toggleTheme() {
|
| 253 |
-
document.body.classList.toggle('bg-gray-100');
|
| 254 |
-
document.body.classList.toggle('text-gray-900');
|
| 255 |
-
document.body.classList.toggle('bg-gray-900');
|
| 256 |
-
document.body.classList.toggle('text-white');
|
| 257 |
-
|
| 258 |
-
const calculator = document.querySelector('.bg-gray-800');
|
| 259 |
-
calculator.classList.toggle('bg-gray-800');
|
| 260 |
-
calculator.classList.toggle('bg-white');
|
| 261 |
-
|
| 262 |
-
const displayBg = document.querySelector('.bg-gray-700');
|
| 263 |
-
displayBg.classList.toggle('bg-gray-700');
|
| 264 |
-
displayBg.classList.toggle('bg-gray-100');
|
| 265 |
-
|
| 266 |
-
const buttons = document.querySelectorAll('.bg-gray-600, .bg-gray-700');
|
| 267 |
-
buttons.forEach(btn => {
|
| 268 |
-
btn.classList.toggle('bg-gray-600');
|
| 269 |
-
btn.classList.toggle('bg-gray-700');
|
| 270 |
-
btn.classList.toggle('bg-gray-200');
|
| 271 |
-
btn.classList.toggle('bg-gray-300');
|
| 272 |
-
btn.classList.toggle('text-white');
|
| 273 |
-
btn.classList.toggle('text-gray-900');
|
| 274 |
-
});
|
| 275 |
-
|
| 276 |
-
const icon = themeToggle.querySelector('i');
|
| 277 |
-
if (icon.classList.contains('fa-moon')) {
|
| 278 |
-
icon.classList.remove('fa-moon');
|
| 279 |
-
icon.classList.add('fa-sun');
|
| 280 |
-
} else {
|
| 281 |
-
icon.classList.remove('fa-sun');
|
| 282 |
-
icon.classList.add('fa-moon');
|
| 283 |
-
}
|
| 284 |
-
}
|
| 285 |
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
|
|
|
| 289 |
}
|
| 290 |
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
|
|
|
| 297 |
}
|
| 298 |
-
updateDisplay();
|
| 299 |
}
|
| 300 |
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
if (currentInput.indexOf('.') === -1) {
|
| 309 |
-
currentInput += '.';
|
| 310 |
-
}
|
| 311 |
-
updateDisplay();
|
| 312 |
-
}
|
| 313 |
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
updateDisplay();
|
| 322 |
}
|
| 323 |
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
return;
|
| 329 |
-
}
|
| 330 |
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
}
|
| 340 |
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
.replace(/×/g, '*')
|
| 346 |
-
.replace(/÷/g, '/')
|
| 347 |
-
.replace(/√\(/g, 'Math.sqrt(')
|
| 348 |
-
.replace(/∛\(/g, 'Math.cbrt(')
|
| 349 |
-
.replace(/\^/g, '**')
|
| 350 |
-
.replace(/mod/g, '%')
|
| 351 |
-
.replace(/abs\(/g, 'Math.abs(')
|
| 352 |
-
.replace(/exp\(/g, 'Math.exp(')
|
| 353 |
-
.replace(/log\(/g, 'Math.log10(')
|
| 354 |
-
.replace(/ln\(/g, 'Math.log(')
|
| 355 |
-
.replace(/sin\(/g, 'Math.sin(')
|
| 356 |
-
.replace(/cos\(/g, 'Math.cos(')
|
| 357 |
-
.replace(/tan\(/g, 'Math.tan(')
|
| 358 |
-
.replace(/π/g, 'Math.PI')
|
| 359 |
-
.replace(/φ/g, '((1 + Math.sqrt(5)) / 2)')
|
| 360 |
-
.replace(/e/g, 'Math.E');
|
| 361 |
-
|
| 362 |
-
// Handle factorial (must be done after other replacements)
|
| 363 |
-
expression = expression.replace(/(\d+)!/g, function(match, num) {
|
| 364 |
-
let n = parseInt(num);
|
| 365 |
-
if (n < 0) return 'NaN';
|
| 366 |
-
let result = 1;
|
| 367 |
-
for (let i = 2; i <= n; i++) result *= i;
|
| 368 |
-
return result;
|
| 369 |
-
});
|
| 370 |
-
|
| 371 |
-
// Handle gcd and lcm (simple implementations)
|
| 372 |
-
expression = expression.replace(/gcd\(([^,]+),([^)]+)\)/g, function(match, a, b) {
|
| 373 |
-
a = parseFloat(a);
|
| 374 |
-
b = parseFloat(b);
|
| 375 |
-
while (b) [a, b] = [b, a % b];
|
| 376 |
-
return a;
|
| 377 |
-
});
|
| 378 |
-
|
| 379 |
-
expression = expression.replace(/lcm\(([^,]+),([^)]+)\)/g, function(match, a, b) {
|
| 380 |
-
a = parseFloat(a);
|
| 381 |
-
b = parseFloat(b);
|
| 382 |
-
return (a * b) / gcd(a, b);
|
| 383 |
-
});
|
| 384 |
-
|
| 385 |
-
// Evaluate the expression
|
| 386 |
-
const computation = eval(expression);
|
| 387 |
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
computation = prev / current;
|
| 400 |
-
break;
|
| 401 |
-
case '^':
|
| 402 |
-
computation = Math.pow(prev, current);
|
| 403 |
-
break;
|
| 404 |
-
default:
|
| 405 |
-
return;
|
| 406 |
-
}
|
| 407 |
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
};
|
| 413 |
-
calculationHistory.unshift(historyEntry);
|
| 414 |
-
if (calculationHistory.length > 10) {
|
| 415 |
-
calculationHistory.pop();
|
| 416 |
-
}
|
| 417 |
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
} else {
|
| 437 |
-
currentInput = currentInput.slice(0, -1);
|
| 438 |
-
}
|
| 439 |
-
updateDisplay();
|
| 440 |
}
|
| 441 |
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
}
|
| 446 |
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
|
|
|
|
|
|
|
|
|
| 450 |
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 459 |
}
|
| 460 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
}
|
| 462 |
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
|
|
|
| 483 |
}
|
| 484 |
|
| 485 |
-
|
|
|
|
| 486 |
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
};
|
| 492 |
-
calculationHistory.unshift(historyEntry);
|
| 493 |
-
if (calculationHistory.length > 10) {
|
| 494 |
-
calculationHistory.pop();
|
| 495 |
-
}
|
| 496 |
|
| 497 |
-
|
| 498 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 499 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
document.getElementById('gcd').addEventListener('click', () => appendFunction('gcd('));
|
| 505 |
-
document.getElementById('lcm').addEventListener('click', () => appendFunction('lcm('));
|
| 506 |
-
document.getElementById('abs').addEventListener('click', () => appendFunction('abs('));
|
| 507 |
-
document.getElementById('exp').addEventListener('click', () => appendFunction('exp('));
|
| 508 |
-
document.getElementById('mod').addEventListener('click', () => appendFunction(' mod '));
|
| 509 |
-
document.getElementById('rand').addEventListener('click', randomNumber);
|
| 510 |
-
document.getElementById('open-paren').addEventListener('click', () => appendValue('('));
|
| 511 |
-
document.getElementById('close-paren').addEventListener('click', () => appendValue(')'));
|
| 512 |
-
document.getElementById('e').addEventListener('click', appendE);
|
| 513 |
-
document.getElementById('phi').addEventListener('click', appendPhi);
|
| 514 |
-
|
| 515 |
-
// New functions
|
| 516 |
-
function appendFunction(func) {
|
| 517 |
-
if (resetInput) {
|
| 518 |
-
currentInput = func;
|
| 519 |
-
resetInput = false;
|
| 520 |
-
} else {
|
| 521 |
-
currentInput += func;
|
| 522 |
-
}
|
| 523 |
-
updateDisplay();
|
| 524 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 525 |
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
resetInput = false;
|
| 530 |
-
} else {
|
| 531 |
-
currentInput += value;
|
| 532 |
}
|
| 533 |
-
updateDisplay();
|
| 534 |
-
}
|
| 535 |
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
} else {
|
| 540 |
-
currentInput = Math.E.toString();
|
| 541 |
-
resetInput = false;
|
| 542 |
-
}
|
| 543 |
-
updateDisplay();
|
| 544 |
-
}
|
| 545 |
|
| 546 |
-
|
| 547 |
-
const
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
|
|
|
|
|
|
|
|
|
| 556 |
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 564 |
}
|
| 565 |
-
updateDisplay();
|
| 566 |
-
}
|
| 567 |
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 579 |
}
|
| 580 |
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
};
|
| 588 |
-
calculationHistory.unshift(historyEntry);
|
| 589 |
-
if (calculationHistory.length > 10) {
|
| 590 |
-
calculationHistory.pop();
|
| 591 |
}
|
| 592 |
-
|
| 593 |
-
updateDisplay();
|
| 594 |
-
updateHistoryList();
|
| 595 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 596 |
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 604 |
}
|
| 605 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 606 |
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
while (b) [a, b] = [b, a % b];
|
| 612 |
-
return a;
|
| 613 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 614 |
|
| 615 |
-
|
| 616 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 617 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 618 |
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
resetInput = true;
|
| 633 |
-
updateDisplay();
|
| 634 |
-
});
|
| 635 |
-
|
| 636 |
-
historyList.appendChild(historyItem);
|
| 637 |
-
});
|
| 638 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
} else if (e.key === '(') {
|
| 656 |
-
appendValue('(');
|
| 657 |
-
} else if (e.key === ')') {
|
| 658 |
-
appendValue(')');
|
| 659 |
-
} else if (e.key === 'Enter' || e.key === '=') {
|
| 660 |
-
compute();
|
| 661 |
-
} else if (e.key === 'Escape') {
|
| 662 |
-
clear();
|
| 663 |
-
} else if (e.key === 'Backspace') {
|
| 664 |
-
backspace();
|
| 665 |
-
} else if (e.key === '%') {
|
| 666 |
-
percentage();
|
| 667 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 668 |
}
|
| 669 |
});
|
| 670 |
</script>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>NexusChat - Advanced AI Client</title>
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 9 |
+
<script>
|
| 10 |
+
tailwind.config = {
|
| 11 |
+
theme: {
|
| 12 |
+
extend: {
|
| 13 |
+
colors: {
|
| 14 |
+
primary: {
|
| 15 |
+
50: '#f0f9ff',
|
| 16 |
+
100: '#e0f2fe',
|
| 17 |
+
200: '#bae6fd',
|
| 18 |
+
300: '#7dd3fc',
|
| 19 |
+
400: '#38bdf8',
|
| 20 |
+
500: '#0ea5e9',
|
| 21 |
+
600: '#0284c7',
|
| 22 |
+
700: '#0369a1',
|
| 23 |
+
800: '#075985',
|
| 24 |
+
900: '#0c4a6e',
|
| 25 |
+
},
|
| 26 |
+
dark: {
|
| 27 |
+
800: '#1e293b',
|
| 28 |
+
900: '#0f172a',
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
</script>
|
| 35 |
<style>
|
| 36 |
+
/* Custom scrollbar */
|
| 37 |
+
::-webkit-scrollbar {
|
| 38 |
+
width: 6px;
|
| 39 |
}
|
| 40 |
+
::-webkit-scrollbar-track {
|
| 41 |
+
background: #f1f1f1;
|
| 42 |
}
|
| 43 |
+
::-webkit-scrollbar-thumb {
|
| 44 |
+
background: #888;
|
| 45 |
+
border-radius: 3px;
|
| 46 |
}
|
| 47 |
+
::-webkit-scrollbar-thumb:hover {
|
| 48 |
+
background: #555;
|
| 49 |
}
|
| 50 |
+
|
| 51 |
+
/* Dark mode scrollbar */
|
| 52 |
+
.dark ::-webkit-scrollbar-track {
|
| 53 |
+
background: #1e293b;
|
| 54 |
+
}
|
| 55 |
+
.dark ::-webkit-scrollbar-thumb {
|
| 56 |
+
background: #64748b;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
/* Chat message typing animation */
|
| 60 |
+
@keyframes typing {
|
| 61 |
+
from { width: 0 }
|
| 62 |
+
to { width: 100% }
|
| 63 |
}
|
| 64 |
+
|
| 65 |
+
.typing-animation {
|
| 66 |
+
overflow: hidden;
|
| 67 |
+
white-space: nowrap;
|
| 68 |
+
animation: typing 2s steps(40, end);
|
| 69 |
}
|
| 70 |
+
|
| 71 |
+
/* Smooth transitions */
|
| 72 |
+
.transition-all {
|
| 73 |
+
transition: all 0.3s ease;
|
| 74 |
}
|
| 75 |
</style>
|
| 76 |
</head>
|
| 77 |
+
<body class="bg-gray-50 dark:bg-dark-900 text-gray-800 dark:text-gray-200 min-h-screen">
|
| 78 |
+
<div class="flex h-screen overflow-hidden">
|
| 79 |
+
<!-- Sidebar -->
|
| 80 |
+
<div class="w-64 bg-white dark:bg-dark-800 border-r border-gray-200 dark:border-gray-700 flex flex-col">
|
| 81 |
+
<div class="p-4 border-b border-gray-200 dark:border-gray-700">
|
| 82 |
+
<button id="newChatBtn" class="w-full flex items-center justify-center gap-2 bg-primary-500 hover:bg-primary-600 text-white py-2 px-4 rounded-lg transition-all">
|
| 83 |
+
<i class="fas fa-plus"></i>
|
| 84 |
+
<span>New Chat</span>
|
| 85 |
+
</button>
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
+
<div class="flex-1 overflow-y-auto p-2" id="chatList">
|
| 89 |
+
<!-- Chat items will be added here dynamically -->
|
| 90 |
+
</div>
|
| 91 |
+
|
| 92 |
+
<div class="p-4 border-t border-gray-200 dark:border-gray-700">
|
| 93 |
+
<div class="flex items-center gap-3 mb-4 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 p-2 rounded-lg transition-all">
|
| 94 |
+
<div class="w-8 h-8 rounded-full bg-primary-500 flex items-center justify-center text-white">
|
| 95 |
+
<i class="fas fa-user"></i>
|
| 96 |
</div>
|
| 97 |
+
<span class="font-medium">User Account</span>
|
| 98 |
</div>
|
| 99 |
|
| 100 |
+
<div id="settingsBtn" class="flex items-center gap-3 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 p-2 rounded-lg transition-all">
|
| 101 |
+
<div class="w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center">
|
| 102 |
+
<i class="fas fa-cog"></i>
|
| 103 |
+
</div>
|
| 104 |
+
<span>Settings</span>
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
+
<!-- Main Content -->
|
| 110 |
+
<div class="flex-1 flex flex-col overflow-hidden">
|
| 111 |
+
<!-- Header -->
|
| 112 |
+
<header class="bg-white dark:bg-dark-800 border-b border-gray-200 dark:border-gray-700 p-4 flex items-center justify-between">
|
| 113 |
+
<div class="flex items-center gap-2">
|
| 114 |
+
<button id="sidebarToggle" class="md:hidden text-gray-500 dark:text-gray-400">
|
| 115 |
+
<i class="fas fa-bars text-xl"></i>
|
| 116 |
+
</button>
|
| 117 |
+
<h1 class="text-xl font-bold">NexusChat</h1>
|
| 118 |
</div>
|
| 119 |
|
| 120 |
+
<div class="flex items-center gap-4">
|
| 121 |
+
<div class="relative">
|
| 122 |
+
<button id="modelDropdownBtn" class="flex items-center gap-2 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 py-2 px-3 rounded-lg transition-all">
|
| 123 |
+
<span id="currentModel">GPT-4</span>
|
| 124 |
+
<i class="fas fa-chevron-down text-xs"></i>
|
| 125 |
+
</button>
|
| 126 |
+
|
| 127 |
+
<div id="modelDropdown" class="hidden absolute right-0 mt-2 w-48 bg-white dark:bg-dark-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-10">
|
| 128 |
+
<div class="p-2">
|
| 129 |
+
<div class="text-xs uppercase text-gray-500 dark:text-gray-400 px-2 py-1">Default Models</div>
|
| 130 |
+
<div class="model-option cursor-pointer px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" data-model="gpt-3.5">GPT-3.5</div>
|
| 131 |
+
<div class="model-option cursor-pointer px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" data-model="gpt-4">GPT-4</div>
|
| 132 |
+
|
| 133 |
+
<div class="text-xs uppercase text-gray-500 dark:text-gray-400 px-2 py-1 mt-2">Custom Models</div>
|
| 134 |
+
<div id="customModelsList">
|
| 135 |
+
<!-- Custom models will be added here -->
|
| 136 |
+
</div>
|
| 137 |
+
|
| 138 |
+
<div class="border-t border-gray-200 dark:border-gray-700 mt-2 pt-2">
|
| 139 |
+
<div id="addModelBtn" class="cursor-pointer px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded flex items-center gap-2 text-primary-500">
|
| 140 |
+
<i class="fas fa-plus"></i>
|
| 141 |
+
<span>Add Custom Model</span>
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
|
| 148 |
+
<button id="darkModeToggle" class="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200">
|
| 149 |
+
<i class="fas fa-moon dark:hidden"></i>
|
| 150 |
+
<i class="fas fa-sun hidden dark:block"></i>
|
| 151 |
</button>
|
| 152 |
+
</div>
|
| 153 |
+
</header>
|
| 154 |
+
|
| 155 |
+
<!-- Chat Area -->
|
| 156 |
+
<div class="flex-1 overflow-y-auto p-4 bg-gray-50 dark:bg-dark-900" id="chatArea">
|
| 157 |
+
<div class="max-w-3xl mx-auto">
|
| 158 |
+
<!-- Welcome message -->
|
| 159 |
+
<div id="welcomeMessage" class="text-center py-10">
|
| 160 |
+
<div class="w-16 h-16 mx-auto mb-4 bg-primary-500 rounded-full flex items-center justify-center text-white">
|
| 161 |
+
<i class="fas fa-robot text-2xl"></i>
|
| 162 |
+
</div>
|
| 163 |
+
<h2 class="text-2xl font-bold mb-2">Welcome to NexusChat</h2>
|
| 164 |
+
<p class="text-gray-600 dark:text-gray-400 mb-6">Start a new conversation or select one from your history</p>
|
| 165 |
+
<button id="quickStartBtn" class="bg-primary-500 hover:bg-primary-600 text-white py-2 px-6 rounded-lg transition-all">
|
| 166 |
+
Quick Start
|
| 167 |
+
</button>
|
| 168 |
+
</div>
|
| 169 |
+
|
| 170 |
+
<!-- Chat messages will be added here dynamically -->
|
| 171 |
</div>
|
| 172 |
</div>
|
| 173 |
|
| 174 |
+
<!-- Input Area -->
|
| 175 |
+
<div class="p-4 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-dark-800">
|
| 176 |
+
<div class="max-w-3xl mx-auto">
|
| 177 |
+
<form id="messageForm" class="relative">
|
| 178 |
+
<textarea id="messageInput" rows="1" class="w-full p-4 pr-16 bg-gray-100 dark:bg-gray-700 rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-primary-500" placeholder="Type your message here..."></textarea>
|
| 179 |
+
<button type="submit" class="absolute right-4 bottom-4 w-8 h-8 bg-primary-500 hover:bg-primary-600 text-white rounded-full flex items-center justify-center transition-all">
|
| 180 |
+
<i class="fas fa-paper-plane"></i>
|
| 181 |
+
</button>
|
| 182 |
+
</form>
|
| 183 |
+
|
| 184 |
+
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2 text-center">
|
| 185 |
+
NexusChat may produce inaccurate information about people, places, or facts.
|
| 186 |
+
</div>
|
| 187 |
+
</div>
|
| 188 |
+
</div>
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
|
| 192 |
+
<!-- Add Model Modal -->
|
| 193 |
+
<div id="addModelModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
| 194 |
+
<div class="bg-white dark:bg-dark-800 rounded-lg shadow-xl w-full max-w-md p-6">
|
| 195 |
+
<div class="flex justify-between items-center mb-4">
|
| 196 |
+
<h3 class="text-lg font-bold">Add Custom Model</h3>
|
| 197 |
+
<button id="closeModelModal" class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
|
| 198 |
+
<i class="fas fa-times"></i>
|
| 199 |
+
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
</div>
|
| 201 |
|
| 202 |
+
<form id="addModelForm">
|
| 203 |
+
<div class="mb-4">
|
| 204 |
+
<label for="modelName" class="block text-sm font-medium mb-1">Model Name</label>
|
| 205 |
+
<input type="text" id="modelName" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:ring-primary-500 focus:border-primary-500" placeholder="e.g. My Custom Model" required>
|
|
|
|
|
|
|
|
|
|
| 206 |
</div>
|
| 207 |
|
| 208 |
+
<div class="mb-4">
|
| 209 |
+
<label for="modelEndpoint" class="block text-sm font-medium mb-1">API Endpoint</label>
|
| 210 |
+
<input type="text" id="modelEndpoint" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:ring-primary-500 focus:border-primary-500" placeholder="https://api.example.com/v1/chat" required>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
</div>
|
| 212 |
|
| 213 |
<div class="mb-4">
|
| 214 |
+
<label for="modelKey" class="block text-sm font-medium mb-1">API Key (optional)</label>
|
| 215 |
+
<input type="password" id="modelKey" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:ring-primary-500 focus:border-primary-500" placeholder="sk-...">
|
| 216 |
+
</div>
|
| 217 |
+
|
| 218 |
+
<div class="mb-4 flex items-center">
|
| 219 |
+
<input type="checkbox" id="modelStreaming" class="w-4 h-4 text-primary-600 rounded border-gray-300 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700">
|
| 220 |
+
<label for="modelStreaming" class="ml-2 text-sm font-medium text-gray-700 dark:text-gray-300">Supports Streaming</label>
|
| 221 |
</div>
|
| 222 |
+
<input type="hidden" id="editModelId" value="">
|
| 223 |
|
| 224 |
+
<div class="flex justify-end gap-2">
|
| 225 |
+
<button type="button" id="cancelAddModel" class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
|
| 226 |
+
Cancel
|
| 227 |
+
</button>
|
| 228 |
+
<button type="submit" class="px-4 py-2 bg-primary-500 hover:bg-primary-600 text-white rounded transition-all">
|
| 229 |
+
Add Model
|
| 230 |
+
</button>
|
| 231 |
</div>
|
| 232 |
+
</form>
|
| 233 |
+
</div>
|
| 234 |
+
</div>
|
| 235 |
+
|
| 236 |
+
<!-- API Key Modal -->
|
| 237 |
+
<div id="apiKeyModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
| 238 |
+
<div class="bg-white dark:bg-dark-800 rounded-lg shadow-xl w-full max-w-md p-6">
|
| 239 |
+
<div class="flex justify-between items-center mb-4">
|
| 240 |
+
<h3 class="text-lg font-bold">OpenAI API Key Required</h3>
|
| 241 |
+
<button id="closeApiKeyModal" class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
|
| 242 |
+
<i class="fas fa-times"></i>
|
| 243 |
+
</button>
|
| 244 |
</div>
|
| 245 |
+
|
| 246 |
+
<p class="mb-4">To use OpenAI models, please enter your API key:</p>
|
| 247 |
+
|
| 248 |
+
<form id="apiKeyForm">
|
| 249 |
+
<div class="mb-4">
|
| 250 |
+
<input type="password" id="apiKeyInput" class="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 focus:ring-primary-500 focus:border-primary-500" placeholder="sk-..." required>
|
| 251 |
</div>
|
| 252 |
+
|
| 253 |
+
<div class="flex justify-end gap-2">
|
| 254 |
+
<button type="button" id="cancelApiKey" class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
|
| 255 |
+
Cancel
|
| 256 |
+
</button>
|
| 257 |
+
<button type="submit" class="px-4 py-2 bg-primary-500 hover:bg-primary-600 text-white rounded transition-all">
|
| 258 |
+
Save Key
|
| 259 |
+
</button>
|
| 260 |
+
</div>
|
| 261 |
+
</form>
|
| 262 |
+
</div>
|
| 263 |
+
</div>
|
| 264 |
+
|
| 265 |
+
<!-- Delete Chat Confirmation Modal -->
|
| 266 |
+
<div id="deleteChatModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
| 267 |
+
<div class="bg-white dark:bg-dark-800 rounded-lg shadow-xl w-full max-w-md p-6">
|
| 268 |
+
<div class="flex justify-between items-center mb-4">
|
| 269 |
+
<h3 class="text-lg font-bold">Delete Chat</h3>
|
| 270 |
+
<button id="closeDeleteModal" class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
|
| 271 |
+
<i class="fas fa-times"></i>
|
| 272 |
+
</button>
|
| 273 |
+
</div>
|
| 274 |
+
|
| 275 |
+
<p class="mb-6">Are you sure you want to delete this chat? This action cannot be undone.</p>
|
| 276 |
+
|
| 277 |
+
<div class="flex justify-end gap-2">
|
| 278 |
+
<button type="button" id="cancelDelete" class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
|
| 279 |
+
Cancel
|
| 280 |
+
</button>
|
| 281 |
+
<button type="button" id="confirmDelete" class="px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded transition-all">
|
| 282 |
+
Delete
|
| 283 |
+
</button>
|
| 284 |
</div>
|
| 285 |
</div>
|
| 286 |
</div>
|
| 287 |
|
| 288 |
<script>
|
| 289 |
+
// DOM Elements
|
| 290 |
+
const apiKeyModal = document.getElementById('apiKeyModal');
|
| 291 |
+
const closeApiKeyModal = document.getElementById('closeApiKeyModal');
|
| 292 |
+
const cancelApiKey = document.getElementById('cancelApiKey');
|
| 293 |
+
const apiKeyForm = document.getElementById('apiKeyForm');
|
| 294 |
+
const apiKeyInput = document.getElementById('apiKeyInput');
|
| 295 |
+
const sidebarToggle = document.getElementById('sidebarToggle');
|
| 296 |
+
const darkModeToggle = document.getElementById('darkModeToggle');
|
| 297 |
+
const newChatBtn = document.getElementById('newChatBtn');
|
| 298 |
+
const chatList = document.getElementById('chatList');
|
| 299 |
+
const chatArea = document.getElementById('chatArea');
|
| 300 |
+
const welcomeMessage = document.getElementById('welcomeMessage');
|
| 301 |
+
const messageForm = document.getElementById('messageForm');
|
| 302 |
+
const messageInput = document.getElementById('messageInput');
|
| 303 |
+
const quickStartBtn = document.getElementById('quickStartBtn');
|
| 304 |
+
const modelDropdownBtn = document.getElementById('modelDropdownBtn');
|
| 305 |
+
const modelDropdown = document.getElementById('modelDropdown');
|
| 306 |
+
const currentModel = document.getElementById('currentModel');
|
| 307 |
+
const customModelsList = document.getElementById('customModelsList');
|
| 308 |
+
const addModelBtn = document.getElementById('addModelBtn');
|
| 309 |
+
const addModelModal = document.getElementById('addModelModal');
|
| 310 |
+
const closeModelModal = document.getElementById('closeModelModal');
|
| 311 |
+
const cancelAddModel = document.getElementById('cancelAddModel');
|
| 312 |
+
const addModelForm = document.getElementById('addModelForm');
|
| 313 |
+
const deleteChatModal = document.getElementById('deleteChatModal');
|
| 314 |
+
const closeDeleteModal = document.getElementById('closeDeleteModal');
|
| 315 |
+
const cancelDelete = document.getElementById('cancelDelete');
|
| 316 |
+
const confirmDelete = document.getElementById('confirmDelete');
|
| 317 |
+
|
| 318 |
+
// State
|
| 319 |
+
let chats = JSON.parse(localStorage.getItem('chats')) || [];
|
| 320 |
+
let currentChatId = null;
|
| 321 |
+
let models = [
|
| 322 |
+
{ name: 'GPT-3.5', id: 'gpt-3.5', type: 'default' },
|
| 323 |
+
{ name: 'GPT-4', id: 'gpt-4', type: 'default' }
|
| 324 |
+
];
|
| 325 |
+
let customModels = JSON.parse(localStorage.getItem('customModels')) || [];
|
| 326 |
+
let selectedModel = 'gpt-4';
|
| 327 |
+
let chatToDelete = null;
|
| 328 |
+
|
| 329 |
+
// Initialize
|
| 330 |
+
function init() {
|
| 331 |
+
// Load custom models
|
| 332 |
+
customModels.forEach(model => {
|
| 333 |
+
models.push({ ...model, type: 'custom' });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
});
|
| 335 |
|
| 336 |
+
// Render chat list
|
| 337 |
+
renderChatList();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
|
| 339 |
+
// Set dark mode if preferred
|
| 340 |
+
if (localStorage.getItem('darkMode') === 'true' ||
|
| 341 |
+
(!localStorage.getItem('darkMode') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
| 342 |
+
document.documentElement.classList.add('dark');
|
| 343 |
}
|
| 344 |
|
| 345 |
+
// Set current model
|
| 346 |
+
const savedModel = localStorage.getItem('selectedModel');
|
| 347 |
+
if (savedModel) {
|
| 348 |
+
const model = models.find(m => m.id === savedModel);
|
| 349 |
+
if (model) {
|
| 350 |
+
selectedModel = savedModel;
|
| 351 |
+
currentModel.textContent = model.name;
|
| 352 |
}
|
|
|
|
| 353 |
}
|
| 354 |
|
| 355 |
+
// Render custom models in dropdown
|
| 356 |
+
renderCustomModels();
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
// Render chat list
|
| 360 |
+
function renderChatList() {
|
| 361 |
+
chatList.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
|
| 363 |
+
if (chats.length === 0) {
|
| 364 |
+
chatList.innerHTML = `
|
| 365 |
+
<div class="p-4 text-center text-gray-500 dark:text-gray-400">
|
| 366 |
+
No chats yet
|
| 367 |
+
</div>
|
| 368 |
+
`;
|
| 369 |
+
return;
|
|
|
|
| 370 |
}
|
| 371 |
|
| 372 |
+
chats.forEach(chat => {
|
| 373 |
+
const chatElement = document.createElement('div');
|
| 374 |
+
chatElement.className = `group relative flex items-center gap-3 p-2 rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 transition-all ${currentChatId === chat.id ? 'bg-gray-100 dark:bg-gray-700' : ''}`;
|
| 375 |
+
chatElement.dataset.id = chat.id;
|
|
|
|
|
|
|
| 376 |
|
| 377 |
+
chatElement.innerHTML = `
|
| 378 |
+
<div class="w-8 h-8 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-500 dark:text-primary-300 flex items-center justify-center">
|
| 379 |
+
<i class="fas fa-comment"></i>
|
| 380 |
+
</div>
|
| 381 |
+
<div class="flex-1 min-w-0">
|
| 382 |
+
<p class="truncate font-medium">${chat.title || 'New Chat'}</p>
|
| 383 |
+
<p class="text-xs text-gray-500 dark:text-gray-400 truncate">${chat.lastMessage || ''}</p>
|
| 384 |
+
</div>
|
| 385 |
+
<button class="delete-chat opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-500 transition-all p-1">
|
| 386 |
+
<i class="fas fa-trash text-sm"></i>
|
| 387 |
+
</button>
|
| 388 |
+
`;
|
| 389 |
+
|
| 390 |
+
chatElement.addEventListener('click', () => loadChat(chat.id));
|
| 391 |
|
| 392 |
+
const deleteBtn = chatElement.querySelector('.delete-chat');
|
| 393 |
+
deleteBtn.addEventListener('click', (e) => {
|
| 394 |
+
e.stopPropagation();
|
| 395 |
+
showDeleteModal(chat.id);
|
| 396 |
+
});
|
| 397 |
+
|
| 398 |
+
chatList.appendChild(chatElement);
|
| 399 |
+
});
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
// Render custom models in dropdown
|
| 403 |
+
function renderCustomModels() {
|
| 404 |
+
customModelsList.innerHTML = '';
|
| 405 |
+
|
| 406 |
+
if (customModels.length === 0) {
|
| 407 |
+
customModelsList.innerHTML = `
|
| 408 |
+
<div class="text-center text-gray-500 dark:text-gray-400 py-2 text-sm">
|
| 409 |
+
No custom models added
|
| 410 |
+
</div>
|
| 411 |
+
`;
|
| 412 |
+
return;
|
| 413 |
}
|
| 414 |
|
| 415 |
+
customModels.forEach(model => {
|
| 416 |
+
const modelElement = document.createElement('div');
|
| 417 |
+
modelElement.className = 'model-option cursor-pointer px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded flex justify-between items-center';
|
| 418 |
+
modelElement.dataset.model = model.id;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
|
| 420 |
+
modelElement.innerHTML = `
|
| 421 |
+
<span>${model.name}</span>
|
| 422 |
+
<div class="flex gap-1">
|
| 423 |
+
<button class="edit-model text-gray-400 hover:text-blue-500 transition-all p-1">
|
| 424 |
+
<i class="fas fa-edit text-xs"></i>
|
| 425 |
+
</button>
|
| 426 |
+
<button class="delete-model text-gray-400 hover:text-red-500 transition-all p-1">
|
| 427 |
+
<i class="fas fa-trash text-xs"></i>
|
| 428 |
+
</button>
|
| 429 |
+
</div>
|
| 430 |
+
`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
|
| 432 |
+
modelElement.addEventListener('click', (e) => {
|
| 433 |
+
if (!e.target.classList.contains('delete-model')) {
|
| 434 |
+
selectModel(model.id);
|
| 435 |
+
}
|
| 436 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
|
| 438 |
+
const deleteBtn = modelElement.querySelector('.delete-model');
|
| 439 |
+
deleteBtn.addEventListener('click', (e) => {
|
| 440 |
+
e.stopPropagation();
|
| 441 |
+
deleteCustomModel(model.id);
|
| 442 |
+
});
|
| 443 |
+
|
| 444 |
+
const editBtn = modelElement.querySelector('.edit-model');
|
| 445 |
+
editBtn.addEventListener('click', (e) => {
|
| 446 |
+
e.stopPropagation();
|
| 447 |
+
showEditModelModal(model);
|
| 448 |
+
});
|
| 449 |
|
| 450 |
+
customModelsList.appendChild(modelElement);
|
| 451 |
+
});
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
// Create new chat
|
| 455 |
+
function createNewChat() {
|
| 456 |
+
const newChat = {
|
| 457 |
+
id: Date.now().toString(),
|
| 458 |
+
title: 'New Chat',
|
| 459 |
+
model: selectedModel,
|
| 460 |
+
messages: [],
|
| 461 |
+
lastMessage: '',
|
| 462 |
+
createdAt: new Date().toISOString()
|
| 463 |
+
};
|
| 464 |
|
| 465 |
+
chats.unshift(newChat);
|
| 466 |
+
saveChats();
|
| 467 |
+
loadChat(newChat.id);
|
| 468 |
+
renderChatList();
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
// Load chat
|
| 472 |
+
function loadChat(chatId) {
|
| 473 |
+
currentChatId = chatId;
|
| 474 |
+
const chat = chats.find(c => c.id === chatId);
|
| 475 |
|
| 476 |
+
if (!chat) {
|
| 477 |
+
createNewChat();
|
| 478 |
+
return;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
}
|
| 480 |
|
| 481 |
+
// Update UI
|
| 482 |
+
document.querySelectorAll('[data-id]').forEach(el => {
|
| 483 |
+
el.classList.toggle('bg-gray-100', el.dataset.id === chatId);
|
| 484 |
+
el.classList.toggle('dark:bg-gray-700', el.dataset.id === chatId);
|
| 485 |
+
});
|
| 486 |
+
|
| 487 |
+
// Render messages
|
| 488 |
+
renderMessages(chat.messages);
|
| 489 |
+
|
| 490 |
+
// Hide welcome message
|
| 491 |
+
welcomeMessage.classList.add('hidden');
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
// Render messages
|
| 495 |
+
function renderMessages(messages) {
|
| 496 |
+
chatArea.innerHTML = '';
|
| 497 |
+
|
| 498 |
+
if (messages.length === 0) {
|
| 499 |
+
welcomeMessage.classList.remove('hidden');
|
| 500 |
+
return;
|
| 501 |
}
|
| 502 |
|
| 503 |
+
const messagesContainer = document.createElement('div');
|
| 504 |
+
messagesContainer.className = 'space-y-6';
|
| 505 |
+
|
| 506 |
+
messages.forEach((msg, index) => {
|
| 507 |
+
const messageElement = document.createElement('div');
|
| 508 |
+
messageElement.className = `flex gap-4 ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`;
|
| 509 |
|
| 510 |
+
if (msg.role === 'assistant') {
|
| 511 |
+
messageElement.innerHTML = `
|
| 512 |
+
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-primary-500 text-white flex items-center justify-center">
|
| 513 |
+
<i class="fas fa-robot"></i>
|
| 514 |
+
</div>
|
| 515 |
+
<div class="max-w-[80%] bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm relative group">
|
| 516 |
+
<div class="prose dark:prose-invert">${msg.content}</div>
|
| 517 |
+
<div class="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
|
| 518 |
+
<button class="message-action bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 p-1 rounded text-gray-700 dark:text-gray-300" data-action="regenerate" title="Regenerate">
|
| 519 |
+
<i class="fas fa-sync-alt text-xs"></i>
|
| 520 |
+
</button>
|
| 521 |
+
<button class="message-action bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 p-1 rounded text-gray-700 dark:text-gray-300" data-action="edit" title="Edit">
|
| 522 |
+
<i class="fas fa-edit text-xs"></i>
|
| 523 |
+
</button>
|
| 524 |
+
</div>
|
| 525 |
+
</div>
|
| 526 |
+
`;
|
| 527 |
+
} else {
|
| 528 |
+
messageElement.innerHTML = `
|
| 529 |
+
<div class="max-w-[80%] bg-primary-500 text-white rounded-lg p-4 shadow-sm relative group">
|
| 530 |
+
<div class="prose prose-white">${msg.content}</div>
|
| 531 |
+
<div class="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
| 532 |
+
<button class="message-action bg-primary-600 hover:bg-primary-700 p-1 rounded text-white" data-action="edit" title="Edit">
|
| 533 |
+
<i class="fas fa-edit text-xs"></i>
|
| 534 |
+
</button>
|
| 535 |
+
</div>
|
| 536 |
+
</div>
|
| 537 |
+
`;
|
| 538 |
}
|
| 539 |
+
|
| 540 |
+
messagesContainer.appendChild(messageElement);
|
| 541 |
+
});
|
| 542 |
+
|
| 543 |
+
chatArea.appendChild(messagesContainer);
|
| 544 |
+
chatArea.scrollTop = chatArea.scrollHeight;
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
// Send message
|
| 548 |
+
async function sendMessage() {
|
| 549 |
+
const message = messageInput.value.trim();
|
| 550 |
+
if (!message || !currentChatId) return;
|
| 551 |
+
|
| 552 |
+
// Get current chat
|
| 553 |
+
const chatIndex = chats.findIndex(c => c.id === currentChatId);
|
| 554 |
+
if (chatIndex === -1) return;
|
| 555 |
+
|
| 556 |
+
const chat = chats[chatIndex];
|
| 557 |
+
const model = models.find(m => m.id === chat.model);
|
| 558 |
+
if (!model) return;
|
| 559 |
+
|
| 560 |
+
if (model.type === 'default' && !localStorage.getItem('openaiApiKey')) {
|
| 561 |
+
showApiKeyModal();
|
| 562 |
+
return;
|
| 563 |
}
|
| 564 |
|
| 565 |
+
// Add user message to chat
|
| 566 |
+
const userMessage = {
|
| 567 |
+
role: 'user',
|
| 568 |
+
content: message,
|
| 569 |
+
timestamp: new Date().toISOString()
|
| 570 |
+
};
|
| 571 |
+
|
| 572 |
+
chat.messages.push(userMessage);
|
| 573 |
+
chat.lastMessage = message.length > 30 ? message.substring(0, 30) + '...' : message;
|
| 574 |
+
|
| 575 |
+
// Update UI
|
| 576 |
+
renderMessages(chat.messages);
|
| 577 |
+
messageInput.value = '';
|
| 578 |
+
|
| 579 |
+
// Add loading indicator for assistant response
|
| 580 |
+
const loadingElement = document.createElement('div');
|
| 581 |
+
loadingElement.className = 'flex gap-4 justify-start';
|
| 582 |
+
loadingElement.innerHTML = `
|
| 583 |
+
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-primary-500 text-white flex items-center justify-center">
|
| 584 |
+
<i class="fas fa-robot"></i>
|
| 585 |
+
</div>
|
| 586 |
+
<div class="max-w-[80%] bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm w-full">
|
| 587 |
+
<div class="flex gap-2">
|
| 588 |
+
<div class="w-2 h-2 rounded-full bg-gray-300 animate-pulse"></div>
|
| 589 |
+
<div class="w-2 h-2 rounded-full bg-gray-300 animate-pulse delay-75"></div>
|
| 590 |
+
<div class="w-2 h-2 rounded-full bg-gray-300 animate-pulse delay-150"></div>
|
| 591 |
+
</div>
|
| 592 |
+
</div>
|
| 593 |
+
`;
|
| 594 |
+
|
| 595 |
+
chatArea.appendChild(loadingElement);
|
| 596 |
+
chatArea.scrollTop = chatArea.scrollHeight;
|
| 597 |
+
|
| 598 |
+
try {
|
| 599 |
+
// Ensure we have the latest chat reference first
|
| 600 |
+
const chatIndex = chats.findIndex(c => c.id === currentChatId);
|
| 601 |
+
if (chatIndex === -1) throw new Error('Chat not found');
|
| 602 |
+
const currentChat = chats[chatIndex];
|
| 603 |
|
| 604 |
+
// Get the current model
|
| 605 |
+
const model = models.find(m => m.id === currentChat.model);
|
| 606 |
+
if (!model) throw new Error('Model not found');
|
| 607 |
+
|
| 608 |
+
// Prepare messages for API
|
| 609 |
+
const messages = currentChat.messages.map(msg => ({
|
| 610 |
+
role: msg.role,
|
| 611 |
+
content: msg.content
|
| 612 |
+
}));
|
| 613 |
+
|
| 614 |
+
let response;
|
| 615 |
+
if (model.type === 'default') {
|
| 616 |
+
// Call OpenAI API
|
| 617 |
+
response = await callOpenAIAPI(messages, model.id);
|
| 618 |
+
} else {
|
| 619 |
+
// Call custom model API
|
| 620 |
+
response = await callCustomModelAPI(messages, model);
|
| 621 |
}
|
| 622 |
|
| 623 |
+
// Remove loading indicator
|
| 624 |
+
loadingElement.remove();
|
| 625 |
|
| 626 |
+
const assistantMessage = {
|
| 627 |
+
role: 'assistant',
|
| 628 |
+
content: response,
|
| 629 |
+
timestamp: new Date().toISOString()
|
| 630 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
| 631 |
|
| 632 |
+
currentChat.messages.push(assistantMessage);
|
| 633 |
+
saveChats();
|
| 634 |
+
renderMessages(chat.messages);
|
| 635 |
+
|
| 636 |
+
// Update chat list
|
| 637 |
+
renderChatList();
|
| 638 |
+
} catch (error) {
|
| 639 |
+
console.error('API Error:', error);
|
| 640 |
+
loadingElement.remove();
|
| 641 |
+
|
| 642 |
+
// Show error message
|
| 643 |
+
const errorElement = document.createElement('div');
|
| 644 |
+
errorElement.className = 'flex gap-4 justify-start';
|
| 645 |
+
errorElement.innerHTML = `
|
| 646 |
+
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-red-500 text-white flex items-center justify-center">
|
| 647 |
+
<i class="fas fa-exclamation-triangle"></i>
|
| 648 |
+
</div>
|
| 649 |
+
<div class="max-w-[80%] bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm">
|
| 650 |
+
<div class="text-red-500">Error: ${error.message}</div>
|
| 651 |
+
</div>
|
| 652 |
+
`;
|
| 653 |
+
|
| 654 |
+
chatArea.appendChild(errorElement);
|
| 655 |
+
chatArea.scrollTop = chatArea.scrollHeight;
|
| 656 |
}
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
// Call OpenAI API
|
| 660 |
+
async function callOpenAIAPI(messages, model, apiKey) {
|
| 661 |
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
| 662 |
+
method: 'POST',
|
| 663 |
+
headers: {
|
| 664 |
+
'Content-Type': 'application/json',
|
| 665 |
+
'Authorization': `Bearer ${apiKey}`
|
| 666 |
+
},
|
| 667 |
+
body: JSON.stringify({
|
| 668 |
+
model: model === 'gpt-3.5' ? 'gpt-3.5-turbo' : 'gpt-4',
|
| 669 |
+
messages,
|
| 670 |
+
temperature: 0.7
|
| 671 |
+
})
|
| 672 |
+
});
|
| 673 |
|
| 674 |
+
if (!response.ok) {
|
| 675 |
+
const errorData = await response.json();
|
| 676 |
+
throw new Error(errorData.error?.message || 'Failed to call OpenAI API');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 677 |
}
|
| 678 |
+
|
| 679 |
+
const data = await response.json();
|
| 680 |
+
return data.choices[0]?.message?.content || 'No response from model';
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
// Call custom model API
|
| 684 |
+
async function callCustomModelAPI(messages, model) {
|
| 685 |
+
if (model.streaming) {
|
| 686 |
+
// Handle streaming response
|
| 687 |
+
const response = await fetch(model.endpoint, {
|
| 688 |
+
method: 'POST',
|
| 689 |
+
headers: {
|
| 690 |
+
'Content-Type': 'application/json',
|
| 691 |
+
...(model.key ? { 'Authorization': `Bearer ${model.key}` } : {})
|
| 692 |
+
},
|
| 693 |
+
body: JSON.stringify({
|
| 694 |
+
model: model.id,
|
| 695 |
+
messages,
|
| 696 |
+
temperature: 0.7,
|
| 697 |
+
stream: true
|
| 698 |
+
})
|
| 699 |
+
});
|
| 700 |
|
| 701 |
+
if (!response.ok) {
|
| 702 |
+
const errorText = await response.text();
|
| 703 |
+
throw new Error(errorText || 'Failed to call custom model API');
|
|
|
|
|
|
|
|
|
|
| 704 |
}
|
|
|
|
|
|
|
| 705 |
|
| 706 |
+
const reader = response.body.getReader();
|
| 707 |
+
const decoder = new TextDecoder();
|
| 708 |
+
let result = '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 709 |
|
| 710 |
+
// Create message element for streaming
|
| 711 |
+
const messageElement = document.createElement('div');
|
| 712 |
+
messageElement.className = 'flex gap-4 justify-start';
|
| 713 |
+
messageElement.innerHTML = `
|
| 714 |
+
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-primary-500 text-white flex items-center justify-center">
|
| 715 |
+
<i class="fas fa-robot"></i>
|
| 716 |
+
</div>
|
| 717 |
+
<div class="max-w-[80%] bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm relative group">
|
| 718 |
+
<div class="prose dark:prose-invert" id="streaming-content"></div>
|
| 719 |
+
</div>
|
| 720 |
+
`;
|
| 721 |
+
chatArea.appendChild(messageElement);
|
| 722 |
+
const contentElement = messageElement.querySelector('#streaming-content');
|
| 723 |
|
| 724 |
+
try {
|
| 725 |
+
while (true) {
|
| 726 |
+
const { done, value } = await reader.read();
|
| 727 |
+
if (done) break;
|
| 728 |
+
|
| 729 |
+
const chunk = decoder.decode(value);
|
| 730 |
+
const lines = chunk.split('\n').filter(line => line.trim() !== '');
|
| 731 |
+
|
| 732 |
+
for (const line of lines) {
|
| 733 |
+
if (line.startsWith('data: ')) {
|
| 734 |
+
const data = line.substring(6);
|
| 735 |
+
if (data === '[DONE]') continue;
|
| 736 |
+
|
| 737 |
+
try {
|
| 738 |
+
const parsed = JSON.parse(data);
|
| 739 |
+
const content = parsed.choices?.[0]?.delta?.content || '';
|
| 740 |
+
result += content;
|
| 741 |
+
contentElement.innerHTML = result;
|
| 742 |
+
chatArea.scrollTop = chatArea.scrollHeight;
|
| 743 |
+
} catch (e) {
|
| 744 |
+
console.error('Error parsing stream data:', e);
|
| 745 |
+
}
|
| 746 |
+
}
|
| 747 |
+
}
|
| 748 |
+
}
|
| 749 |
+
} catch (e) {
|
| 750 |
+
console.error('Stream error:', e);
|
| 751 |
+
throw e;
|
| 752 |
}
|
|
|
|
|
|
|
| 753 |
|
| 754 |
+
return result;
|
| 755 |
+
} else {
|
| 756 |
+
// Handle regular response
|
| 757 |
+
const response = await fetch(model.endpoint, {
|
| 758 |
+
method: 'POST',
|
| 759 |
+
headers: {
|
| 760 |
+
'Content-Type': 'application/json',
|
| 761 |
+
...(model.key ? { 'Authorization': `Bearer ${model.key}` } : {})
|
| 762 |
+
},
|
| 763 |
+
body: JSON.stringify({
|
| 764 |
+
model: model.id,
|
| 765 |
+
messages,
|
| 766 |
+
temperature: 0.7,
|
| 767 |
+
stream: false
|
| 768 |
+
})
|
| 769 |
+
});
|
| 770 |
|
| 771 |
+
if (!response.ok) {
|
| 772 |
+
try {
|
| 773 |
+
const errorData = await response.json();
|
| 774 |
+
throw new Error(errorData.error?.message || 'Failed to call custom model API');
|
| 775 |
+
} catch (e) {
|
| 776 |
+
const errorText = await response.text();
|
| 777 |
+
throw new Error(errorText || 'Failed to call custom model API');
|
| 778 |
+
}
|
| 779 |
}
|
| 780 |
|
| 781 |
+
try {
|
| 782 |
+
const data = await response.json();
|
| 783 |
+
return data.choices[0]?.message?.content || data.response || 'No response from model';
|
| 784 |
+
} catch (e) {
|
| 785 |
+
const text = await response.text();
|
| 786 |
+
return text || 'No response from model';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 787 |
}
|
|
|
|
|
|
|
|
|
|
| 788 |
}
|
| 789 |
+
}
|
| 790 |
+
|
| 791 |
+
// Select model
|
| 792 |
+
function selectModel(modelId) {
|
| 793 |
+
const model = models.find(m => m.id === modelId);
|
| 794 |
+
if (!model) return;
|
| 795 |
|
| 796 |
+
selectedModel = modelId;
|
| 797 |
+
currentModel.textContent = model.name;
|
| 798 |
+
modelDropdown.classList.add('hidden');
|
| 799 |
+
|
| 800 |
+
// Save to localStorage
|
| 801 |
+
localStorage.setItem('selectedModel', modelId);
|
| 802 |
+
|
| 803 |
+
// Update current chat's model if one is active
|
| 804 |
+
if (currentChatId) {
|
| 805 |
+
const chat = chats.find(c => c.id === currentChatId);
|
| 806 |
+
if (chat) {
|
| 807 |
+
chat.model = modelId;
|
| 808 |
+
saveChats();
|
| 809 |
}
|
| 810 |
}
|
| 811 |
+
}
|
| 812 |
+
|
| 813 |
+
// Add custom model
|
| 814 |
+
function addCustomModel(name, endpoint, key, streaming) {
|
| 815 |
+
const newModel = {
|
| 816 |
+
id: `custom-${Date.now()}`,
|
| 817 |
+
name,
|
| 818 |
+
endpoint,
|
| 819 |
+
key,
|
| 820 |
+
streaming: streaming || false,
|
| 821 |
+
type: 'custom'
|
| 822 |
+
};
|
| 823 |
+
|
| 824 |
+
customModels.push(newModel);
|
| 825 |
+
models.push(newModel);
|
| 826 |
+
|
| 827 |
+
// Save to localStorage
|
| 828 |
+
localStorage.setItem('customModels', JSON.stringify(customModels));
|
| 829 |
+
|
| 830 |
+
// Update UI
|
| 831 |
+
renderCustomModels();
|
| 832 |
+
hideAddModelModal();
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
// Edit custom model
|
| 836 |
+
function editCustomModel(modelId, name, endpoint, key, streaming) {
|
| 837 |
+
const modelIndex = customModels.findIndex(m => m.id === modelId);
|
| 838 |
+
if (modelIndex === -1) return;
|
| 839 |
+
|
| 840 |
+
customModels[modelIndex] = {
|
| 841 |
+
...customModels[modelIndex],
|
| 842 |
+
name,
|
| 843 |
+
endpoint,
|
| 844 |
+
key,
|
| 845 |
+
streaming: streaming || false
|
| 846 |
+
};
|
| 847 |
+
|
| 848 |
+
// Update in models array
|
| 849 |
+
const globalModelIndex = models.findIndex(m => m.id === modelId);
|
| 850 |
+
if (globalModelIndex !== -1) {
|
| 851 |
+
models[globalModelIndex] = {
|
| 852 |
+
...models[globalModelIndex],
|
| 853 |
+
name,
|
| 854 |
+
endpoint,
|
| 855 |
+
key
|
| 856 |
+
};
|
| 857 |
+
}
|
| 858 |
+
|
| 859 |
+
// Save to localStorage
|
| 860 |
+
localStorage.setItem('customModels', JSON.stringify(customModels));
|
| 861 |
+
|
| 862 |
+
// Update UI
|
| 863 |
+
renderCustomModels();
|
| 864 |
+
hideAddModelModal();
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
// Delete custom model
|
| 868 |
+
function deleteCustomModel(modelId) {
|
| 869 |
+
customModels = customModels.filter(m => m.id !== modelId);
|
| 870 |
+
models = models.filter(m => m.id !== modelId);
|
| 871 |
+
|
| 872 |
+
// Save to localStorage
|
| 873 |
+
localStorage.setItem('customModels', JSON.stringify(customModels));
|
| 874 |
+
|
| 875 |
+
// Update UI
|
| 876 |
+
renderCustomModels();
|
| 877 |
+
|
| 878 |
+
// If the deleted model was selected, switch to default
|
| 879 |
+
if (selectedModel === modelId) {
|
| 880 |
+
selectModel('gpt-4');
|
| 881 |
+
}
|
| 882 |
+
}
|
| 883 |
+
|
| 884 |
+
// Delete chat
|
| 885 |
+
function deleteChat(chatId) {
|
| 886 |
+
chats = chats.filter(c => c.id !== chatId);
|
| 887 |
+
saveChats();
|
| 888 |
|
| 889 |
+
if (currentChatId === chatId) {
|
| 890 |
+
currentChatId = null;
|
| 891 |
+
welcomeMessage.classList.remove('hidden');
|
| 892 |
+
chatArea.innerHTML = '';
|
|
|
|
|
|
|
| 893 |
}
|
| 894 |
+
|
| 895 |
+
renderChatList();
|
| 896 |
+
hideDeleteModal();
|
| 897 |
+
}
|
| 898 |
+
|
| 899 |
+
// Show delete modal
|
| 900 |
+
function showDeleteModal(chatId) {
|
| 901 |
+
chatToDelete = chatId;
|
| 902 |
+
deleteChatModal.classList.remove('hidden');
|
| 903 |
+
}
|
| 904 |
+
|
| 905 |
+
// Hide delete modal
|
| 906 |
+
function hideDeleteModal() {
|
| 907 |
+
deleteChatModal.classList.add('hidden');
|
| 908 |
+
chatToDelete = null;
|
| 909 |
+
}
|
| 910 |
+
|
| 911 |
+
// Show add model modal
|
| 912 |
+
function showAddModelModal() {
|
| 913 |
+
addModelModal.classList.remove('hidden');
|
| 914 |
+
}
|
| 915 |
+
|
| 916 |
+
// Show edit model modal
|
| 917 |
+
function showEditModelModal(model) {
|
| 918 |
+
document.getElementById('modelName').value = model.name;
|
| 919 |
+
document.getElementById('modelEndpoint').value = model.endpoint;
|
| 920 |
+
document.getElementById('modelKey').value = model.key || '';
|
| 921 |
+
document.getElementById('modelStreaming').checked = model.streaming || false;
|
| 922 |
+
document.getElementById('editModelId').value = model.id;
|
| 923 |
+
|
| 924 |
+
// Change modal title and submit button text
|
| 925 |
+
document.querySelector('#addModelModal h3').textContent = 'Edit Custom Model';
|
| 926 |
+
document.querySelector('#addModelForm button[type="submit"]').textContent = 'Save Changes';
|
| 927 |
+
|
| 928 |
+
showAddModelModal();
|
| 929 |
+
}
|
| 930 |
|
| 931 |
+
// Hide add model modal
|
| 932 |
+
function hideAddModelModal() {
|
| 933 |
+
addModelModal.classList.add('hidden');
|
| 934 |
+
addModelForm.reset();
|
| 935 |
+
|
| 936 |
+
// Reset modal title and submit button text
|
| 937 |
+
document.querySelector('#addModelModal h3').textContent = 'Add Custom Model';
|
| 938 |
+
document.querySelector('#addModelForm button[type="submit"]').textContent = 'Add Model';
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
// Save chats to localStorage
|
| 942 |
+
function saveChats() {
|
| 943 |
+
localStorage.setItem('chats', JSON.stringify(chats));
|
| 944 |
+
}
|
| 945 |
+
|
| 946 |
+
// Toggle dark mode
|
| 947 |
+
function toggleDarkMode() {
|
| 948 |
+
const isDark = document.documentElement.classList.toggle('dark');
|
| 949 |
+
localStorage.setItem('darkMode', isDark);
|
| 950 |
+
}
|
| 951 |
+
|
| 952 |
+
// Toggle sidebar on mobile
|
| 953 |
+
function toggleSidebar() {
|
| 954 |
+
document.querySelector('.w-64').classList.toggle('hidden');
|
| 955 |
+
document.querySelector('.w-64').classList.toggle('block');
|
| 956 |
+
}
|
| 957 |
+
|
| 958 |
+
// Show API key modal
|
| 959 |
+
function showApiKeyModal() {
|
| 960 |
+
apiKeyModal.classList.remove('hidden');
|
| 961 |
+
}
|
| 962 |
+
|
| 963 |
+
// Hide API key modal
|
| 964 |
+
function hideApiKeyModal() {
|
| 965 |
+
apiKeyModal.classList.add('hidden');
|
| 966 |
+
apiKeyInput.value = '';
|
| 967 |
+
}
|
| 968 |
+
|
| 969 |
+
// Save API key
|
| 970 |
+
function saveApiKey(key) {
|
| 971 |
+
localStorage.setItem('openaiApiKey', key);
|
| 972 |
+
hideApiKeyModal();
|
| 973 |
+
}
|
| 974 |
+
|
| 975 |
+
// Event Listeners
|
| 976 |
+
closeApiKeyModal.addEventListener('click', hideApiKeyModal);
|
| 977 |
+
cancelApiKey.addEventListener('click', hideApiKeyModal);
|
| 978 |
+
|
| 979 |
+
apiKeyForm.addEventListener('submit', (e) => {
|
| 980 |
+
e.preventDefault();
|
| 981 |
+
const key = apiKeyInput.value.trim();
|
| 982 |
+
if (key) {
|
| 983 |
+
saveApiKey(key);
|
| 984 |
+
// Retry sending the message
|
| 985 |
+
setTimeout(() => {
|
| 986 |
+
sendMessage();
|
| 987 |
+
}, 100);
|
| 988 |
}
|
| 989 |
+
});
|
| 990 |
+
sidebarToggle.addEventListener('click', toggleSidebar);
|
| 991 |
+
darkModeToggle.addEventListener('click', (e) => {
|
| 992 |
+
e.stopPropagation();
|
| 993 |
+
toggleDarkMode();
|
| 994 |
+
});
|
| 995 |
+
newChatBtn.addEventListener('click', (e) => {
|
| 996 |
+
e.preventDefault();
|
| 997 |
+
e.stopPropagation();
|
| 998 |
+
createNewChat();
|
| 999 |
+
|
| 1000 |
+
// Hide welcome message when new chat is created
|
| 1001 |
+
welcomeMessage.classList.add('hidden');
|
| 1002 |
+
|
| 1003 |
+
// Close sidebar on mobile after creating new chat
|
| 1004 |
+
if (window.innerWidth < 768) {
|
| 1005 |
+
document.querySelector('.w-64').classList.add('hidden');
|
| 1006 |
+
}
|
| 1007 |
+
});
|
| 1008 |
+
|
| 1009 |
+
quickStartBtn.addEventListener('click', (e) => {
|
| 1010 |
+
e.preventDefault();
|
| 1011 |
+
e.stopPropagation();
|
| 1012 |
+
createNewChat();
|
| 1013 |
+
|
| 1014 |
+
// Hide welcome message when quick start is clicked
|
| 1015 |
+
welcomeMessage.classList.add('hidden');
|
| 1016 |
+
|
| 1017 |
+
// Close sidebar on mobile after quick start
|
| 1018 |
+
if (window.innerWidth < 768) {
|
| 1019 |
+
document.querySelector('.w-64').classList.add('hidden');
|
| 1020 |
+
}
|
| 1021 |
+
});
|
| 1022 |
+
|
| 1023 |
+
messageForm.addEventListener('submit', (e) => {
|
| 1024 |
+
e.preventDefault();
|
| 1025 |
+
sendMessage();
|
| 1026 |
+
});
|
| 1027 |
+
|
| 1028 |
+
// Auto-resize textarea
|
| 1029 |
+
messageInput.addEventListener('input', () => {
|
| 1030 |
+
messageInput.style.height = 'auto';
|
| 1031 |
+
messageInput.style.height = `${messageInput.scrollHeight}px`;
|
| 1032 |
+
});
|
| 1033 |
+
|
| 1034 |
+
// Model dropdown
|
| 1035 |
+
modelDropdownBtn.addEventListener('click', (e) => {
|
| 1036 |
+
e.stopPropagation();
|
| 1037 |
+
modelDropdown.classList.toggle('hidden');
|
| 1038 |
+
});
|
| 1039 |
|
| 1040 |
+
// Close dropdown when clicking outside
|
| 1041 |
+
document.addEventListener('click', (e) => {
|
| 1042 |
+
if (!modelDropdown.contains(e.target) && e.target !== modelDropdownBtn && !e.target.closest('.model-option')) {
|
| 1043 |
+
modelDropdown.classList.add('hidden');
|
| 1044 |
+
}
|
| 1045 |
+
});
|
| 1046 |
+
|
| 1047 |
+
// Handle model selection - delegated event listener
|
| 1048 |
+
document.addEventListener('click', (e) => {
|
| 1049 |
+
const modelOption = e.target.closest('.model-option');
|
| 1050 |
+
if (modelOption && !e.target.classList.contains('delete-model')) {
|
| 1051 |
+
const modelId = modelOption.dataset.model;
|
| 1052 |
+
selectModel(modelId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1053 |
}
|
| 1054 |
+
});
|
| 1055 |
+
|
| 1056 |
+
// Add model
|
| 1057 |
+
addModelBtn.addEventListener('click', showAddModelModal);
|
| 1058 |
+
closeModelModal.addEventListener('click', hideAddModelModal);
|
| 1059 |
+
cancelAddModel.addEventListener('click', hideAddModelModal);
|
| 1060 |
+
|
| 1061 |
+
addModelForm.addEventListener('submit', (e) => {
|
| 1062 |
+
e.preventDefault();
|
| 1063 |
+
const name = document.getElementById('modelName').value;
|
| 1064 |
+
const endpoint = document.getElementById('modelEndpoint').value;
|
| 1065 |
+
const key = document.getElementById('modelKey').value;
|
| 1066 |
+
const streaming = document.getElementById('modelStreaming').checked;
|
| 1067 |
+
const editModelId = document.getElementById('editModelId').value;
|
| 1068 |
+
|
| 1069 |
+
if (editModelId) {
|
| 1070 |
+
editCustomModel(editModelId, name, endpoint, key, streaming);
|
| 1071 |
+
} else {
|
| 1072 |
+
addCustomModel(name, endpoint, key);
|
| 1073 |
+
}
|
| 1074 |
+
});
|
| 1075 |
+
|
| 1076 |
+
// Delete chat
|
| 1077 |
+
closeDeleteModal.addEventListener('click', hideDeleteModal);
|
| 1078 |
+
cancelDelete.addEventListener('click', hideDeleteModal);
|
| 1079 |
+
confirmDelete.addEventListener('click', () => {
|
| 1080 |
+
if (chatToDelete) {
|
| 1081 |
+
deleteChat(chatToDelete);
|
| 1082 |
+
}
|
| 1083 |
+
});
|
| 1084 |
+
|
| 1085 |
+
// Settings button
|
| 1086 |
+
document.getElementById('settingsBtn').addEventListener('click', (e) => {
|
| 1087 |
+
e.stopPropagation();
|
| 1088 |
+
alert('Settings panel will be implemented here');
|
| 1089 |
+
// You can replace this with actual settings modal later
|
| 1090 |
+
});
|
| 1091 |
+
|
| 1092 |
+
// Handle message actions
|
| 1093 |
+
function handleMessageAction(action, messageIndex) {
|
| 1094 |
+
if (!currentChatId) return;
|
| 1095 |
|
| 1096 |
+
const chat = chats.find(c => c.id === currentChatId);
|
| 1097 |
+
if (!chat || !chat.messages[messageIndex]) return;
|
| 1098 |
+
|
| 1099 |
+
if (action === 'regenerate') {
|
| 1100 |
+
// Only regenerate if this is the last assistant message
|
| 1101 |
+
if (messageIndex === chat.messages.length - 1 &&
|
| 1102 |
+
chat.messages[messageIndex].role === 'assistant') {
|
| 1103 |
+
chat.messages.splice(messageIndex, 1);
|
| 1104 |
+
renderMessages(chat.messages);
|
| 1105 |
+
|
| 1106 |
+
// Resend last user message
|
| 1107 |
+
const lastUserMessage = chat.messages[chat.messages.length - 1];
|
| 1108 |
+
if (lastUserMessage && lastUserMessage.role === 'user') {
|
| 1109 |
+
sendMessage(lastUserMessage.content);
|
| 1110 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1111 |
}
|
| 1112 |
+
} else if (action === 'edit') {
|
| 1113 |
+
// TODO: Implement edit functionality
|
| 1114 |
+
alert('Edit functionality will be implemented here');
|
| 1115 |
+
}
|
| 1116 |
+
}
|
| 1117 |
+
|
| 1118 |
+
// Initialize the app
|
| 1119 |
+
init();
|
| 1120 |
+
|
| 1121 |
+
// Add delegated event listener for message actions
|
| 1122 |
+
document.addEventListener('click', (e) => {
|
| 1123 |
+
const actionBtn = e.target.closest('.message-action');
|
| 1124 |
+
if (actionBtn) {
|
| 1125 |
+
e.preventDefault();
|
| 1126 |
+
e.stopPropagation();
|
| 1127 |
+
|
| 1128 |
+
const messageElement = actionBtn.closest('.flex.gap-4');
|
| 1129 |
+
const messageIndex = Array.from(messageElement.parentNode.children).indexOf(messageElement);
|
| 1130 |
+
const action = actionBtn.dataset.action;
|
| 1131 |
+
|
| 1132 |
+
handleMessageAction(action, messageIndex);
|
| 1133 |
}
|
| 1134 |
});
|
| 1135 |
</script>
|