Update index.html
Browse files- index.html +226 -208
index.html
CHANGED
@@ -41,7 +41,7 @@
|
|
41 |
.settings-icon.active-ai { color: #4a90e2; }
|
42 |
.settings-icon.active-ai:hover { color: #6aaeff; }
|
43 |
.settings-icon svg, .close-btn svg { width: 18px; height: 18px; }
|
44 |
-
|
45 |
.chess-board-container {
|
46 |
flex: 1;
|
47 |
display: flex;
|
@@ -58,13 +58,13 @@
|
|
58 |
z-index: 0;
|
59 |
contain: content;
|
60 |
}
|
61 |
-
|
62 |
#game-board {
|
63 |
width: 100%;
|
64 |
height: 100%;
|
65 |
aspect-ratio: 1/1;
|
66 |
}
|
67 |
-
|
68 |
.bottom-section {
|
69 |
padding: 16px; border-top: 1px solid #404040; display: flex;
|
70 |
align-items: center; gap: 12px; min-height: 56px; position: relative; z-index: 2;
|
@@ -138,7 +138,7 @@
|
|
138 |
}
|
139 |
.new-game-btn:hover { background-color: #3a7bc8; }
|
140 |
.resign-btn svg, .new-game-btn svg { width: 16px; height: 16px; fill: currentColor; }
|
141 |
-
|
142 |
.modal-overlay {
|
143 |
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
144 |
background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(4px); display: none;
|
@@ -221,7 +221,7 @@
|
|
221 |
transition: top 0.5s ease-in-out; display: flex; align-items: center; gap: 10px;
|
222 |
}
|
223 |
.notification.show { top: 20px; }
|
224 |
-
|
225 |
@media (max-width: 768px) {
|
226 |
.container { flex-direction: column; }
|
227 |
.left-panel {
|
@@ -260,12 +260,12 @@
|
|
260 |
padding: 2px;
|
261 |
}
|
262 |
}
|
263 |
-
|
264 |
#api-key-input { width: 100%; padding: 10px; background: #404040; border: 1px solid #555; color: #fff; }
|
265 |
.credit { font-size: 12px; color: #888; margin-left: auto; }
|
266 |
.credit a { color: #4a90e2; text-decoration: none; }
|
267 |
.credit a:hover { text-decoration: underline; }
|
268 |
-
|
269 |
.model-icon {
|
270 |
width: 20px;
|
271 |
height: 20px;
|
@@ -282,7 +282,7 @@
|
|
282 |
height: 12px;
|
283 |
fill: currentColor;
|
284 |
}
|
285 |
-
|
286 |
.api-key-warning {
|
287 |
background-color: #d32f2f;
|
288 |
color: white;
|
@@ -327,7 +327,7 @@
|
|
327 |
</div>
|
328 |
Blue
|
329 |
</button>
|
330 |
-
|
331 |
<div class="settings-section-header">Gemini Model</div>
|
332 |
<button id="model-flash-btn" class="model-btn active">
|
333 |
<div class="model-icon">
|
@@ -341,7 +341,7 @@
|
|
341 |
</div>
|
342 |
Pro (Powerful)
|
343 |
</button>
|
344 |
-
|
345 |
<div class="settings-section-header">API Settings</div>
|
346 |
<button id="api-settings-btn" class="model-btn">
|
347 |
<div class="model-icon">
|
@@ -384,7 +384,7 @@
|
|
384 |
</div>
|
385 |
</div>
|
386 |
</div>
|
387 |
-
|
388 |
<div class="modal-overlay" id="pgn-modal-overlay">
|
389 |
<div class="modal">
|
390 |
<div class="modal-header">
|
@@ -403,7 +403,7 @@
|
|
403 |
</div>
|
404 |
</div>
|
405 |
</div>
|
406 |
-
|
407 |
<div class="modal-overlay" id="ai-modal-overlay">
|
408 |
<div class="modal">
|
409 |
<div class="modal-header">
|
@@ -435,7 +435,7 @@
|
|
435 |
API key tidak valid atau kosong. Silakan masukkan API key yang valid.
|
436 |
</div>
|
437 |
<button class="modal-action-btn save-ai" id="save-settings-btn"><svg fill="currentColor" width="24" height="24" viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>Save API Key</button>
|
438 |
-
|
439 |
<div class="settings-section-header">Board Theme</div>
|
440 |
<button id="theme-brown-btn-modal" class="theme-btn"><div class="theme-swatch"><div class="theme-swatch-light" style="background-color: #f0d9b5;"></div><div class="theme-swatch-dark" style="background-color: #b58863;"></div></div>Brown</button>
|
441 |
<button id="theme-blue-btn-modal" class="theme-btn active"><div class="theme-swatch"><div class="theme-swatch-light" style="background-color: #D8DEE9;"></div><div class="theme-swatch-dark" style="background-color: #819AAE;"></div></div>Blue</button>
|
@@ -446,7 +446,7 @@
|
|
446 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
447 |
<script src="https://unpkg.com/@chrisoakman/[email protected]/dist/chessboard-1.0.0.min.js"></script>
|
448 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.10.2/chess.min.js"></script>
|
449 |
-
|
450 |
<script>
|
451 |
var board = null, game = new Chess(), $status = $('#status-display');
|
452 |
var playerColor = 'w', engine = null, isGameOver = false, viewingMoveIndex = -1;
|
@@ -454,11 +454,11 @@
|
|
454 |
document.head.appendChild(boardThemeStyle);
|
455 |
|
456 |
var aiPersonality = '';
|
457 |
-
var geminiApiKey = '';
|
458 |
-
var userApiKey = '';
|
459 |
var isEngineSearching = false;
|
460 |
var topMovesWithEvals = [];
|
461 |
-
var selectedGeminiModel = 'gemini-1.5-flash-latest';
|
462 |
const useLLM = () => aiPersonality && aiPersonality.trim() !== '' && (userApiKey || geminiApiKey);
|
463 |
|
464 |
const THEMES = {
|
@@ -469,6 +469,32 @@
|
|
469 |
const pieceImagePaths = {'wP': 'https://upload.wikimedia.org/wikipedia/commons/4/45/Chess_plt45.svg','wR': 'https://upload.wikimedia.org/wikipedia/commons/7/72/Chess_rlt45.svg','wN': 'https://upload.wikimedia.org/wikipedia/commons/7/70/Chess_nlt45.svg','wB': 'https://upload.wikimedia.org/wikipedia/commons/b/b1/Chess_blt45.svg','wQ': 'https://upload.wikimedia.org/wikipedia/commons/1/15/Chess_qlt45.svg','wK': 'https://upload.wikimedia.org/wikipedia/commons/4/42/Chess_klt45.svg','bP': 'https://upload.wikimedia.org/wikipedia/commons/c/c7/Chess_pdt45.svg','bR': 'https://upload.wikimedia.org/wikipedia/commons/f/ff/Chess_rdt45.svg','bN': 'https://upload.wikimedia.org/wikipedia/commons/e/ef/Chess_ndt45.svg','bB': 'https://upload.wikimedia.org/wikipedia/commons/9/98/Chess_bdt45.svg','bQ': 'https://upload.wikimedia.org/wikipedia/commons/4/47/Chess_qdt45.svg','bK': 'https://upload.wikimedia.org/wikipedia/commons/f/f0/Chess_kdt45.svg'};
|
470 |
function pieceTheme(piece) { return pieceImagePaths[piece]; }
|
471 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
472 |
function setBoardTheme(themeName) {
|
473 |
const theme = THEMES[themeName];
|
474 |
boardThemeStyle.innerHTML = `
|
@@ -491,21 +517,20 @@
|
|
491 |
showNotification(`AI Model set to ${modelName.includes('pro') ? 'Pro (Powerful)' : 'Flash (Fast)'}.`, 3000);
|
492 |
}
|
493 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
494 |
function onDragStart(source, piece) {
|
495 |
-
const canMove = !isGameOver && !isEngineSearching && !game.game_over() && game.turn() === playerColor &&
|
496 |
-
|
497 |
-
if (!canMove) {
|
498 |
-
console.log("Move disallowed. Current state:", {
|
499 |
-
isGameOver,
|
500 |
-
isEngineSearching,
|
501 |
-
isGameTerminated: game.game_over(),
|
502 |
-
isPlayerTurn: game.turn() === playerColor,
|
503 |
-
isViewingLatestMove: viewingMoveIndex === game.history().length - 1
|
504 |
-
});
|
505 |
-
}
|
506 |
return canMove;
|
507 |
}
|
508 |
-
|
509 |
function createEngineWorker() {
|
510 |
return fetch(STOCKFISH_WORKER_URL).then(res => res.text()).then(scriptText => {
|
511 |
const blob = new Blob([scriptText], { type: 'application/javascript' });
|
@@ -513,6 +538,11 @@
|
|
513 |
});
|
514 |
}
|
515 |
|
|
|
|
|
|
|
|
|
|
|
516 |
function initializeEngine() {
|
517 |
createEngineWorker().then(worker => {
|
518 |
engine = worker;
|
@@ -525,205 +555,203 @@
|
|
525 |
updateStatus("Error: Could not load engine.");
|
526 |
});
|
527 |
}
|
528 |
-
|
529 |
function showNotification(message, duration = 6000) {
|
530 |
const notification = $('#ai-notification');
|
531 |
$('#ai-notification-text').text(message);
|
532 |
notification.addClass('show');
|
533 |
setTimeout(() => { notification.removeClass('show'); }, duration);
|
534 |
}
|
535 |
-
|
536 |
function handleEngineMessage(event) {
|
537 |
-
const message = event.data;
|
538 |
if (useLLM()) {
|
539 |
-
if (message.startsWith('info')
|
540 |
-
const
|
541 |
-
|
542 |
-
|
543 |
-
const
|
544 |
-
const move =
|
545 |
-
|
|
|
546 |
topMovesWithEvals.push({ move, score });
|
|
|
547 |
}
|
548 |
}
|
549 |
}
|
550 |
if (message.startsWith('bestmove')) {
|
551 |
const bestMoveFallback = message.split(' ')[1];
|
552 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
553 |
}
|
554 |
} else {
|
555 |
if (message.startsWith('bestmove')) {
|
556 |
-
|
557 |
-
makeMoveOnBoard(
|
|
|
|
|
|
|
558 |
}
|
559 |
}
|
560 |
}
|
561 |
|
562 |
-
async function decideMoveWithLLM(fallbackMove) {
|
563 |
-
|
564 |
-
|
565 |
-
|
566 |
-
|
567 |
-
|
568 |
-
|
569 |
-
|
570 |
-
|
571 |
-
return;
|
572 |
-
}
|
573 |
-
|
574 |
-
const maxRetries = 2; // Reduced retries to prevent long loops
|
575 |
-
let retryCount = 0;
|
576 |
-
|
577 |
-
while (retryCount < maxRetries) {
|
578 |
-
try {
|
579 |
-
if (topMovesWithEvals.length === 0) {
|
580 |
-
console.warn("No top moves were analyzed. Using fallback move:", fallbackMove);
|
581 |
-
isEngineSearching = false;
|
582 |
-
makeMoveOnBoard(fallbackMove);
|
583 |
-
return;
|
584 |
-
}
|
585 |
-
|
586 |
-
const fen = game.fen();
|
587 |
-
const turn = game.turn() === 'w' ? 'White' : 'Black';
|
588 |
-
const movesData = JSON.stringify(topMovesWithEvals);
|
589 |
-
|
590 |
-
const prompt = `You are a world-class chess engine and a master role-player. Your goal is to perfectly blend strong chess play with a defined personality. Your priority is 80% playing the best move and 20% fitting your character. You must select a move from the provided list.
|
591 |
|
592 |
-
|
593 |
-
|
594 |
-
|
595 |
-
|
596 |
|
597 |
-
|
598 |
-
|
|
|
|
|
|
|
|
|
599 |
|
600 |
-
|
601 |
-
|
|
|
|
|
602 |
|
603 |
-
|
604 |
-
|
605 |
-
-
|
606 |
-
- "
|
607 |
-
- "deviationWarning": If you were forced to deviate from your personality to avoid a blunder, provide a short message explaining why. If you did not deviate, this MUST be null.`;
|
608 |
|
609 |
-
|
610 |
-
$('#ai-reasoning-text').text('Thinking...');
|
611 |
|
612 |
-
|
613 |
-
|
614 |
-
|
615 |
-
|
616 |
-
|
617 |
-
|
618 |
-
if (response.status === 429) {
|
619 |
-
const delay = Math.pow(2, retryCount) * 1000 + Math.random() * 1000;
|
620 |
-
console.warn(`Rate limit hit. Retrying in ${delay.toFixed(2)}ms...`);
|
621 |
-
await new Promise(resolve => setTimeout(resolve, delay));
|
622 |
-
retryCount++;
|
623 |
-
continue;
|
624 |
-
}
|
625 |
|
626 |
-
|
627 |
-
|
628 |
-
}
|
629 |
|
630 |
-
|
|
|
|
|
|
|
|
|
631 |
|
632 |
-
|
633 |
-
|
634 |
-
|
635 |
-
throw new Error(`Invalid response structure from Gemini API. Finish Reason: ${reason}`);
|
636 |
-
}
|
637 |
|
638 |
-
|
639 |
-
|
640 |
-
|
641 |
-
|
|
|
642 |
}
|
|
|
|
|
643 |
|
644 |
-
|
645 |
-
|
646 |
-
|
647 |
-
|
648 |
-
throw new Error("Parsed JSON from LLM is missing required keys ('bestMove', 'reasoning').");
|
649 |
-
}
|
650 |
|
651 |
-
|
652 |
-
|
653 |
-
|
654 |
-
|
655 |
|
656 |
-
|
657 |
-
|
658 |
-
} else {
|
659 |
-
console.warn(`LLM chose a move (${result.bestMove}) not in the valid list. Defaulting to best Stockfish move.`);
|
660 |
-
makeMoveOnBoard(fallbackMove);
|
661 |
-
}
|
662 |
-
isEngineSearching = false;
|
663 |
-
return;
|
664 |
|
665 |
-
|
666 |
-
|
667 |
-
|
668 |
-
|
669 |
-
|
670 |
-
|
671 |
-
updateStatus('Error with AI. Using best move.');
|
672 |
-
|
673 |
-
// Show specific error message based on the error
|
674 |
-
if (error.message.includes('API request failed with status 400')) {
|
675 |
-
showNotification('Invalid API key. Please check your API key in Settings.', 8000);
|
676 |
-
} else {
|
677 |
-
showNotification('Error with AI service. Using best move.', 5000);
|
678 |
-
}
|
679 |
-
|
680 |
-
makeMoveOnBoard(fallbackMove);
|
681 |
-
isEngineSearching = false;
|
682 |
-
break;
|
683 |
-
}
|
684 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
685 |
}
|
686 |
}
|
687 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
688 |
function makeEngineMove() {
|
689 |
if (isGameOver || game.game_over() || !engine) return;
|
690 |
-
isEngineSearching
|
|
|
691 |
updateStatus('Gemifish is thinking...');
|
692 |
|
693 |
topMovesWithEvals = [];
|
|
|
694 |
engine.postMessage('position fen ' + game.fen());
|
695 |
engine.postMessage('go depth 15');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
696 |
}
|
697 |
|
698 |
function makeMoveOnBoard(move) {
|
699 |
-
|
|
|
|
|
700 |
board.position(game.fen());
|
701 |
-
board.resize();
|
702 |
viewingMoveIndex = game.history().length - 1;
|
703 |
-
updateStatus();
|
704 |
updateMoveHistory();
|
705 |
-
|
|
|
706 |
}
|
707 |
|
708 |
function onDrop(source, target) {
|
|
|
709 |
var move = game.move({ from: source, to: target, promotion: 'q' });
|
710 |
if (move === null) return 'snapback';
|
711 |
-
|
712 |
viewingMoveIndex = game.history().length - 1;
|
713 |
updateMoveHistory();
|
714 |
-
|
|
|
715 |
if (game.game_over()) {
|
716 |
updateStatus();
|
|
|
717 |
} else {
|
718 |
-
window.setTimeout(makeEngineMove,
|
719 |
}
|
720 |
}
|
721 |
|
722 |
function onSnapEnd() {
|
723 |
board.position(game.fen());
|
724 |
-
|
725 |
}
|
726 |
-
|
727 |
function updateMoveHistory() {
|
728 |
const history = game.history({ verbose: false });
|
729 |
const moveList = $('#moves-list');
|
@@ -740,13 +768,14 @@ You MUST respond in a VALID JSON FORMAT, with no other text. The JSON object mus
|
|
740 |
}
|
741 |
$('.moves-section').scrollTop($('.moves-section')[0].scrollHeight);
|
742 |
updateHighlighting();
|
|
|
743 |
}
|
744 |
|
745 |
function updateHighlighting() {
|
746 |
$('.move').removeClass('highlighted-move');
|
747 |
if (viewingMoveIndex > -1) $('.move').eq(viewingMoveIndex).addClass('highlighted-move');
|
748 |
}
|
749 |
-
|
750 |
function navigateToMove(index) {
|
751 |
const history = game.history();
|
752 |
if (index < -1 || index >= history.length) return;
|
@@ -754,9 +783,9 @@ You MUST respond in a VALID JSON FORMAT, with no other text. The JSON object mus
|
|
754 |
const tempGame = new Chess();
|
755 |
for (let i = 0; i <= viewingMoveIndex; i++) tempGame.move(history[i]);
|
756 |
board.position(tempGame.fen());
|
757 |
-
setTimeout(() => board.resize(), 10);
|
758 |
updateHighlighting();
|
759 |
updateNavButtons();
|
|
|
760 |
}
|
761 |
|
762 |
function updateStatus(customStatus) {
|
@@ -776,11 +805,13 @@ You MUST respond in a VALID JSON FORMAT, with no other text. The JSON object mus
|
|
776 |
}
|
777 |
if (game.game_over() || isGameOver) {
|
778 |
$('#resign-btn').prop('disabled', true);
|
|
|
|
|
779 |
}
|
780 |
$status.html(status);
|
781 |
updateNavButtons();
|
782 |
}
|
783 |
-
|
784 |
function updateNavButtons() {
|
785 |
const historyLength = game.history().length;
|
786 |
const onLatestMove = viewingMoveIndex === historyLength - 1;
|
@@ -790,32 +821,35 @@ You MUST respond in a VALID JSON FORMAT, with no other text. The JSON object mus
|
|
790 |
$('#btn-last').prop('disabled', onLatestMove);
|
791 |
}
|
792 |
|
793 |
-
// Function to start a new game
|
794 |
function newGame() {
|
|
|
795 |
game = new Chess();
|
796 |
board.position('start');
|
797 |
viewingMoveIndex = -1;
|
798 |
isGameOver = false;
|
799 |
-
|
800 |
topMovesWithEvals = [];
|
801 |
updateMoveHistory();
|
802 |
updateStatus();
|
803 |
$('#ai-reasoning-text').text('Set a personality to see the AI\'s thoughts here.');
|
804 |
$('#resign-btn').prop('disabled', false);
|
805 |
showNotification('New game started!', 2000);
|
|
|
806 |
}
|
807 |
|
808 |
function openShareModal() {
|
809 |
$('#fenInput').val(game.fen());
|
810 |
$('#pgnText').val(game.pgn());
|
811 |
$('#pgn-modal-overlay').css('display', 'flex');
|
|
|
812 |
}
|
813 |
-
function closeShareModal() { $('#pgn-modal-overlay').css('display', 'none'); }
|
814 |
function openAIPersonalityModal() {
|
815 |
$('#ai-personality-input').val(aiPersonality);
|
816 |
$('#ai-modal-overlay').css('display', 'flex');
|
|
|
817 |
}
|
818 |
-
function closeAIPersonalityModal() { $('#ai-modal-overlay').css('display', 'none'); }
|
819 |
function saveAIPersonality() {
|
820 |
aiPersonality = $('#ai-personality-input').val();
|
821 |
if (useLLM()) {
|
@@ -851,13 +885,13 @@ You MUST respond in a VALID JSON FORMAT, with no other text. The JSON object mus
|
|
851 |
document.body.removeChild(a); URL.revokeObjectURL(url);
|
852 |
}
|
853 |
|
854 |
-
// New functions for settings modal
|
855 |
function openSettingsModal() {
|
856 |
$('#api-key-input').val(userApiKey);
|
857 |
$('#api-key-warning').hide();
|
858 |
$('#settings-modal-overlay').css('display', 'flex');
|
|
|
859 |
}
|
860 |
-
function closeSettingsModal() { $('#settings-modal-overlay').css('display', 'none'); }
|
861 |
|
862 |
var config = {
|
863 |
draggable: true,
|
@@ -869,41 +903,30 @@ You MUST respond in a VALID JSON FORMAT, with no other text. The JSON object mus
|
|
869 |
showNotation: true
|
870 |
};
|
871 |
board = Chessboard('game-board', config);
|
872 |
-
|
873 |
-
// Fungsi untuk memastikan board menyesuaikan dengan container
|
874 |
-
function ensureBoardFits() {
|
875 |
-
const container = $('.chess-board-container');
|
876 |
-
const containerWidth = container.width() - 16;
|
877 |
-
const containerHeight = container.height() - 16;
|
878 |
-
|
879 |
-
const maxSize = Math.min(containerWidth, containerHeight);
|
880 |
-
|
881 |
-
$('#game-board').css({
|
882 |
-
'width': maxSize + 'px',
|
883 |
-
'height': maxSize + 'px'
|
884 |
-
});
|
885 |
-
|
886 |
-
board.resize();
|
887 |
-
}
|
888 |
-
|
889 |
-
// Panggil fungsi saat inisialisasi
|
890 |
ensureBoardFits();
|
891 |
-
|
892 |
-
|
893 |
-
$(window).on('resize', ensureBoardFits);
|
894 |
|
895 |
// Event Handlers
|
896 |
$('#new-game-btn').on('click', newGame);
|
897 |
-
$('#resign-btn').on('click', () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
898 |
$('#btn-back').on('click', () => navigateToMove(viewingMoveIndex - 1));
|
899 |
$('#btn-forward').on('click', () => navigateToMove(viewingMoveIndex + 1));
|
900 |
$('#btn-first').on('click', () => navigateToMove(-1));
|
901 |
$('#btn-last').on('click', () => navigateToMove(game.history().length - 1));
|
902 |
-
|
903 |
$('#share-btn').on('click', openShareModal);
|
904 |
$('#modal-close-btn').on('click', function(e) { e.stopPropagation(); closeShareModal(); });
|
905 |
$('#pgn-modal-overlay').on('click', function(e) { if (e.target === this || $(e.target).is('#modal-close-btn, #modal-close-btn *')) closeShareModal(); });
|
906 |
-
|
907 |
$('#ai-btn').on('click', openAIPersonalityModal);
|
908 |
$('#ai-modal-close-btn').on('click', function(e) { e.stopPropagation(); closeAIPersonalityModal(); });
|
909 |
$('#ai-modal-overlay').on('click', function(e) { if (e.target === this || $(e.target).is('#ai-modal-close-btn, #ai-modal-close-btn *')) closeAIPersonalityModal(); });
|
@@ -913,59 +936,54 @@ You MUST respond in a VALID JSON FORMAT, with no other text. The JSON object mus
|
|
913 |
$('#copy-fen-btn').on('click', function() { copyToClipboard('fenInput', this); });
|
914 |
$('#copy-pgn-btn').on('click', function() { copyToClipboard('pgnText', this); });
|
915 |
$('#download-pgn-btn').on('click', downloadPGN);
|
916 |
-
|
917 |
$('#settings-btn').on('click', function(e) {
|
918 |
e.stopPropagation();
|
919 |
$('#settings-popover').toggle();
|
920 |
});
|
921 |
-
|
922 |
$(document).on('click', function() {
|
923 |
$('#settings-popover').hide();
|
924 |
});
|
925 |
-
|
926 |
$('#settings-popover').on('click', e => e.stopPropagation());
|
927 |
-
|
928 |
$('#api-settings-btn').on('click', function() {
|
929 |
$('#settings-popover').hide();
|
930 |
openSettingsModal();
|
931 |
});
|
932 |
-
|
933 |
$('#settings-modal-close-btn').on('click', function(e) { e.stopPropagation(); closeSettingsModal(); });
|
934 |
$('#settings-modal-overlay').on('click', function(e) { if (e.target === this || $(e.target).is('#settings-modal-close-btn, #settings-modal-close-btn *')) closeSettingsModal(); });
|
935 |
-
|
936 |
$('#save-settings-btn').on('click', () => {
|
937 |
const newApiKey = $('#api-key-input').val().trim();
|
938 |
-
|
939 |
-
// Simple validation for API key
|
940 |
if (!newApiKey || newApiKey.length < 10) {
|
941 |
$('#api-key-warning').show();
|
942 |
return;
|
943 |
}
|
944 |
-
|
945 |
userApiKey = newApiKey;
|
946 |
$('#api-key-warning').hide();
|
947 |
closeSettingsModal();
|
948 |
showNotification('API key saved successfully!', 3000);
|
949 |
-
|
950 |
-
// If AI personality is set but no API key was available before, update status
|
951 |
if (aiPersonality && aiPersonality.trim() !== '') {
|
952 |
$('#ai-btn').addClass('active-ai');
|
953 |
$('#ai-reasoning-text').text('Personality set. Make your move!');
|
954 |
}
|
955 |
});
|
956 |
-
|
957 |
// Theme buttons in popover
|
958 |
$('#theme-brown-btn').on('click', () => setBoardTheme('brown'));
|
959 |
$('#theme-blue-btn').on('click', () => setBoardTheme('blue'));
|
960 |
-
|
961 |
// Theme buttons in modal
|
962 |
$('#theme-brown-btn-modal').on('click', () => setBoardTheme('brown'));
|
963 |
$('#theme-blue-btn-modal').on('click', () => setBoardTheme('blue'));
|
964 |
-
|
965 |
// Model selection buttons
|
966 |
$('#model-flash-btn').on('click', () => setAIModel('gemini-1.5-flash-latest'));
|
967 |
$('#model-pro-btn').on('click', () => setAIModel('gemini-1.5-pro-latest'));
|
968 |
-
|
969 |
setBoardTheme('blue');
|
970 |
initializeEngine();
|
971 |
</script>
|
|
|
41 |
.settings-icon.active-ai { color: #4a90e2; }
|
42 |
.settings-icon.active-ai:hover { color: #6aaeff; }
|
43 |
.settings-icon svg, .close-btn svg { width: 18px; height: 18px; }
|
44 |
+
|
45 |
.chess-board-container {
|
46 |
flex: 1;
|
47 |
display: flex;
|
|
|
58 |
z-index: 0;
|
59 |
contain: content;
|
60 |
}
|
61 |
+
|
62 |
#game-board {
|
63 |
width: 100%;
|
64 |
height: 100%;
|
65 |
aspect-ratio: 1/1;
|
66 |
}
|
67 |
+
|
68 |
.bottom-section {
|
69 |
padding: 16px; border-top: 1px solid #404040; display: flex;
|
70 |
align-items: center; gap: 12px; min-height: 56px; position: relative; z-index: 2;
|
|
|
138 |
}
|
139 |
.new-game-btn:hover { background-color: #3a7bc8; }
|
140 |
.resign-btn svg, .new-game-btn svg { width: 16px; height: 16px; fill: currentColor; }
|
141 |
+
|
142 |
.modal-overlay {
|
143 |
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
144 |
background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(4px); display: none;
|
|
|
221 |
transition: top 0.5s ease-in-out; display: flex; align-items: center; gap: 10px;
|
222 |
}
|
223 |
.notification.show { top: 20px; }
|
224 |
+
|
225 |
@media (max-width: 768px) {
|
226 |
.container { flex-direction: column; }
|
227 |
.left-panel {
|
|
|
260 |
padding: 2px;
|
261 |
}
|
262 |
}
|
263 |
+
|
264 |
#api-key-input { width: 100%; padding: 10px; background: #404040; border: 1px solid #555; color: #fff; }
|
265 |
.credit { font-size: 12px; color: #888; margin-left: auto; }
|
266 |
.credit a { color: #4a90e2; text-decoration: none; }
|
267 |
.credit a:hover { text-decoration: underline; }
|
268 |
+
|
269 |
.model-icon {
|
270 |
width: 20px;
|
271 |
height: 20px;
|
|
|
282 |
height: 12px;
|
283 |
fill: currentColor;
|
284 |
}
|
285 |
+
|
286 |
.api-key-warning {
|
287 |
background-color: #d32f2f;
|
288 |
color: white;
|
|
|
327 |
</div>
|
328 |
Blue
|
329 |
</button>
|
330 |
+
|
331 |
<div class="settings-section-header">Gemini Model</div>
|
332 |
<button id="model-flash-btn" class="model-btn active">
|
333 |
<div class="model-icon">
|
|
|
341 |
</div>
|
342 |
Pro (Powerful)
|
343 |
</button>
|
344 |
+
|
345 |
<div class="settings-section-header">API Settings</div>
|
346 |
<button id="api-settings-btn" class="model-btn">
|
347 |
<div class="model-icon">
|
|
|
384 |
</div>
|
385 |
</div>
|
386 |
</div>
|
387 |
+
|
388 |
<div class="modal-overlay" id="pgn-modal-overlay">
|
389 |
<div class="modal">
|
390 |
<div class="modal-header">
|
|
|
403 |
</div>
|
404 |
</div>
|
405 |
</div>
|
406 |
+
|
407 |
<div class="modal-overlay" id="ai-modal-overlay">
|
408 |
<div class="modal">
|
409 |
<div class="modal-header">
|
|
|
435 |
API key tidak valid atau kosong. Silakan masukkan API key yang valid.
|
436 |
</div>
|
437 |
<button class="modal-action-btn save-ai" id="save-settings-btn"><svg fill="currentColor" width="24" height="24" viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>Save API Key</button>
|
438 |
+
|
439 |
<div class="settings-section-header">Board Theme</div>
|
440 |
<button id="theme-brown-btn-modal" class="theme-btn"><div class="theme-swatch"><div class="theme-swatch-light" style="background-color: #f0d9b5;"></div><div class="theme-swatch-dark" style="background-color: #b58863;"></div></div>Brown</button>
|
441 |
<button id="theme-blue-btn-modal" class="theme-btn active"><div class="theme-swatch"><div class="theme-swatch-light" style="background-color: #D8DEE9;"></div><div class="theme-swatch-dark" style="background-color: #819AAE;"></div></div>Blue</button>
|
|
|
446 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
447 |
<script src="https://unpkg.com/@chrisoakman/[email protected]/dist/chessboard-1.0.0.min.js"></script>
|
448 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.10.2/chess.min.js"></script>
|
449 |
+
|
450 |
<script>
|
451 |
var board = null, game = new Chess(), $status = $('#status-display');
|
452 |
var playerColor = 'w', engine = null, isGameOver = false, viewingMoveIndex = -1;
|
|
|
454 |
document.head.appendChild(boardThemeStyle);
|
455 |
|
456 |
var aiPersonality = '';
|
457 |
+
var geminiApiKey = '';
|
458 |
+
var userApiKey = '';
|
459 |
var isEngineSearching = false;
|
460 |
var topMovesWithEvals = [];
|
461 |
+
var selectedGeminiModel = 'gemini-1.5-flash-latest';
|
462 |
const useLLM = () => aiPersonality && aiPersonality.trim() !== '' && (userApiKey || geminiApiKey);
|
463 |
|
464 |
const THEMES = {
|
|
|
469 |
const pieceImagePaths = {'wP': 'https://upload.wikimedia.org/wikipedia/commons/4/45/Chess_plt45.svg','wR': 'https://upload.wikimedia.org/wikipedia/commons/7/72/Chess_rlt45.svg','wN': 'https://upload.wikimedia.org/wikipedia/commons/7/70/Chess_nlt45.svg','wB': 'https://upload.wikimedia.org/wikipedia/commons/b/b1/Chess_blt45.svg','wQ': 'https://upload.wikimedia.org/wikipedia/commons/1/15/Chess_qlt45.svg','wK': 'https://upload.wikimedia.org/wikipedia/commons/4/42/Chess_klt45.svg','bP': 'https://upload.wikimedia.org/wikipedia/commons/c/c7/Chess_pdt45.svg','bR': 'https://upload.wikimedia.org/wikipedia/commons/f/ff/Chess_rdt45.svg','bN': 'https://upload.wikimedia.org/wikipedia/commons/e/ef/Chess_ndt45.svg','bB': 'https://upload.wikimedia.org/wikipedia/commons/9/98/Chess_bdt45.svg','bQ': 'https://upload.wikimedia.org/wikipedia/commons/4/47/Chess_qdt45.svg','bK': 'https://upload.wikimedia.org/wikipedia/commons/f/f0/Chess_kdt45.svg'};
|
470 |
function pieceTheme(piece) { return pieceImagePaths[piece]; }
|
471 |
|
472 |
+
// Resize handling
|
473 |
+
let resizeObserver = null;
|
474 |
+
let resizeRAF = null;
|
475 |
+
function rafResize(cb) {
|
476 |
+
if (resizeRAF) cancelAnimationFrame(resizeRAF);
|
477 |
+
resizeRAF = requestAnimationFrame(cb);
|
478 |
+
}
|
479 |
+
function ensureBoardFits() {
|
480 |
+
const anyModalOpen = $('.modal-overlay:visible').length > 0;
|
481 |
+
if (anyModalOpen) return;
|
482 |
+
const container = $('.chess-board-container');
|
483 |
+
const containerWidth = container.width() - 16;
|
484 |
+
const containerHeight = container.height() - 16;
|
485 |
+
if (containerWidth <= 0 || containerHeight <= 0) return;
|
486 |
+
const maxSize = Math.min(containerWidth, containerHeight);
|
487 |
+
$('#game-board').css({ 'width': maxSize + 'px', 'height': maxSize + 'px' });
|
488 |
+
board.resize();
|
489 |
+
}
|
490 |
+
function startResizeObserver() {
|
491 |
+
const el = document.querySelector('.chess-board-container');
|
492 |
+
if (!el) return;
|
493 |
+
if (resizeObserver) resizeObserver.disconnect();
|
494 |
+
resizeObserver = new ResizeObserver(() => rafResize(ensureBoardFits));
|
495 |
+
resizeObserver.observe(el);
|
496 |
+
}
|
497 |
+
|
498 |
function setBoardTheme(themeName) {
|
499 |
const theme = THEMES[themeName];
|
500 |
boardThemeStyle.innerHTML = `
|
|
|
517 |
showNotification(`AI Model set to ${modelName.includes('pro') ? 'Pro (Powerful)' : 'Flash (Fast)'}.`, 3000);
|
518 |
}
|
519 |
|
520 |
+
function isAtLatest() {
|
521 |
+
return viewingMoveIndex === game.history().length - 1;
|
522 |
+
}
|
523 |
+
|
524 |
+
function safeSetEngineSearching(v) {
|
525 |
+
isEngineSearching = v;
|
526 |
+
updateStatus();
|
527 |
+
}
|
528 |
+
|
529 |
function onDragStart(source, piece) {
|
530 |
+
const canMove = !isGameOver && !isEngineSearching && !game.game_over() && game.turn() === playerColor && isAtLatest();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
531 |
return canMove;
|
532 |
}
|
533 |
+
|
534 |
function createEngineWorker() {
|
535 |
return fetch(STOCKFISH_WORKER_URL).then(res => res.text()).then(scriptText => {
|
536 |
const blob = new Blob([scriptText], { type: 'application/javascript' });
|
|
|
538 |
});
|
539 |
}
|
540 |
|
541 |
+
let searchTimeoutId = null;
|
542 |
+
function clearSearchTimeout() {
|
543 |
+
if (searchTimeoutId) { clearTimeout(searchTimeoutId); searchTimeoutId = null; }
|
544 |
+
}
|
545 |
+
|
546 |
function initializeEngine() {
|
547 |
createEngineWorker().then(worker => {
|
548 |
engine = worker;
|
|
|
555 |
updateStatus("Error: Could not load engine.");
|
556 |
});
|
557 |
}
|
558 |
+
|
559 |
function showNotification(message, duration = 6000) {
|
560 |
const notification = $('#ai-notification');
|
561 |
$('#ai-notification-text').text(message);
|
562 |
notification.addClass('show');
|
563 |
setTimeout(() => { notification.removeClass('show'); }, duration);
|
564 |
}
|
565 |
+
|
566 |
function handleEngineMessage(event) {
|
567 |
+
const message = event.data || '';
|
568 |
if (useLLM()) {
|
569 |
+
if (message.startsWith('info')) {
|
570 |
+
const m = message.match(/score\s(cp|mate)\s(-?\d+).*?\spv\s+([a-h][1-8][a-h][1-8][qrbn]?)/);
|
571 |
+
if (m) {
|
572 |
+
const type = m[1];
|
573 |
+
const val = parseInt(m[2], 10);
|
574 |
+
const move = m[3];
|
575 |
+
const score = type === 'mate' ? (val > 0 ? 100000 - Math.abs(val) : -100000 + Math.abs(val)) : val;
|
576 |
+
if (!topMovesWithEvals.some(x => x.move === move)) {
|
577 |
topMovesWithEvals.push({ move, score });
|
578 |
+
if (topMovesWithEvals.length > 8) topMovesWithEvals.length = 8;
|
579 |
}
|
580 |
}
|
581 |
}
|
582 |
if (message.startsWith('bestmove')) {
|
583 |
const bestMoveFallback = message.split(' ')[1];
|
584 |
+
const movesSnapshot = topMovesWithEvals.slice();
|
585 |
+
topMovesWithEvals = [];
|
586 |
+
decideMoveWithLLM(bestMoveFallback, movesSnapshot).catch(err => {
|
587 |
+
console.error('LLM error (outer):', err);
|
588 |
+
$('#ai-reasoning-text').text("Error with AI. Playing fallback.");
|
589 |
+
makeMoveOnBoard(bestMoveFallback);
|
590 |
+
safeSetEngineSearching(false);
|
591 |
+
});
|
592 |
+
clearSearchTimeout();
|
593 |
}
|
594 |
} else {
|
595 |
if (message.startsWith('bestmove')) {
|
596 |
+
const bestMove = message.split(' ')[1];
|
597 |
+
makeMoveOnBoard(bestMove);
|
598 |
+
safeSetEngineSearching(false);
|
599 |
+
topMovesWithEvals = [];
|
600 |
+
clearSearchTimeout();
|
601 |
}
|
602 |
}
|
603 |
}
|
604 |
|
605 |
+
async function decideMoveWithLLM(fallbackMove, movesSnapshot) {
|
606 |
+
try {
|
607 |
+
const apiKeyToUse = userApiKey || geminiApiKey;
|
608 |
+
if (!apiKeyToUse || apiKeyToUse.trim() === '') {
|
609 |
+
showNotification('API key is required for AI personality. Please set it in Settings.', 8000);
|
610 |
+
makeMoveOnBoard(fallbackMove);
|
611 |
+
safeSetEngineSearching(false);
|
612 |
+
return;
|
613 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
614 |
|
615 |
+
if (isGameOver || game.game_over()) {
|
616 |
+
safeSetEngineSearching(false);
|
617 |
+
return;
|
618 |
+
}
|
619 |
|
620 |
+
const movesData = Array.isArray(movesSnapshot) ? movesSnapshot : [];
|
621 |
+
if (movesData.length === 0) {
|
622 |
+
makeMoveOnBoard(fallbackMove);
|
623 |
+
safeSetEngineSearching(false);
|
624 |
+
return;
|
625 |
+
}
|
626 |
|
627 |
+
const fen = game.fen();
|
628 |
+
const turn = game.turn() === 'w' ? 'White' : 'Black';
|
629 |
+
const prompt = `You are a strong chess engine and role-player. Choose only from moves list.
|
630 |
+
Prioritize strength (80%) and personality (20%). Output strict JSON only.
|
631 |
|
632 |
+
State:
|
633 |
+
- FEN: ${fen}
|
634 |
+
- Turn: ${turn}
|
635 |
+
- Personality: "${aiPersonality}"
|
|
|
636 |
|
637 |
+
Moves (JSON array of {move, score}): ${JSON.stringify(movesData)}
|
|
|
638 |
|
639 |
+
Output JSON:
|
640 |
+
{
|
641 |
+
"bestMove": "e2e4",
|
642 |
+
"reasoning": "short in-character explanation",
|
643 |
+
"deviationWarning": null | "short reason if deviated"
|
644 |
+
}`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
645 |
|
646 |
+
updateStatus('Consulting AI personality...');
|
647 |
+
$('#ai-reasoning-text').text('Thinking...');
|
|
|
648 |
|
649 |
+
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${selectedGeminiModel}:generateContent?key=${apiKeyToUse}`, {
|
650 |
+
method: 'POST',
|
651 |
+
headers: { 'Content-Type': 'application/json' },
|
652 |
+
body: JSON.stringify({ "contents": [{ "parts": [{ "text": prompt }] }] })
|
653 |
+
});
|
654 |
|
655 |
+
if (response.status === 429) {
|
656 |
+
await new Promise(res => setTimeout(res, 1200 + Math.random()*500));
|
657 |
+
}
|
|
|
|
|
658 |
|
659 |
+
if (!response.ok) {
|
660 |
+
if ([400,401,403].includes(response.status)) {
|
661 |
+
showNotification('Invalid/unauthorized API key. Check Settings.', 8000);
|
662 |
+
} else {
|
663 |
+
showNotification('Error with AI service. Using best move.', 5000);
|
664 |
}
|
665 |
+
throw new Error(`API request failed with status ${response.status}`);
|
666 |
+
}
|
667 |
|
668 |
+
const data = await response.json();
|
669 |
+
const rawText = data?.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
670 |
+
const jsonMatch = rawText.match(/```json\s*([\s\S]*?)```/i) || rawText.match(/(\{[\s\S]*\})/);
|
671 |
+
if (!jsonMatch) throw new Error("LLM did not return a parsable JSON object.");
|
|
|
|
|
672 |
|
673 |
+
const result = JSON.parse(jsonMatch[1] || jsonMatch[0]);
|
674 |
+
if (!result.bestMove || typeof result.reasoning === 'undefined') {
|
675 |
+
throw new Error("Parsed JSON from LLM is missing required keys ('bestMove', 'reasoning').");
|
676 |
+
}
|
677 |
|
678 |
+
if (result.deviationWarning) showNotification(result.deviationWarning);
|
679 |
+
$('#ai-reasoning-text').text(result.reasoning || "Move chosen.");
|
|
|
|
|
|
|
|
|
|
|
|
|
680 |
|
681 |
+
const allowed = movesData.some(item => item.move === result.bestMove);
|
682 |
+
if (allowed) {
|
683 |
+
makeMoveOnBoard(result.bestMove);
|
684 |
+
} else {
|
685 |
+
console.warn(`LLM chose a move (${result.bestMove}) not in the valid list. Defaulting to best Stockfish move.`);
|
686 |
+
makeMoveOnBoard(fallbackMove);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
687 |
}
|
688 |
+
} catch (error) {
|
689 |
+
console.error("An error occurred while getting the AI's move:", error);
|
690 |
+
$('#ai-reasoning-text').text("AI error. Using fallback.");
|
691 |
+
makeMoveOnBoard(fallbackMove);
|
692 |
+
} finally {
|
693 |
+
safeSetEngineSearching(false);
|
694 |
}
|
695 |
}
|
696 |
+
|
697 |
+
function stopEngineSearch() {
|
698 |
+
if (!engine) return;
|
699 |
+
try { engine.postMessage('stop'); } catch (e) { console.warn('stop failed:', e); }
|
700 |
+
clearSearchTimeout();
|
701 |
+
}
|
702 |
+
|
703 |
function makeEngineMove() {
|
704 |
if (isGameOver || game.game_over() || !engine) return;
|
705 |
+
if (isEngineSearching) return;
|
706 |
+
safeSetEngineSearching(true);
|
707 |
updateStatus('Gemifish is thinking...');
|
708 |
|
709 |
topMovesWithEvals = [];
|
710 |
+
stopEngineSearch();
|
711 |
engine.postMessage('position fen ' + game.fen());
|
712 |
engine.postMessage('go depth 15');
|
713 |
+
|
714 |
+
clearSearchTimeout();
|
715 |
+
searchTimeoutId = setTimeout(() => {
|
716 |
+
console.warn('Engine search timeout; forcing stop.');
|
717 |
+
stopEngineSearch();
|
718 |
+
safeSetEngineSearching(false);
|
719 |
+
}, 12000);
|
720 |
}
|
721 |
|
722 |
function makeMoveOnBoard(move) {
|
723 |
+
if (!move || isGameOver) return;
|
724 |
+
const res = game.move(move, { sloppy: true });
|
725 |
+
if (!res) return;
|
726 |
board.position(game.fen());
|
|
|
727 |
viewingMoveIndex = game.history().length - 1;
|
|
|
728 |
updateMoveHistory();
|
729 |
+
updateStatus();
|
730 |
+
rafResize(ensureBoardFits);
|
731 |
}
|
732 |
|
733 |
function onDrop(source, target) {
|
734 |
+
if (isEngineSearching || !isAtLatest()) return 'snapback';
|
735 |
var move = game.move({ from: source, to: target, promotion: 'q' });
|
736 |
if (move === null) return 'snapback';
|
737 |
+
|
738 |
viewingMoveIndex = game.history().length - 1;
|
739 |
updateMoveHistory();
|
740 |
+
updateStatus();
|
741 |
+
|
742 |
if (game.game_over()) {
|
743 |
updateStatus();
|
744 |
+
safeSetEngineSearching(false);
|
745 |
} else {
|
746 |
+
window.setTimeout(makeEngineMove, 200);
|
747 |
}
|
748 |
}
|
749 |
|
750 |
function onSnapEnd() {
|
751 |
board.position(game.fen());
|
752 |
+
rafResize(ensureBoardFits);
|
753 |
}
|
754 |
+
|
755 |
function updateMoveHistory() {
|
756 |
const history = game.history({ verbose: false });
|
757 |
const moveList = $('#moves-list');
|
|
|
768 |
}
|
769 |
$('.moves-section').scrollTop($('.moves-section')[0].scrollHeight);
|
770 |
updateHighlighting();
|
771 |
+
updateNavButtons();
|
772 |
}
|
773 |
|
774 |
function updateHighlighting() {
|
775 |
$('.move').removeClass('highlighted-move');
|
776 |
if (viewingMoveIndex > -1) $('.move').eq(viewingMoveIndex).addClass('highlighted-move');
|
777 |
}
|
778 |
+
|
779 |
function navigateToMove(index) {
|
780 |
const history = game.history();
|
781 |
if (index < -1 || index >= history.length) return;
|
|
|
783 |
const tempGame = new Chess();
|
784 |
for (let i = 0; i <= viewingMoveIndex; i++) tempGame.move(history[i]);
|
785 |
board.position(tempGame.fen());
|
|
|
786 |
updateHighlighting();
|
787 |
updateNavButtons();
|
788 |
+
rafResize(ensureBoardFits);
|
789 |
}
|
790 |
|
791 |
function updateStatus(customStatus) {
|
|
|
805 |
}
|
806 |
if (game.game_over() || isGameOver) {
|
807 |
$('#resign-btn').prop('disabled', true);
|
808 |
+
} else {
|
809 |
+
$('#resign-btn').prop('disabled', false);
|
810 |
}
|
811 |
$status.html(status);
|
812 |
updateNavButtons();
|
813 |
}
|
814 |
+
|
815 |
function updateNavButtons() {
|
816 |
const historyLength = game.history().length;
|
817 |
const onLatestMove = viewingMoveIndex === historyLength - 1;
|
|
|
821 |
$('#btn-last').prop('disabled', onLatestMove);
|
822 |
}
|
823 |
|
|
|
824 |
function newGame() {
|
825 |
+
stopEngineSearch();
|
826 |
game = new Chess();
|
827 |
board.position('start');
|
828 |
viewingMoveIndex = -1;
|
829 |
isGameOver = false;
|
830 |
+
safeSetEngineSearching(false);
|
831 |
topMovesWithEvals = [];
|
832 |
updateMoveHistory();
|
833 |
updateStatus();
|
834 |
$('#ai-reasoning-text').text('Set a personality to see the AI\'s thoughts here.');
|
835 |
$('#resign-btn').prop('disabled', false);
|
836 |
showNotification('New game started!', 2000);
|
837 |
+
rafResize(ensureBoardFits);
|
838 |
}
|
839 |
|
840 |
function openShareModal() {
|
841 |
$('#fenInput').val(game.fen());
|
842 |
$('#pgnText').val(game.pgn());
|
843 |
$('#pgn-modal-overlay').css('display', 'flex');
|
844 |
+
rafResize(ensureBoardFits);
|
845 |
}
|
846 |
+
function closeShareModal() { $('#pgn-modal-overlay').css('display', 'none'); rafResize(ensureBoardFits); }
|
847 |
function openAIPersonalityModal() {
|
848 |
$('#ai-personality-input').val(aiPersonality);
|
849 |
$('#ai-modal-overlay').css('display', 'flex');
|
850 |
+
rafResize(ensureBoardFits);
|
851 |
}
|
852 |
+
function closeAIPersonalityModal() { $('#ai-modal-overlay').css('display', 'none'); rafResize(ensureBoardFits); }
|
853 |
function saveAIPersonality() {
|
854 |
aiPersonality = $('#ai-personality-input').val();
|
855 |
if (useLLM()) {
|
|
|
885 |
document.body.removeChild(a); URL.revokeObjectURL(url);
|
886 |
}
|
887 |
|
|
|
888 |
function openSettingsModal() {
|
889 |
$('#api-key-input').val(userApiKey);
|
890 |
$('#api-key-warning').hide();
|
891 |
$('#settings-modal-overlay').css('display', 'flex');
|
892 |
+
rafResize(ensureBoardFits);
|
893 |
}
|
894 |
+
function closeSettingsModal() { $('#settings-modal-overlay').css('display', 'none'); rafResize(ensureBoardFits); }
|
895 |
|
896 |
var config = {
|
897 |
draggable: true,
|
|
|
903 |
showNotation: true
|
904 |
};
|
905 |
board = Chessboard('game-board', config);
|
906 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
907 |
ensureBoardFits();
|
908 |
+
$(window).on('resize', () => rafResize(ensureBoardFits));
|
909 |
+
startResizeObserver();
|
|
|
910 |
|
911 |
// Event Handlers
|
912 |
$('#new-game-btn').on('click', newGame);
|
913 |
+
$('#resign-btn').on('click', () => {
|
914 |
+
if (!isGameOver && !game.game_over()) {
|
915 |
+
isGameOver = true;
|
916 |
+
stopEngineSearch();
|
917 |
+
safeSetEngineSearching(false);
|
918 |
+
updateStatus();
|
919 |
+
}
|
920 |
+
});
|
921 |
$('#btn-back').on('click', () => navigateToMove(viewingMoveIndex - 1));
|
922 |
$('#btn-forward').on('click', () => navigateToMove(viewingMoveIndex + 1));
|
923 |
$('#btn-first').on('click', () => navigateToMove(-1));
|
924 |
$('#btn-last').on('click', () => navigateToMove(game.history().length - 1));
|
925 |
+
|
926 |
$('#share-btn').on('click', openShareModal);
|
927 |
$('#modal-close-btn').on('click', function(e) { e.stopPropagation(); closeShareModal(); });
|
928 |
$('#pgn-modal-overlay').on('click', function(e) { if (e.target === this || $(e.target).is('#modal-close-btn, #modal-close-btn *')) closeShareModal(); });
|
929 |
+
|
930 |
$('#ai-btn').on('click', openAIPersonalityModal);
|
931 |
$('#ai-modal-close-btn').on('click', function(e) { e.stopPropagation(); closeAIPersonalityModal(); });
|
932 |
$('#ai-modal-overlay').on('click', function(e) { if (e.target === this || $(e.target).is('#ai-modal-close-btn, #ai-modal-close-btn *')) closeAIPersonalityModal(); });
|
|
|
936 |
$('#copy-fen-btn').on('click', function() { copyToClipboard('fenInput', this); });
|
937 |
$('#copy-pgn-btn').on('click', function() { copyToClipboard('pgnText', this); });
|
938 |
$('#download-pgn-btn').on('click', downloadPGN);
|
939 |
+
|
940 |
$('#settings-btn').on('click', function(e) {
|
941 |
e.stopPropagation();
|
942 |
$('#settings-popover').toggle();
|
943 |
});
|
944 |
+
|
945 |
$(document).on('click', function() {
|
946 |
$('#settings-popover').hide();
|
947 |
});
|
948 |
+
|
949 |
$('#settings-popover').on('click', e => e.stopPropagation());
|
950 |
+
|
951 |
$('#api-settings-btn').on('click', function() {
|
952 |
$('#settings-popover').hide();
|
953 |
openSettingsModal();
|
954 |
});
|
955 |
+
|
956 |
$('#settings-modal-close-btn').on('click', function(e) { e.stopPropagation(); closeSettingsModal(); });
|
957 |
$('#settings-modal-overlay').on('click', function(e) { if (e.target === this || $(e.target).is('#settings-modal-close-btn, #settings-modal-close-btn *')) closeSettingsModal(); });
|
958 |
+
|
959 |
$('#save-settings-btn').on('click', () => {
|
960 |
const newApiKey = $('#api-key-input').val().trim();
|
|
|
|
|
961 |
if (!newApiKey || newApiKey.length < 10) {
|
962 |
$('#api-key-warning').show();
|
963 |
return;
|
964 |
}
|
|
|
965 |
userApiKey = newApiKey;
|
966 |
$('#api-key-warning').hide();
|
967 |
closeSettingsModal();
|
968 |
showNotification('API key saved successfully!', 3000);
|
|
|
|
|
969 |
if (aiPersonality && aiPersonality.trim() !== '') {
|
970 |
$('#ai-btn').addClass('active-ai');
|
971 |
$('#ai-reasoning-text').text('Personality set. Make your move!');
|
972 |
}
|
973 |
});
|
974 |
+
|
975 |
// Theme buttons in popover
|
976 |
$('#theme-brown-btn').on('click', () => setBoardTheme('brown'));
|
977 |
$('#theme-blue-btn').on('click', () => setBoardTheme('blue'));
|
978 |
+
|
979 |
// Theme buttons in modal
|
980 |
$('#theme-brown-btn-modal').on('click', () => setBoardTheme('brown'));
|
981 |
$('#theme-blue-btn-modal').on('click', () => setBoardTheme('blue'));
|
982 |
+
|
983 |
// Model selection buttons
|
984 |
$('#model-flash-btn').on('click', () => setAIModel('gemini-1.5-flash-latest'));
|
985 |
$('#model-pro-btn').on('click', () => setAIModel('gemini-1.5-pro-latest'));
|
986 |
+
|
987 |
setBoardTheme('blue');
|
988 |
initializeEngine();
|
989 |
</script>
|