llamameta commited on
Commit
e2a7bbc
·
verified ·
1 Parent(s): edbde0b

Update index.html

Browse files
Files changed (1) hide show
  1. 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 = ''; // Default API key dihapus agar user harus memasukkan
458
- var userApiKey = ''; // User-provided key
459
  var isEngineSearching = false;
460
  var topMovesWithEvals = [];
461
- var selectedGeminiModel = 'gemini-1.5-flash-latest'; // Default model
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 && viewingMoveIndex === game.history().length - 1;
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') && message.includes('score cp')) {
540
- const scoreMatch = message.match(/score cp (-?\d+)/);
541
- const moveMatch = message.match(/pv\s+([a-h][1-8][a-h][1-8][qrbn]?)/);
542
- if (scoreMatch && moveMatch) {
543
- const score = parseInt(scoreMatch[1], 10);
544
- const move = moveMatch[1];
545
- if (!topMovesWithEvals.some(item => item.move === move)) {
 
546
  topMovesWithEvals.push({ move, score });
 
547
  }
548
  }
549
  }
550
  if (message.startsWith('bestmove')) {
551
  const bestMoveFallback = message.split(' ')[1];
552
- decideMoveWithLLM(bestMoveFallback);
 
 
 
 
 
 
 
 
553
  }
554
  } else {
555
  if (message.startsWith('bestmove')) {
556
- isEngineSearching = false;
557
- makeMoveOnBoard(message.split(' ')[1]);
 
 
 
558
  }
559
  }
560
  }
561
 
562
- async function decideMoveWithLLM(fallbackMove) {
563
- const apiKeyToUse = userApiKey || geminiApiKey; // Use user key if provided
564
-
565
- // Check if API key is available
566
- if (!apiKeyToUse || apiKeyToUse.trim() === '') {
567
- console.error("API key is missing or empty");
568
- showNotification('API key is required for AI personality. Please set it in Settings.', 8000);
569
- isEngineSearching = false;
570
- makeMoveOnBoard(fallbackMove);
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
- Current State:
593
- - Board (FEN): ${fen}
594
- - It is ${turn}'s turn to move.
595
- - Your designated personality is: "${aiPersonality}"
596
 
597
- Your Task:
598
- I have provided a JSON array of the top moves calculated by a strong chess engine. First, identify the strongest moves that don't lead to a disadvantage. From that small group of good moves, pick the one that best reflects your character. Then, explain your choice from your character's perspective.
 
 
 
 
599
 
600
- Input Moves Data:
601
- ${movesData}
 
 
602
 
603
- Output Format:
604
- You MUST respond in a VALID JSON FORMAT, with no other text. The JSON object must have three keys:
605
- - "bestMove": The chosen move from the list (e.g., "e2e4"). Required.
606
- - "reasoning": A brief, in-character explanation for your choice. DO NOT mention scores or technical analysis. This should be your character's internal monologue. Required.
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
- updateStatus('Consulting AI personality...');
610
- $('#ai-reasoning-text').text('Thinking...');
611
 
612
- const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${selectedGeminiModel}:generateContent?key=${apiKeyToUse}`, {
613
- method: 'POST',
614
- headers: { 'Content-Type': 'application/json' },
615
- body: JSON.stringify({ "contents": [{ "parts": [{ "text": prompt }] }] })
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
- if (!response.ok) {
627
- throw new Error(`API request failed with status ${response.status}`);
628
- }
629
 
630
- const data = await response.json();
 
 
 
 
631
 
632
- if (!data.candidates || !data.candidates[0]?.content?.parts[0]?.text) {
633
- const reason = data.candidates?.[0]?.finishReason || 'Unknown reason';
634
- console.error("Invalid response structure from Gemini API. Content may be blocked or missing. Finish Reason:", reason, data);
635
- throw new Error(`Invalid response structure from Gemini API. Finish Reason: ${reason}`);
636
- }
637
 
638
- let rawText = data.candidates[0].content.parts[0].text;
639
- const jsonMatch = rawText.match(/```json\s*(\{[\s\S]*\})\s*```|(\{[\s\S]*\})/);
640
- if (!jsonMatch) {
641
- throw new Error("LLM did not return a parsable JSON object.");
 
642
  }
 
 
643
 
644
- const jsonString = jsonMatch[1] || jsonMatch[2];
645
- const result = JSON.parse(jsonString);
646
-
647
- if (!result.bestMove || typeof result.reasoning === 'undefined') {
648
- throw new Error("Parsed JSON from LLM is missing required keys ('bestMove', 'reasoning').");
649
- }
650
 
651
- if (result.deviationWarning) {
652
- showNotification(result.deviationWarning);
653
- }
654
- $('#ai-reasoning-text').text(result.reasoning || "I have made my move.");
655
 
656
- if (topMovesWithEvals.some(item => item.move === result.bestMove)) {
657
- makeMoveOnBoard(result.bestMove);
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
- } catch (error) {
666
- console.error("An error occurred while getting the AI's move:", error);
667
-
668
- // If we've exhausted retries, give up and use the fallback.
669
- if (retryCount >= maxRetries - 1) {
670
- $('#ai-reasoning-text').text("An error occurred. I will play the best move as a fallback.");
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 = true;
 
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
- game.move(move, { sloppy: true });
 
 
700
  board.position(game.fen());
701
- board.resize();
702
  viewingMoveIndex = game.history().length - 1;
703
- updateStatus();
704
  updateMoveHistory();
705
- if (game.game_over()) updateStatus();
 
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, 250);
719
  }
720
  }
721
 
722
  function onSnapEnd() {
723
  board.position(game.fen());
724
- setTimeout(() => board.resize(), 10);
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
- isEngineSearching = false;
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
- // Panggil fungsi saat window di-resize
893
- $(window).on('resize', ensureBoardFits);
894
 
895
  // Event Handlers
896
  $('#new-game-btn').on('click', newGame);
897
- $('#resign-btn').on('click', () => { if (!isGameOver && !game.game_over()) { isGameOver = true; updateStatus(); }});
 
 
 
 
 
 
 
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>