Changeset 3415333
- Timestamp:
- 12/09/2025 12:08:38 PM (2 months ago)
- Location:
- askeet/assets
- Files:
-
- 2 edited
-
css/style.css (modified) (3 diffs)
-
js/script.js (modified) (14 diffs)
Legend:
- Unmodified
- Added
- Removed
-
askeet/assets/css/style.css
r3323653 r3415333 45 45 } 46 46 47 .btn { 48 color: white; 47 .wp-core-ui .button-group { 48 font-size: unset !important; 49 } 50 51 .btn, 52 a.btn, 53 .button-group a.btn, 54 .button-group .btn { 55 color: #ffffff !important; 49 56 border: none; 50 padding: 32px 34px;57 padding: 17px 34px; 51 58 cursor: pointer; 52 59 font-weight: 600; … … 54 61 text-align: center; 55 62 min-width: 160px; 56 transition: background0.3s ease;63 transition: all 0.3s ease; 57 64 border-radius: 10px; 58 background: linear-gradient(180deg, rgba(0, 90, 158, 0.74) 0%, #005A9E 100%); 59 } 60 61 .btn:hover { 62 background: linear-gradient(90deg, #004d8c 74%, #004d8c 100%); 65 background: linear-gradient(135deg, #005a9e 0%, #0077cc 100%) !important; 66 text-decoration: none !important; 67 display: inline-flex; 68 align-items: center; 69 justify-content: center; 70 box-shadow: 0 4px 12px rgba(0, 90, 158, 0.25); 71 } 72 73 .btn:hover, 74 a.btn:hover, 75 .button-group a.btn:hover, 76 .button-group .btn:hover { 77 background: linear-gradient(135deg, #004080 0%, #005fa3 100%) !important; 78 color: #ffffff !important; 79 text-decoration: none !important; 80 transform: translateY(-2px); 81 box-shadow: 0 6px 20px rgba(0, 90, 158, 0.35); 82 } 83 84 .btn:focus, 85 a.btn:focus { 86 outline: none; 87 box-shadow: 0 0 0 3px rgba(0, 90, 158, 0.3); 88 } 89 90 /* Specific button overrides for WordPress admin compatibility */ 91 #btn-discord, 92 #btn-slack, 93 #btn-support, 94 .askeet-guide-wrapper #btn-discord, 95 .askeet-guide-wrapper #btn-slack, 96 .askeet-guide-wrapper #btn-support, 97 .askeet-guide-wrapper .button-group a.btn, 98 .askeet-guide-wrapper .button-group .btn, 99 .wrap.askeet .button-group a.btn, 100 .wrap.askeet .button-group .btn, 101 body.wp-admin #btn-discord, 102 body.wp-admin #btn-slack, 103 body.wp-admin #btn-support { 104 color: #ffffff !important; 105 background: linear-gradient(135deg, #302e2e 0%, #90979d 100%) !important; 106 -webkit-text-fill-color: #ffffff !important; 107 text-shadow: none !important; 108 } 109 110 #btn-discord:visited, 111 #btn-slack:visited, 112 #btn-support:visited, 113 .askeet-guide-wrapper a.btn:visited, 114 .button-group a.btn:visited { 115 color: #ffffff !important; 116 -webkit-text-fill-color: #ffffff !important; 63 117 } 64 118 … … 528 582 margin-bottom: 8px; 529 583 } 584 585 /* Top controls bar - compact and attractive */ 586 .wcqa-top-controls { 587 display: flex; 588 justify-content: space-between; 589 align-items: center; 590 background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); 591 padding: 10px 16px; 592 border-radius: 8px; 593 margin-bottom: 12px; 594 border: 1px solid #e3e8ef; 595 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); 596 } 597 598 .wcqa-rows-selector { 599 display: flex; 600 align-items: center; 601 gap: 8px; 602 } 603 604 .wcqa-rows-selector label { 605 font-weight: 600; 606 color: #555; 607 font-size: 0.85em; 608 white-space: nowrap; 609 } 610 611 .wcqa-per-page-dropdown { 612 padding: 5px 10px; 613 border: 1.5px solid #d0d5dd; 614 border-radius: 5px; 615 font-size: 0.85em; 616 background: white; 617 cursor: pointer; 618 min-width: 70px; 619 font-weight: 600; 620 color: #444; 621 transition: all 0.2s; 622 } 623 624 .wcqa-per-page-dropdown:hover { 625 border-color: #005a9e; 626 background: #f0f7ff; 627 } 628 629 .wcqa-per-page-dropdown:focus { 630 outline: none; 631 border-color: #005a9e; 632 box-shadow: 0 0 0 2px rgba(0, 90, 158, 0.1); 633 } 634 635 .wcqa-export-controls-top { 636 display: flex; 637 gap: 8px; 638 align-items: center; 639 } 640 641 .wcqa-export-btn-compact { 642 display: inline-flex !important; 643 align-items: center; 644 gap: 5px; 645 padding: 6px 12px !important; 646 font-size: 0.80em !important; 647 font-weight: 600 !important; 648 border-radius: 5px !important; 649 transition: all 0.2s ease; 650 white-space: nowrap; 651 height: auto !important; 652 line-height: 1.2 !important; 653 } 654 655 .wcqa-export-btn-compact .dashicons { 656 font-size: 16px; 657 width: 16px; 658 height: 16px; 659 margin-right: 0; 660 } 661 662 .wcqa-export-btn-compact { 663 background: white !important; 664 border: 1.5px solid #005a9e !important; 665 color: #005a9e !important; 666 } 667 668 .wcqa-export-btn-compact:hover { 669 background: #005a9e !important; 670 color: white !important; 671 transform: translateY(-1px); 672 box-shadow: 0 2px 6px rgba(0, 90, 158, 0.2); 673 } 674 675 .wcqa-export-all-btn { 676 background: #005a9e !important; 677 border: 1.5px solid #005a9e !important; 678 color: white !important; 679 } 680 681 .wcqa-export-all-btn:hover { 682 background: #003d6b !important; 683 border-color: #003d6b !important; 684 box-shadow: 0 3px 8px rgba(0, 90, 158, 0.3); 685 } 686 687 .wcqa-export-btn-compact:disabled { 688 opacity: 0.6; 689 cursor: not-allowed; 690 transform: none !important; 691 } 692 693 .wcqa-export-warning-top { 694 background: #fff8e1; 695 border-left: 3px solid #ffc107; 696 color: #856404; 697 padding: 8px 12px; 698 border-radius: 4px; 699 font-size: 0.85em; 700 margin-bottom: 10px; 701 display: flex; 702 align-items: center; 703 gap: 6px; 704 } 705 706 /* Performance notice - beautiful and clear */ 707 .wcqa-performance-notice { 708 background: linear-gradient(135deg, #e8f5e9 0%, #f1f8e9 100%); 709 border-left: 4px solid #4caf50; 710 border-radius: 8px; 711 padding: 14px 40px 14px 16px; 712 margin-bottom: 16px; 713 display: flex; 714 align-items: flex-start; 715 gap: 12px; 716 box-shadow: 0 2px 8px rgba(76, 175, 80, 0.15); 717 animation: slideInFromTop 0.4s ease-out; 718 position: relative; 719 } 720 721 @keyframes slideInFromTop { 722 from { 723 opacity: 0; 724 transform: translateY(-10px); 725 } 726 to { 727 opacity: 1; 728 transform: translateY(0); 729 } 730 } 731 732 .wcqa-notice-icon { 733 font-size: 1.5em; 734 line-height: 1; 735 flex-shrink: 0; 736 } 737 738 .wcqa-notice-content { 739 flex: 1; 740 font-size: 0.9em; 741 line-height: 1.5; 742 color: #2e7d32; 743 } 744 745 .wcqa-notice-content strong { 746 font-weight: 700; 747 color: #1b5e20; 748 } 749 750 .wcqa-notice-close { 751 position: absolute; 752 top: 8px; 753 right: 8px; 754 background: none; 755 border: none; 756 font-size: 1.5em; 757 line-height: 1; 758 color: #4caf50; 759 cursor: pointer; 760 padding: 4px 8px; 761 border-radius: 4px; 762 transition: all 0.2s; 763 } 764 765 .wcqa-notice-close:hover { 766 background: rgba(76, 175, 80, 0.15); 767 color: #2e7d32; 768 } 769 770 /* Responsive design */ 771 @media (max-width: 768px) { 772 .wcqa-top-controls { 773 flex-direction: column; 774 gap: 10px; 775 align-items: stretch; 776 } 777 778 .wcqa-rows-selector { 779 justify-content: space-between; 780 width: 100%; 781 } 782 783 .wcqa-export-controls-top { 784 width: 100%; 785 justify-content: space-between; 786 } 787 788 .wcqa-export-btn-compact { 789 flex: 1; 790 justify-content: center; 791 } 792 793 .wcqa-performance-notice { 794 padding: 12px 14px; 795 } 796 797 .wcqa-notice-content { 798 font-size: 0.85em; 799 } 800 } 801 802 @media (max-width: 480px) { 803 .wcqa-export-controls-top { 804 flex-direction: column; 805 gap: 6px; 806 } 807 808 .wcqa-export-btn-compact { 809 width: 100%; 810 } 811 812 .wcqa-performance-notice { 813 padding: 10px 12px; 814 gap: 8px; 815 } 816 817 .wcqa-notice-icon { 818 font-size: 1.2em; 819 } 820 821 .wcqa-notice-content { 822 font-size: 0.8em; 823 } 824 } 825 826 /* Subscription Page - Modern Pricing Cards */ 827 .askeet-subscription-page { 828 max-width: 1200px; 829 margin: 30px auto; 830 padding: 0 20px; 831 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; 832 } 833 834 .askeet-sub-header { 835 text-align: center; 836 margin-bottom: 50px; 837 } 838 839 .askeet-sub-header h1 { 840 font-size: 2.5em; 841 font-weight: 700; 842 color: #1a1a2e; 843 margin-bottom: 12px; 844 } 845 846 .askeet-sub-header p { 847 font-size: 1.2em; 848 color: #666; 849 } 850 851 /* Subscription Info Badge */ 852 .askeet-subscription-info { 853 display: flex; 854 justify-content: center; 855 margin-top: 24px; 856 } 857 858 .subscription-info-badge { 859 display: inline-flex; 860 align-items: center; 861 gap: 12px; 862 background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%); 863 border: 2px solid #005a9e; 864 border-radius: 16px; 865 padding: 16px 28px; 866 box-shadow: 0 4px 12px rgba(0, 90, 158, 0.15); 867 } 868 869 .info-icon { 870 font-size: 2em; 871 line-height: 1; 872 } 873 874 .info-content { 875 display: flex; 876 flex-direction: column; 877 gap: 4px; 878 } 879 880 .info-label { 881 font-size: 0.85em; 882 color: #666; 883 font-weight: 500; 884 text-transform: uppercase; 885 letter-spacing: 0.5px; 886 } 887 888 .info-value { 889 font-size: 1.3em; 890 color: #005a9e; 891 font-weight: 700; 892 } 893 894 /* Billing Toggle (Monthly/Yearly) */ 895 .askeet-billing-toggle-wrapper { 896 display: flex; 897 justify-content: center; 898 margin-bottom: 40px; 899 } 900 901 .askeet-billing-toggle { 902 display: inline-flex; 903 background: #f5f7fa; 904 border-radius: 12px; 905 padding: 6px; 906 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); 907 } 908 909 .billing-option { 910 display: flex; 911 align-items: center; 912 gap: 8px; 913 padding: 12px 24px; 914 border: none; 915 background: transparent; 916 color: #666; 917 font-size: 16px; 918 font-weight: 600; 919 border-radius: 8px; 920 cursor: pointer; 921 transition: all 0.3s ease; 922 position: relative; 923 } 924 925 .billing-option:hover { 926 color: #005a9e; 927 } 928 929 .billing-option.active { 930 background: white; 931 color: #005a9e; 932 box-shadow: 0 2px 6px rgba(0, 90, 158, 0.15); 933 } 934 935 .save-badge { 936 display: inline-block; 937 background: linear-gradient(135deg, #4caf50 0%, #45a049 100%); 938 color: white; 939 font-size: 11px; 940 font-weight: 700; 941 padding: 3px 8px; 942 border-radius: 12px; 943 text-transform: uppercase; 944 letter-spacing: 0.5px; 945 box-shadow: 0 2px 4px rgba(76, 175, 80, 0.3); 946 } 947 948 .askeet-pricing-cards { 949 display: grid; 950 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 951 gap: 30px; 952 margin-bottom: 50px; 953 } 954 955 .askeet-price-card { 956 background: white; 957 border: 2px solid #e0e6ed; 958 border-radius: 16px; 959 padding: 32px 28px; 960 position: relative; 961 transition: all 0.3s ease; 962 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); 963 display: flex; 964 flex-direction: column; 965 min-height: 100%; 966 } 967 968 .askeet-price-card:hover { 969 transform: translateY(-5px); 970 box-shadow: 0 12px 24px rgba(0, 90, 158, 0.15); 971 border-color: #005a9e; 972 } 973 974 .askeet-price-card.current-plan { 975 border: 3px solid #4caf50; 976 background: linear-gradient(135deg, #f8fff9 0%, #ffffff 100%); 977 } 978 979 .askeet-price-card.current-plan:hover { 980 transform: none; 981 box-shadow: 0 8px 16px rgba(76, 175, 80, 0.2); 982 } 983 984 .plan-badge { 985 display: inline-block; 986 padding: 6px 16px; 987 background: #e0e6ed; 988 color: #555; 989 font-size: 0.85em; 990 font-weight: 700; 991 border-radius: 20px; 992 text-transform: uppercase; 993 letter-spacing: 0.5px; 994 margin-bottom: 20px; 995 } 996 997 .plan-badge.popular { 998 background: linear-gradient(135deg, #4f8cff 0%, #6f6fff 100%); 999 color: white; 1000 } 1001 1002 .plan-badge.premium { 1003 background: linear-gradient(135deg, #ff7e5f 0%, #feb47b 100%); 1004 color: white; 1005 } 1006 1007 .current-plan .plan-badge { 1008 background: #4caf50; 1009 color: white; 1010 } 1011 1012 .plan-name { 1013 font-size: 1.8em; 1014 font-weight: 700; 1015 color: #1a1a2e; 1016 margin: 16px 0; 1017 } 1018 1019 .plan-price { 1020 margin: 20px 0 30px 0; 1021 } 1022 1023 .plan-price .price { 1024 font-size: 3em; 1025 font-weight: 800; 1026 color: #1a1a2e; 1027 line-height: 1; 1028 } 1029 1030 .plan-price .period { 1031 font-size: 1.1em; 1032 color: #888; 1033 font-weight: 500; 1034 } 1035 1036 /* Price Row - keeps price and period inline */ 1037 .price-row { 1038 display: inline-flex; 1039 align-items: baseline; 1040 gap: 4px; 1041 } 1042 1043 /* Yearly Price Comparison */ 1044 .yearly-price-wrapper { 1045 display: flex; 1046 flex-direction: column; 1047 align-items: center; 1048 gap: 8px; 1049 } 1050 1051 .price-compare { 1052 font-size: 1.4em; 1053 font-weight: 600; 1054 color: #999; 1055 text-decoration: line-through; 1056 opacity: 0.7; 1057 } 1058 1059 .yearly-price-wrapper .price.yearly-price { 1060 font-size: 3em; 1061 font-weight: 800; 1062 color: #4caf50; 1063 } 1064 1065 .yearly-price-wrapper .period.yearly-period { 1066 font-size: 1.1em; 1067 color: #888; 1068 font-weight: 500; 1069 } 1070 1071 .plan-features { 1072 list-style: none; 1073 padding: 0; 1074 margin: 0 0 30px 0; 1075 flex-grow: 1; 1076 } 1077 1078 .plan-features li { 1079 padding: 12px 0; 1080 border-bottom: 1px solid #f0f0f0; 1081 color: #444; 1082 font-size: 1em; 1083 display: flex; 1084 align-items: center; 1085 gap: 12px; 1086 } 1087 1088 .plan-features li:last-child { 1089 border-bottom: none; 1090 } 1091 1092 .feature-icon { 1093 display: inline-flex; 1094 align-items: center; 1095 justify-content: center; 1096 width: 24px; 1097 height: 24px; 1098 background: #e8f5e9; 1099 color: #4caf50; 1100 border-radius: 50%; 1101 font-weight: 900; 1102 font-size: 0.9em; 1103 flex-shrink: 0; 1104 } 1105 1106 .queries-remaining { 1107 background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%); 1108 padding: 12px 16px; 1109 border-radius: 8px; 1110 text-align: center; 1111 font-weight: 700; 1112 color: #e65100; 1113 margin-bottom: 20px; 1114 font-size: 0.95em; 1115 margin-top: auto; 1116 } 1117 1118 .plan-button { 1119 width: 100%; 1120 padding: 16px 24px; 1121 font-size: 1.1em; 1122 font-weight: 700; 1123 border: none; 1124 border-radius: 10px; 1125 cursor: pointer; 1126 transition: all 0.3s ease; 1127 font-family: inherit; 1128 margin-top: auto; 1129 } 1130 1131 .plan-button.upgrade { 1132 background: linear-gradient(135deg, #005a9e 0%, #003d6b 100%); 1133 color: white; 1134 } 1135 1136 .plan-button.upgrade:hover { 1137 background: linear-gradient(135deg, #003d6b 0%, #002a4a 100%); 1138 transform: translateY(-2px); 1139 box-shadow: 0 6px 16px rgba(0, 90, 158, 0.3); 1140 } 1141 1142 .plan-button.change { 1143 background: linear-gradient(135deg, #4f8cff 0%, #6f6fff 100%); 1144 color: white; 1145 } 1146 1147 .plan-button.change:hover { 1148 background: linear-gradient(135deg, #3a7ae0 0%, #5555e0 100%); 1149 transform: translateY(-2px); 1150 box-shadow: 0 6px 16px rgba(79, 140, 255, 0.3); 1151 } 1152 1153 .plan-button.current { 1154 background: #e8f5e9; 1155 color: #4caf50; 1156 cursor: not-allowed; 1157 opacity: 0.8; 1158 } 1159 1160 .plan-button.downgrade { 1161 background: #f5f5f5; 1162 color: #999; 1163 cursor: not-allowed; 1164 opacity: 0.6; 1165 } 1166 1167 .plan-button:disabled { 1168 cursor: not-allowed; 1169 opacity: 0.7; 1170 } 1171 1172 .askeet-manage-billing { 1173 text-align: center; 1174 margin: 40px 0; 1175 } 1176 1177 .manage-billing-btn { 1178 display: inline-flex; 1179 align-items: center; 1180 gap: 12px; 1181 padding: 16px 32px; 1182 background: linear-gradient(135deg, #005a9e 0%, #003d6b 100%); 1183 color: white; 1184 text-decoration: none; 1185 font-size: 1.1em; 1186 font-weight: 700; 1187 border-radius: 10px; 1188 transition: all 0.3s ease; 1189 } 1190 1191 .manage-billing-btn:hover { 1192 background: linear-gradient(135deg, #003d6b 0%, #002a4a 100%); 1193 transform: translateY(-2px); 1194 box-shadow: 0 6px 16px rgba(0, 90, 158, 0.3); 1195 color: white; 1196 } 1197 1198 .manage-billing-btn .icon { 1199 font-size: 1.3em; 1200 } 1201 1202 .askeet-sub-footer { 1203 text-align: center; 1204 margin-top: 60px; 1205 padding-top: 30px; 1206 border-top: 1px solid #e0e6ed; 1207 } 1208 1209 .askeet-sub-footer p { 1210 color: #888; 1211 font-size: 0.95em; 1212 margin: 8px 0; 1213 } 1214 1215 /* Responsive */ 1216 @media (max-width: 900px) { 1217 .askeet-pricing-cards { 1218 grid-template-columns: 1fr; 1219 max-width: 500px; 1220 margin-left: auto; 1221 margin-right: auto; 1222 } 1223 1224 .askeet-sub-header h1 { 1225 font-size: 2em; 1226 } 1227 1228 .plan-price .price { 1229 font-size: 2.5em; 1230 } 1231 } 1232 1233 @media (max-width: 600px) { 1234 .askeet-subscription-page { 1235 padding: 0 15px; 1236 } 1237 1238 .askeet-sub-header h1 { 1239 font-size: 1.8em; 1240 } 1241 1242 .askeet-price-card { 1243 padding: 24px 20px; 1244 } 1245 1246 .plan-price .price { 1247 font-size: 2.2em; 1248 } 1249 } 1250 1251 /* ======================================== 1252 FEEDBACK MODAL STYLES 1253 ======================================== */ 1254 1255 /* Feedback Trigger Button */ 1256 .askeet-feedback-trigger { 1257 display: inline-flex; 1258 align-items: center; 1259 gap: 8px; 1260 padding: 10px 20px; 1261 background: linear-gradient(135deg, #005a9e 0%, #0077cc 100%); 1262 color: #ffffff !important; 1263 border: none; 1264 border-radius: 10px; 1265 font-size: 14px; 1266 font-weight: 600; 1267 cursor: pointer; 1268 transition: all 0.3s ease; 1269 box-shadow: 0 4px 12px rgba(0, 90, 158, 0.25); 1270 } 1271 1272 .askeet-feedback-trigger:hover { 1273 background: linear-gradient(135deg, #004080 0%, #005fa3 100%); 1274 transform: translateY(-2px); 1275 box-shadow: 0 6px 20px rgba(0, 90, 158, 0.35); 1276 } 1277 1278 .askeet-feedback-trigger svg { 1279 stroke: #ffffff; 1280 } 1281 1282 /* Feedback Modal Overlay */ 1283 .askeet-feedback-overlay { 1284 display: none; 1285 position: fixed; 1286 top: 0; 1287 left: 0; 1288 width: 100%; 1289 height: 100%; 1290 background: rgba(0, 0, 0, 0.6); 1291 backdrop-filter: blur(8px); 1292 z-index: 999999; 1293 opacity: 0; 1294 transition: opacity 0.3s ease; 1295 } 1296 1297 .askeet-feedback-overlay.active { 1298 display: flex; 1299 align-items: center; 1300 justify-content: center; 1301 opacity: 1; 1302 } 1303 1304 /* Feedback Modal */ 1305 .askeet-feedback-modal { 1306 background: #ffffff; 1307 border-radius: 16px; 1308 max-width: 520px; 1309 width: 90%; 1310 box-shadow: 0 25px 80px rgba(0, 0, 0, 0.25); 1311 transform: scale(0.9) translateY(20px); 1312 opacity: 0; 1313 transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); 1314 overflow: hidden; 1315 } 1316 1317 .askeet-feedback-overlay.active .askeet-feedback-modal { 1318 transform: scale(1) translateY(0); 1319 opacity: 1; 1320 } 1321 1322 /* Modal Header */ 1323 .askeet-feedback-header { 1324 position: relative; 1325 padding: 24px 24px 16px; 1326 border-bottom: 1px solid #f0f0f0; 1327 } 1328 1329 .askeet-feedback-header h2 { 1330 font-size: 24px; 1331 font-weight: 700; 1332 color: #1a1a1a; 1333 margin: 0; 1334 text-align: center; 1335 } 1336 1337 .askeet-feedback-close { 1338 position: absolute; 1339 top: 20px; 1340 right: 20px; 1341 background: transparent; 1342 border: none; 1343 padding: 8px; 1344 cursor: pointer; 1345 border-radius: 50%; 1346 transition: background 0.2s ease; 1347 } 1348 1349 .askeet-feedback-close:hover { 1350 background: #f0f0f0; 1351 } 1352 1353 .askeet-feedback-close svg { 1354 stroke: #666; 1355 display: block; 1356 } 1357 1358 /* Modal Body */ 1359 .askeet-feedback-body { 1360 padding: 24px; 1361 } 1362 1363 /* Info Box */ 1364 .askeet-feedback-info { 1365 display: flex; 1366 align-items: center; 1367 gap: 12px; 1368 padding: 14px 16px; 1369 background: linear-gradient(135deg, #e8f4fd 0%, #f0f7ff 100%); 1370 border-radius: 10px; 1371 margin-bottom: 24px; 1372 } 1373 1374 .askeet-feedback-info svg { 1375 flex-shrink: 0; 1376 stroke: #005a9e; 1377 } 1378 1379 .askeet-feedback-info p { 1380 font-size: 14px; 1381 color: #333; 1382 margin: 0; 1383 flex-grow: 1; 1384 } 1385 1386 .askeet-feedback-book { 1387 color: #005a9e !important; 1388 font-size: 14px; 1389 font-weight: 600; 1390 text-decoration: none; 1391 white-space: nowrap; 1392 border-bottom: 1px solid transparent; 1393 transition: border-color 0.2s ease; 1394 } 1395 1396 .askeet-feedback-book:hover { 1397 border-bottom-color: #005a9e; 1398 text-decoration: none !important; 1399 } 1400 1401 /* Satisfaction Section */ 1402 .askeet-feedback-satisfaction { 1403 text-align: center; 1404 margin-bottom: 24px; 1405 } 1406 1407 .askeet-feedback-question { 1408 font-size: 14px; 1409 color: #666; 1410 margin-bottom: 16px; 1411 } 1412 1413 .askeet-feedback-thumbs { 1414 display: flex; 1415 justify-content: center; 1416 gap: 16px; 1417 } 1418 1419 .askeet-thumb-btn { 1420 display: flex; 1421 align-items: center; 1422 gap: 10px; 1423 padding: 12px 24px; 1424 background: transparent; 1425 border: 2px solid #e0e0e0; 1426 border-radius: 12px; 1427 cursor: pointer; 1428 transition: all 0.2s ease; 1429 min-width: 100px; 1430 justify-content: center; 1431 } 1432 1433 .askeet-thumb-btn:hover { 1434 border-color: #bbb; 1435 background: #fafafa; 1436 } 1437 1438 .askeet-thumb-btn.selected { 1439 border-color: #005a9e; 1440 background: linear-gradient(135deg, #e8f4fd 0%, #f0f7ff 100%); 1441 } 1442 1443 .askeet-thumb-btn.selected svg { 1444 stroke: #005a9e; 1445 } 1446 1447 .askeet-thumb-btn svg { 1448 stroke: #888; 1449 transition: stroke 0.2s ease; 1450 } 1451 1452 .askeet-thumb-btn span { 1453 font-size: 15px; 1454 font-weight: 600; 1455 color: #333; 1456 } 1457 1458 /* Textarea */ 1459 .askeet-feedback-textarea-wrapper { 1460 position: relative; 1461 } 1462 1463 .askeet-feedback-textarea { 1464 width: 100%; 1465 height: 120px; 1466 padding: 14px 16px; 1467 border: 1px solid #e0e0e0; 1468 border-radius: 10px; 1469 font-size: 14px; 1470 font-family: inherit; 1471 resize: none; 1472 outline: none; 1473 transition: border-color 0.2s ease, box-shadow 0.2s ease; 1474 box-sizing: border-box; 1475 } 1476 1477 .askeet-feedback-textarea:focus { 1478 border-color: #005a9e; 1479 box-shadow: 0 0 0 3px rgba(0, 90, 158, 0.15); 1480 } 1481 1482 .askeet-feedback-textarea::placeholder { 1483 color: #aaa; 1484 } 1485 1486 .askeet-feedback-counter { 1487 text-align: right; 1488 margin-top: 8px; 1489 font-size: 12px; 1490 color: #999; 1491 } 1492 1493 /* Modal Footer */ 1494 .askeet-feedback-footer { 1495 padding: 20px 24px 24px; 1496 border-top: 1px solid #f0f0f0; 1497 display: flex; 1498 flex-direction: column; 1499 gap: 12px; 1500 align-items: center; 1501 } 1502 1503 .askeet-submit-btn { 1504 display: inline-flex; 1505 align-items: center; 1506 justify-content: center; 1507 padding: 14px 40px; 1508 background: linear-gradient(135deg, #005a9e 0%, #0077cc 100%); 1509 color: #ffffff !important; 1510 border: none; 1511 border-radius: 10px; 1512 font-size: 16px; 1513 font-weight: 600; 1514 cursor: pointer; 1515 transition: all 0.3s ease; 1516 box-shadow: 0 8px 20px rgba(0, 90, 158, 0.3); 1517 min-width: 180px; 1518 } 1519 1520 .askeet-submit-btn:hover:not(:disabled) { 1521 background: linear-gradient(135deg, #004080 0%, #005fa3 100%); 1522 transform: translateY(-2px); 1523 box-shadow: 0 10px 25px rgba(0, 90, 158, 0.4); 1524 } 1525 1526 .askeet-submit-btn:disabled { 1527 background: #e0e0e0; 1528 color: #999 !important; 1529 cursor: not-allowed; 1530 box-shadow: none; 1531 opacity: 0.7; 1532 } 1533 1534 .askeet-cancel-btn { 1535 background: transparent; 1536 border: none; 1537 color: #666 !important; 1538 font-size: 15px; 1539 font-weight: 500; 1540 cursor: pointer; 1541 padding: 8px 16px; 1542 transition: color 0.2s ease; 1543 } 1544 1545 .askeet-cancel-btn:hover { 1546 color: #333 !important; 1547 } 1548 1549 /* Responsive */ 1550 @media (max-width: 480px) { 1551 .askeet-feedback-modal { 1552 width: 95%; 1553 max-width: none; 1554 } 1555 1556 .askeet-feedback-header { 1557 padding: 20px 16px 14px; 1558 } 1559 1560 .askeet-feedback-header h2 { 1561 font-size: 20px; 1562 } 1563 1564 .askeet-feedback-body { 1565 padding: 16px; 1566 } 1567 1568 .askeet-feedback-info { 1569 flex-direction: column; 1570 text-align: center; 1571 } 1572 1573 .askeet-feedback-thumbs { 1574 flex-direction: column; 1575 gap: 10px; 1576 } 1577 1578 .askeet-thumb-btn { 1579 width: 100%; 1580 } 1581 } 1582 1583 /* ======================================== 1584 RETRY CONTAINER & AI WARNING STYLES 1585 ======================================== */ 1586 1587 .wcqa-retry-container { 1588 display: flex; 1589 flex-wrap: wrap; 1590 align-items: center; 1591 gap: 12px; 1592 margin-top: 12px; 1593 padding: 12px 16px; 1594 background: linear-gradient(135deg, rgba(255, 193, 7, 0.1) 0%, rgba(255, 152, 0, 0.08) 100%); 1595 border-radius: 10px; 1596 border: 1px solid rgba(255, 193, 7, 0.3); 1597 } 1598 1599 .wcqa-retry-query.button { 1600 background: linear-gradient(135deg, #6c757d 0%, #5a6268 100%) !important; 1601 color: #fff !important; 1602 border: none !important; 1603 padding: 8px 18px !important; 1604 border-radius: 8px !important; 1605 font-weight: 600 !important; 1606 font-size: 13px !important; 1607 cursor: pointer !important; 1608 transition: all 0.3s ease !important; 1609 box-shadow: 0 2px 8px rgba(108, 117, 125, 0.3) !important; 1610 } 1611 1612 .wcqa-retry-query.button:hover { 1613 background: linear-gradient(135deg, #5a6268 0%, #495057 100%) !important; 1614 transform: translateY(-1px) !important; 1615 box-shadow: 0 4px 12px rgba(108, 117, 125, 0.4) !important; 1616 } 1617 1618 .wcqa-ai-warning { 1619 display: flex; 1620 align-items: center; 1621 gap: 8px; 1622 font-size: 12.5px; 1623 color: #856404; 1624 font-style: italic; 1625 line-height: 1.4; 1626 flex: 1; 1627 min-width: 200px; 1628 } 1629 1630 .wcqa-ai-warning::before { 1631 content: "\26A0"; 1632 font-size: 16px; 1633 color: #f39c12; 1634 font-style: normal; 1635 } 1636 1637 @media (max-width: 600px) { 1638 .wcqa-retry-container { 1639 flex-direction: column; 1640 align-items: flex-start; 1641 } 1642 1643 .wcqa-ai-warning { 1644 min-width: unset; 1645 } 1646 } 1647 1648 /* ======================================== 1649 STAR RATING SYSTEM STYLES 1650 ======================================== */ 1651 1652 .wcqa-rating-container { 1653 display: flex; 1654 flex-wrap: wrap; 1655 align-items: center; 1656 gap: 10px; 1657 margin-top: 14px; 1658 padding: 10px 14px; 1659 background: linear-gradient(135deg, rgba(255, 215, 0, 0.08) 0%, rgba(255, 193, 7, 0.05) 100%); 1660 border-radius: 10px; 1661 border: 1px solid rgba(255, 193, 7, 0.2); 1662 } 1663 1664 .wcqa-rating-label { 1665 font-size: 12.5px; 1666 color: #666; 1667 font-weight: 500; 1668 } 1669 1670 .wcqa-stars-container { 1671 display: flex; 1672 gap: 4px; 1673 cursor: pointer; 1674 } 1675 1676 .wcqa-stars-container.rated { 1677 cursor: default; 1678 } 1679 1680 .wcqa-star { 1681 font-size: 22px; 1682 color: #ddd; 1683 transition: all 0.2s ease; 1684 line-height: 1; 1685 } 1686 1687 .wcqa-star:hover, 1688 .wcqa-star.hovered { 1689 color: #ffc107; 1690 transform: scale(1.15); 1691 } 1692 1693 .wcqa-star.selected { 1694 color: #ffc107; 1695 } 1696 1697 .wcqa-stars-container.rated .wcqa-star { 1698 cursor: default; 1699 } 1700 1701 .wcqa-stars-container.rated .wcqa-star:hover { 1702 transform: none; 1703 } 1704 1705 .wcqa-rating-feedback { 1706 font-size: 12px; 1707 color: #888; 1708 font-style: italic; 1709 transition: all 0.3s ease; 1710 } 1711 1712 .wcqa-rating-feedback.success { 1713 color: #4caf50; 1714 font-weight: 600; 1715 } 1716 1717 .wcqa-rating-feedback.error { 1718 color: #f44336; 1719 } 1720 1721 @media (max-width: 500px) { 1722 .wcqa-rating-container { 1723 flex-direction: column; 1724 align-items: flex-start; 1725 } 1726 } -
askeet/assets/js/script.js
r3323653 r3415333 7 7 let lastPage = 1; 8 8 let lastPerPage = 20; 9 let currentPerPage = 20; // User-selected rows per page 10 let lastTotalCount = 0; // Total results count 9 11 let chatHistory = []; 10 12 let lastQueryRequest = ''; … … 12 14 let lastQueryLastSql = ''; 13 15 let lastQueryLastColumns = []; 16 let executionRetryCount = 0; // Track retries for execution logging 17 let currentQueryId = null; // Track query_id for database updates on retries 14 18 15 19 // Helper: scroll chat to bottom … … 22 26 function renderMessage(content, sender = 'ai', queryText = null, isError = false) { 23 27 const msg = $('<div class="wcqa-message"></div>').addClass(sender); 28 const messageId = 'msg-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9); 29 msg.attr('data-message-id', messageId); 30 24 31 if (sender === 'user' && queryText) { 25 32 // Add Save button for user queries … … 39 46 } 40 47 if (isError) { 48 // Create a container for retry button and AI warning 49 const retryContainer = $('<div class="wcqa-retry-container"></div>'); 41 50 const retryBtn = $('<button class="wcqa-retry-query button">'+askeet_query_assistant_i18n.retry+'</button>'); 42 51 retryBtn.on('click', function(e) { … … 44 53 retryLastQuery(); 45 54 }); 46 msg.append(retryBtn); 47 } 55 retryContainer.append(retryBtn); 56 // Add AI warning text 57 const aiWarning = $('<span class="wcqa-ai-warning">'+(askeet_query_assistant_i18n.ai_warning || 'Askeet est une AI et peut faire des erreurs. Veuillez svp vérifier les réponses.')+'</span>'); 58 retryContainer.append(aiWarning); 59 msg.append(retryContainer); 60 } 61 62 // Add star rating for AI messages (not for errors) 63 if (sender === 'ai' && content && !isError) { 64 const ratingContainer = $('<div class="wcqa-rating-container"></div>'); 65 const ratingLabel = $('<span class="wcqa-rating-label">'+(askeet_query_assistant_i18n.rate_response || 'Rate this response:')+'</span>'); 66 const starsContainer = $('<div class="wcqa-stars-container" data-message-id="'+messageId+'"></div>'); 67 68 // Create 5 stars 69 for (let i = 1; i <= 5; i++) { 70 const star = $('<span class="wcqa-star" data-rating="'+i+'">☆</span>'); 71 starsContainer.append(star); 72 } 73 74 const ratingFeedback = $('<span class="wcqa-rating-feedback"></span>'); 75 ratingContainer.append(ratingLabel); 76 ratingContainer.append(starsContainer); 77 ratingContainer.append(ratingFeedback); 78 msg.append(ratingContainer); 79 } 80 48 81 $('#wcqa-messages').append(msg); 49 82 scrollChatToBottom(); … … 187 220 lastQueryRequest = queryRequest; 188 221 lastQueryChatHistory = JSON.parse(JSON.stringify(chatHistory)); 222 currentQueryId = null; // Reset query_id for new query 223 executionRetryCount = 0; // Reset execution retry count 189 224 renderMessage(null, 'user', queryRequest); 190 225 $('#query-request').val(''); … … 192 227 $('#process-query').prop('disabled', true); 193 228 let attempt = 0; 229 const maxAttempts = 5; // Retry up to 5 times before showing error 230 let lastFailedSqlQuery = ''; // Track failed SQL for retry context 231 let lastErrorMessage = ''; // Track error message for retry context 232 194 233 function tryProcessQuery() { 195 234 attempt++; 235 // Build request data with retry context if this is a retry attempt 236 let requestData = { 237 action: 'askeet_process_query_request', 238 query_request: queryRequest, 239 previous_user_query: previous_user_query, 240 previous_ai_response: previous_ai_response, 241 nonce: askeet_query_assistant.nonce, 242 install_id: askeet_query_assistant.install_id 243 }; 244 245 // If this is a retry, include the failed query, error and query_id for AI context 246 if (attempt > 1 && (lastFailedSqlQuery || lastErrorMessage)) { 247 requestData.failed_sql_query = lastFailedSqlQuery || ''; 248 requestData.sql_error = lastErrorMessage || ''; 249 requestData.retry_attempt = attempt; 250 if (currentQueryId) { 251 requestData.query_id = currentQueryId; 252 } 253 console.log('[RETRY DEBUG] Sending retry - attempt:', attempt, 'currentQueryId:', currentQueryId, 'lastFailedSqlQuery:', !!lastFailedSqlQuery, 'lastErrorMessage:', !!lastErrorMessage); 254 } else { 255 console.log('[QUERY DEBUG] First attempt - attempt:', attempt, 'currentQueryId:', currentQueryId); 256 } 257 196 258 $.ajax({ 197 259 url: askeet_query_assistant.ajax_url, 198 260 type: 'POST', 199 data: { 200 action: 'askeet_process_query_request', 201 query_request: queryRequest, 202 previous_user_query: previous_user_query, 203 previous_ai_response: previous_ai_response, 204 nonce: askeet_query_assistant.nonce, 205 install_id: askeet_query_assistant.install_id 206 }, 261 data: requestData, 207 262 success: function(response) { 208 // // console.log('[WCQA] AJAX success:', response); // Debug log209 removeLoading();210 $('#process-query').prop('disabled', false);211 263 // PATCH: Detect limit errors in AJAX responses and show only one modal 212 if (handleLimitModals(response)) return; 264 if (handleLimitModals(response)) { 265 removeLoading(); 266 $('#process-query').prop('disabled', false); 267 return; 268 } 213 269 let results = (response.data && response.data.results) ? response.data.results : (response.results ? response.results : []); 214 270 let sql_query = (response.data && response.data.sql_query) ? response.data.sql_query : (response.sql_query ? response.sql_query : ''); 215 271 currentSqlQuery = sql_query || ''; 272 273 // Store query_id for tracking (from first response) 274 let responseQueryId = (response.data && response.data.query_id) ? response.data.query_id : null; 275 console.log('[RESPONSE DEBUG] response.success:', response.success, 'responseQueryId:', responseQueryId, 'currentQueryId before:', currentQueryId); 276 if (responseQueryId && !currentQueryId) { 277 currentQueryId = responseQueryId; 278 console.log('[QUERY] Got query_id:', currentQueryId); 279 } 280 216 281 // Only execute the paginated query, do NOT render SQL or button 217 282 if (response.success && currentSqlQuery) { 218 // TODO: Hide this in production 283 removeLoading(); 284 $('#process-query').prop('disabled', false); 219 285 clearResultMessages(); 220 286 executeQueryPage(1, true); 221 287 } else if (response.success) { 288 removeLoading(); 289 $('#process-query').prop('disabled', false); 222 290 clearResultMessages(); 223 291 renderMessage('<div class="query-success">'+askeet_query_assistant_i18n.query_executed_successfully+' 0 '+askeet_query_assistant_i18n.results_found+'.</div>', 'ai'); 224 292 } else { 225 clearErrorMessages(); 226 let errorMsg = ''; 227 if (response.data && response.data.message && response.data.message.indexOf('Seules les requêtes SELECT sont autorisées') !== -1) { 228 errorMsg = response.data.message; 293 // Store failed query info for next retry 294 // Capture SQL query even on failure if available 295 let failedQuery = sql_query || 296 (response.data && response.data.sql_query) || 297 (response.sql_query) || ''; 298 if (failedQuery) { 299 lastFailedSqlQuery = failedQuery; 300 } 301 302 // Capture query_id from error response for retry tracking 303 let errorQueryId = (response.data && response.data.query_id) ? response.data.query_id : null; 304 if (errorQueryId && !currentQueryId) { 305 currentQueryId = errorQueryId; 306 console.log('[RETRY] Got query_id from error response:', currentQueryId); 307 } 308 309 // Get error message from response 310 lastErrorMessage = (response.data && response.data.message) ? response.data.message : 311 (response.message) ? response.message : 'Query failed - AI could not generate valid SQL'; 312 313 console.log('[RETRY] Attempt', attempt, 'failed. SQL:', lastFailedSqlQuery, 'Error:', lastErrorMessage, 'query_id:', currentQueryId); 314 315 // Check if it's a non-retryable error (like security restriction) 316 let isNonRetryableError = response.data && response.data.message && 317 response.data.message.indexOf('Seules les requêtes SELECT sont autorisées') !== -1; 318 319 if (isNonRetryableError || attempt >= maxAttempts) { 320 // Max attempts reached or non-retryable error - log final failure 321 logExecutionResult(false, lastFailedSqlQuery, lastErrorMessage, attempt); 322 executionRetryCount = 0; // Reset 323 324 removeLoading(); 325 $('#process-query').prop('disabled', false); 326 clearErrorMessages(); 327 let errorMsg = ''; 328 if (isNonRetryableError) { 329 errorMsg = response.data.message; 330 } else { 331 errorMsg = askeet_query_assistant_i18n.ai_failed_5_times || askeet_query_assistant_i18n.ai_failed_3_times; 332 } 333 renderMessage('<div class="query-error">'+errorMsg+'</div>', 'ai', null, true); 229 334 } else { 230 errorMsg = askeet_query_assistant_i18n.ai_failed_3_times; 335 // Auto-retry silently in background with context 336 setTimeout(tryProcessQuery, 1000); 231 337 } 232 renderMessage('<div class="query-error">'+errorMsg+'</div>', 'ai', null, true);233 338 } 234 339 }, 235 340 error: function(xhr, status, error) { 236 removeLoading(); 237 clearErrorMessages(); 341 // Store error for retry context 342 lastErrorMessage = error || 'Network error'; 343 238 344 try { 239 345 var resp = xhr.responseJSON || JSON.parse(xhr.responseText); 240 // console.log('[WCQA] AJAX error:', resp); // Debug log241 346 // PATCH: Detect limit errors in AJAX responses and show only one modal 242 if (handleLimitModals(resp)) return; 243 } catch(e){ 244 // console.log('[WCQA] AJAX error (parse fail):', xhr.responseText); 245 } 246 renderMessage('<div class="query-error">'+askeet_query_assistant_i18n.error_communication+'</div>', 'ai', null, true); 347 if (handleLimitModals(resp)) { 348 removeLoading(); 349 $('#process-query').prop('disabled', false); 350 return; 351 } 352 if (resp && resp.data && resp.data.message) { 353 lastErrorMessage = resp.data.message; 354 } 355 // Capture SQL query from error response if available 356 if (resp && resp.data && resp.data.sql_query) { 357 lastFailedSqlQuery = resp.data.sql_query; 358 } 359 } catch(e){} 360 361 console.log('[RETRY] Attempt', attempt, 'error. Error:', lastErrorMessage); 362 363 if (attempt >= maxAttempts) { 364 // Max attempts reached - log final failure 365 logExecutionResult(false, lastFailedSqlQuery, 'Network error: ' + lastErrorMessage, attempt); 366 executionRetryCount = 0; // Reset 367 368 removeLoading(); 369 $('#process-query').prop('disabled', false); 370 clearErrorMessages(); 371 renderMessage('<div class="query-error">'+askeet_query_assistant_i18n.error_communication+'</div>', 'ai', null, true); 372 } else { 373 // Auto-retry silently in background 374 setTimeout(tryProcessQuery, 1000); 375 } 247 376 } 248 377 }); … … 265 394 $('#process-query').prop('disabled', true); 266 395 let attempt = 0; 396 const maxAttempts = 5; // Retry up to 5 times before showing error 397 let lastFailedSqlQuery = currentSqlQuery || ''; // Use last known SQL query 398 let lastErrorMessage = ''; // Track error message for retry context 399 267 400 function tryProcessQuery() { 268 401 attempt++; 402 // Build request data with retry context if this is a retry attempt 403 let requestData = { 404 action: 'askeet_process_query_request', 405 query_request: lastQueryRequest, 406 nonce: askeet_query_assistant.nonce, 407 install_id: askeet_query_assistant.install_id 408 }; 409 410 // If this is a retry, include the failed query and error for AI context 411 if (attempt > 1 && (lastFailedSqlQuery || lastErrorMessage)) { 412 requestData.failed_sql_query = lastFailedSqlQuery || ''; 413 requestData.sql_error = lastErrorMessage || ''; 414 requestData.retry_attempt = attempt; 415 } 416 269 417 $.ajax({ 270 418 url: askeet_query_assistant.ajax_url, 271 419 type: 'POST', 272 data: { 273 action: 'askeet_process_query_request', 274 query_request: lastQueryRequest, 275 nonce: askeet_query_assistant.nonce, 276 install_id: askeet_query_assistant.install_id 277 }, 420 data: requestData, 278 421 success: function(response) { 279 // console.log('[WCQA] AJAX success:', response); // Debug log280 removeLoading();281 $('#process-query').prop('disabled', false);282 422 // PATCH: Detect limit errors in AJAX responses and show only one modal 283 if (handleLimitModals(response)) return; 423 if (handleLimitModals(response)) { 424 removeLoading(); 425 $('#process-query').prop('disabled', false); 426 return; 427 } 284 428 let results = (response.data && response.data.results) ? response.data.results : (response.results ? response.results : []); 285 429 let sql_query = (response.data && response.data.sql_query) ? response.data.sql_query : (response.sql_query ? response.sql_query : ''); 286 430 currentSqlQuery = sql_query || ''; 287 if (response.success && results && results.length > 0) { 431 if (response.success && currentSqlQuery) { 432 removeLoading(); 433 $('#process-query').prop('disabled', false); 288 434 executeQueryPage(1, true); 289 435 } else if (response.success) { 436 removeLoading(); 437 $('#process-query').prop('disabled', false); 290 438 renderMessage('<div class="query-success">'+askeet_query_assistant_i18n.query_executed_successfully+' 0 '+askeet_query_assistant_i18n.results_found+'.</div>', 'ai'); 291 439 } else { 292 clearErrorMessages(); 293 let errorMsg = ''; 294 if (response.data && response.data.message && response.data.message.indexOf('Seules les requêtes SELECT sont autorisées') !== -1) { 295 errorMsg = response.data.message; 440 // Store failed query info for next retry 441 // Capture SQL query even on failure if available 442 let failedQuery = sql_query || 443 (response.data && response.data.sql_query) || 444 (response.sql_query) || ''; 445 if (failedQuery) { 446 lastFailedSqlQuery = failedQuery; 447 } 448 449 // Get error message from response 450 lastErrorMessage = (response.data && response.data.message) ? response.data.message : 451 (response.message) ? response.message : 'Query failed - AI could not generate valid SQL'; 452 453 console.log('[RETRY-LAST] Attempt', attempt, 'failed. SQL:', lastFailedSqlQuery, 'Error:', lastErrorMessage); 454 455 // Check if it's a non-retryable error (like security restriction) 456 let isNonRetryableError = response.data && response.data.message && 457 response.data.message.indexOf('Seules les requêtes SELECT sont autorisées') !== -1; 458 459 if (isNonRetryableError || attempt >= maxAttempts) { 460 // Max attempts reached or non-retryable error - log final failure 461 logExecutionResult(false, lastFailedSqlQuery, lastErrorMessage, attempt); 462 executionRetryCount = 0; // Reset 463 464 removeLoading(); 465 $('#process-query').prop('disabled', false); 466 clearErrorMessages(); 467 let errorMsg = ''; 468 if (isNonRetryableError) { 469 errorMsg = response.data.message; 470 } else { 471 errorMsg = askeet_query_assistant_i18n.ai_failed_5_times || askeet_query_assistant_i18n.ai_failed_3_times; 472 } 473 renderMessage('<div class="query-error">'+errorMsg+'</div>', 'ai', null, true); 296 474 } else { 297 errorMsg = askeet_query_assistant_i18n.ai_failed_3_times; 475 // Auto-retry silently in background with context 476 setTimeout(tryProcessQuery, 1000); 298 477 } 299 renderMessage('<div class="query-error">'+errorMsg+'</div>', 'ai', null, true);300 478 } 301 479 }, 302 480 error: function(xhr, status, error) { 303 removeLoading(); 304 clearErrorMessages(); 481 // Store error for retry context 482 lastErrorMessage = error || 'Network error'; 483 305 484 try { 306 485 var resp = xhr.responseJSON || JSON.parse(xhr.responseText); 307 // console.log('[WCQA] AJAX error:', resp); // Debug log308 486 // PATCH: Detect limit errors in AJAX responses and show only one modal 309 if (handleLimitModals(resp)) return; 310 } catch(e){ 311 // console.log('[WCQA] AJAX error (parse fail):', xhr.responseText); 312 } 313 renderMessage('<div class="query-error">'+askeet_query_assistant_i18n.error_communication+'</div>', 'ai', null, true); 487 if (handleLimitModals(resp)) { 488 removeLoading(); 489 $('#process-query').prop('disabled', false); 490 return; 491 } 492 if (resp && resp.data && resp.data.message) { 493 lastErrorMessage = resp.data.message; 494 } 495 // Capture SQL query from error response if available 496 if (resp && resp.data && resp.data.sql_query) { 497 lastFailedSqlQuery = resp.data.sql_query; 498 } 499 } catch(e){} 500 501 console.log('[RETRY-LAST] Attempt', attempt, 'error. Error:', lastErrorMessage); 502 503 if (attempt >= maxAttempts) { 504 // Max attempts reached - log final failure 505 logExecutionResult(false, lastFailedSqlQuery, 'Network error: ' + lastErrorMessage, attempt); 506 executionRetryCount = 0; // Reset 507 508 removeLoading(); 509 $('#process-query').prop('disabled', false); 510 clearErrorMessages(); 511 renderMessage('<div class="query-error">'+askeet_query_assistant_i18n.error_communication+'</div>', 'ai', null, true); 512 } else { 513 // Auto-retry silently in background 514 setTimeout(tryProcessQuery, 1000); 515 } 314 516 } 315 517 }); 316 518 } 317 519 tryProcessQuery(); 520 } 521 522 // Retry with execution error context - called when SQL execution fails 523 function retryWithExecutionError(originalQuery, failedSql, executionError) { 524 console.log('[RETRY EXEC] Starting retry with execution error context'); 525 console.log('[RETRY EXEC] Original query:', originalQuery); 526 console.log('[RETRY EXEC] Failed SQL:', failedSql); 527 console.log('[RETRY EXEC] Execution error:', executionError); 528 529 // Show loading 530 renderLoading(); 531 $('#process-query').prop('disabled', true); 532 533 let attempt = 0; 534 const maxAttempts = 5; 535 let lastFailedSqlQuery = failedSql; 536 let lastErrorMessage = executionError; 537 538 function tryProcessQueryWithContext() { 539 attempt++; 540 console.log('[RETRY EXEC] Attempt', attempt, 'of', maxAttempts); 541 542 // Always include error context since we're retrying due to execution error 543 let requestData = { 544 action: 'askeet_process_query_request', 545 query_request: originalQuery, 546 failed_sql_query: lastFailedSqlQuery, 547 sql_error: lastErrorMessage, 548 retry_attempt: attempt, 549 nonce: askeet_query_assistant.nonce, 550 install_id: askeet_query_assistant.install_id 551 }; 552 553 // Include query_id for database tracking 554 if (currentQueryId) { 555 requestData.query_id = currentQueryId; 556 } 557 558 console.log('[RETRY EXEC DEBUG] query_id being sent:', currentQueryId, 'retry_attempt:', attempt); 559 560 $.ajax({ 561 url: askeet_query_assistant.ajax_url, 562 type: 'POST', 563 data: requestData, 564 success: function(response) { 565 if (handleLimitModals(response)) { 566 removeLoading(); 567 $('#process-query').prop('disabled', false); 568 return; 569 } 570 571 let sql_query = (response.data && response.data.sql_query) ? response.data.sql_query : (response.sql_query ? response.sql_query : ''); 572 currentSqlQuery = sql_query || ''; 573 574 if (response.success && currentSqlQuery) { 575 console.log('[RETRY EXEC] Got new SQL, executing:', currentSqlQuery); 576 // Try to execute the new query 577 executeQueryPage(1, true); 578 } else if (response.success) { 579 removeLoading(); 580 $('#process-query').prop('disabled', false); 581 renderMessage('<div class="query-success">'+askeet_query_assistant_i18n.query_executed_successfully+' 0 '+askeet_query_assistant_i18n.results_found+'.</div>', 'ai'); 582 } else { 583 // AI returned error - capture and retry if attempts left 584 let newFailedQuery = sql_query || 585 (response.data && response.data.sql_query) || 586 (response.sql_query) || lastFailedSqlQuery; 587 if (newFailedQuery) { 588 lastFailedSqlQuery = newFailedQuery; 589 } 590 lastErrorMessage = (response.data && response.data.message) ? response.data.message : 591 (response.message) ? response.message : 'Query generation failed'; 592 593 console.log('[RETRY EXEC] Attempt', attempt, 'failed. New error:', lastErrorMessage); 594 595 if (attempt >= maxAttempts) { 596 // Log final failure after all retries exhausted 597 logExecutionResult(false, lastFailedSqlQuery, lastErrorMessage, attempt); 598 executionRetryCount = 0; // Reset 599 600 removeLoading(); 601 $('#process-query').prop('disabled', false); 602 clearErrorMessages(); 603 renderMessage('<div class="query-error">'+(askeet_query_assistant_i18n.ai_failed_5_times || askeet_query_assistant_i18n.ai_failed_3_times)+'</div>', 'ai', null, true); 604 } else { 605 setTimeout(tryProcessQueryWithContext, 1000); 606 } 607 } 608 }, 609 error: function(xhr, status, error) { 610 lastErrorMessage = error || 'Network error'; 611 console.log('[RETRY EXEC] Network error on attempt', attempt, ':', lastErrorMessage); 612 613 if (attempt >= maxAttempts) { 614 // Log final failure after all retries exhausted (network error) 615 logExecutionResult(false, lastFailedSqlQuery, 'Network error: ' + lastErrorMessage, attempt); 616 executionRetryCount = 0; // Reset 617 618 removeLoading(); 619 $('#process-query').prop('disabled', false); 620 clearErrorMessages(); 621 renderMessage('<div class="query-error">'+askeet_query_assistant_i18n.error_communication+'</div>', 'ai', null, true); 622 } else { 623 setTimeout(tryProcessQueryWithContext, 1000); 624 } 625 } 626 }); 627 } 628 629 tryProcessQueryWithContext(); 318 630 } 319 631 … … 380 692 sql_query: currentSqlQuery, 381 693 nonce: askeet_query_assistant.nonce, 382 page: pageNum 694 page: pageNum, 695 per_page: currentPerPage 383 696 }, 384 697 success: function(response) { 385 698 // console.log('[WCQA] AJAX success:', response); // Debug log 386 699 removeLoading(); 700 701 // Check for upgrade requirement 702 if (handleLimitModals(response)) return; 703 387 704 if (response.success) { 705 // Log successful execution 706 logExecutionResult(true, currentSqlQuery, '', executionRetryCount); 707 executionRetryCount = 0; // Reset retry count after success 708 388 709 lastResults = response.data.results; 389 710 lastResultsHtml = response.data.html_results; … … 396 717 lastPage = response.data.page; 397 718 lastPerPage = response.data.per_page; 719 lastTotalCount = response.data.total_count; 398 720 const totalPages = Math.max(1, Math.ceil(response.data.total_count / response.data.per_page)); 399 721 let html = ''; 400 722 if (response.data.results && response.data.results.length > 0) { 723 // Show rows per page selector and export buttons at the top 724 html += '<div class="wcqa-top-controls">'; 725 726 // Rows per page selector 727 html += '<div class="wcqa-rows-selector">'; 728 html += '<label for="wcqa-per-page-select">Rows per page:</label> '; 729 html += '<select id="wcqa-per-page-select" class="wcqa-per-page-dropdown">'; 730 html += '<option value="20"'+(currentPerPage===20?' selected':'')+'>20</option>'; 731 html += '<option value="50"'+(currentPerPage===50?' selected':'')+'>50</option>'; 732 html += '<option value="100"'+(currentPerPage===100?' selected':'')+'>100</option>'; 733 html += '<option value="500"'+(currentPerPage===500?' selected':'')+'>500</option>'; 734 html += '<option value="1500"'+(currentPerPage===1500?' selected':'')+'>1500</option>'; 735 html += '</select>'; 736 html += '</div>'; 737 738 // Export buttons (compact, inline) 739 html += '<div class="wcqa-export-controls-top">'; 740 const actualRowsOnPage = response.data.actual_rows || response.data.results.length; 741 html += '<button id="export-current-page" class="button wcqa-export-btn-compact" title="Export all '+actualRowsOnPage+' rows from this page">'; 742 html += '<span class="dashicons dashicons-download"></span> Export Page ('+actualRowsOnPage+')'; 743 html += '</button>'; 744 const maxExportLimit = 10000; 745 const exportAllCount = Math.min(response.data.total_count, maxExportLimit); 746 html += '<button id="export-all-results" class="button wcqa-export-btn-compact wcqa-export-all-btn" title="Export up to '+maxExportLimit.toLocaleString()+' rows">'; 747 html += '<span class="dashicons dashicons-database-export"></span> Export All ('+exportAllCount.toLocaleString()+')'; 748 html += '</button>'; 749 html += '</div>'; 750 751 html += '</div>'; 752 753 // Warning for large exports 754 if (response.data.total_count > maxExportLimit) { 755 html += '<div class="wcqa-export-warning-top">⚠️ '+response.data.total_count.toLocaleString()+' results found. Export All will download the first '+maxExportLimit.toLocaleString()+' rows.</div>'; 756 } 757 758 // Performance notice for large page sizes 759 if (response.data.display_limited === true || (currentPerPage >= 500 && response.data.actual_rows >= 500)) { 760 html += '<div class="wcqa-performance-notice">'; 761 html += '<div class="wcqa-notice-icon">💡</div>'; 762 html += '<div class="wcqa-notice-content">'; 763 html += '<strong>Performance Mode Active:</strong> For optimal browser performance, we\'re displaying only the first 50 rows. '; 764 html += 'Don\'t worry - you can export all <strong>'+response.data.actual_rows+' rows</strong> from this page using the <strong>"Export Page"</strong> button above!'; 765 html += '</div>'; 766 html += '<button class="wcqa-notice-close" title="Close this message">×</button>'; 767 html += '</div>'; 768 } 769 401 770 // Show summary message 402 771 const startIdx = (response.data.page - 1) * response.data.per_page + 1; 403 772 const endIdx = startIdx + response.data.results.length - 1; 404 html += '<div class="query-success">Displaying '+startIdx+' to '+endIdx+' of '+response.data.total_count+' results (Page '+response.data.page+' of '+totalPages+'). '+askeet_query_assistant_i18n.execution_time+': '+response.data.execution_time+' '+askeet_query_assistant_i18n.seconds+'.</div>'; 773 const displayInfo = response.data.display_limited ? ' (displaying first '+response.data.results.length+' rows for performance)' : ''; 774 html += '<div class="query-success">Page '+response.data.page+' of '+totalPages+': showing rows '+startIdx+' to '+endIdx+' of '+response.data.total_count+' total'+displayInfo+'. '+askeet_query_assistant_i18n.execution_time+': '+response.data.execution_time+'s</div>'; 775 405 776 html += '<div class="results-block">' + renderResultsTable(response.data.results, response.data.total_count, response.data.page) + '</div>'; 777 406 778 if (!response.data.disable_pagination) { 407 779 html += renderPagination(response.data.total_count, response.data.page, response.data.per_page); 408 780 } 409 html += '<button id="export-csv" class="button" style="margin-top:10px;">'+askeet_query_assistant_i18n.export_csv+'</button>';410 781 } else { 411 782 html += '<div class="query-success">'+askeet_query_assistant_i18n.no_result_found+'</div>'; 412 783 } 413 784 renderMessage(html, 'ai'); 785 786 // Add close functionality to performance notice after rendering 787 if (response.data.display_limited === true || (currentPerPage >= 500 && response.data.actual_rows >= 500)) { 788 setTimeout(function() { 789 const $notice = $('.wcqa-performance-notice').last(); 790 791 // Close button handler 792 $notice.find('.wcqa-notice-close').on('click', function(e) { 793 e.preventDefault(); 794 $(this).closest('.wcqa-performance-notice').fadeOut(300, function() { 795 $(this).remove(); 796 }); 797 }); 798 799 // Auto-fade after 12 seconds 800 setTimeout(function() { 801 if ($notice.is(':visible')) { 802 $notice.fadeOut(400, function() { 803 $(this).remove(); 804 }); 805 } 806 }, 12000); 807 }, 200); 808 } 414 809 } 415 810 else { 416 clearErrorMessages(); 417 let errorMsg = ''; 418 if (response.data && response.data.message && response.data.message.indexOf('Seules les requêtes SELECT sont autorisées') !== -1) { 419 errorMsg = response.data.message; 811 // SQL execution error - capture for potential retry 812 let executionError = (response.data && response.data.message) ? response.data.message : 'SQL execution failed'; 813 let failedSql = currentSqlQuery || ''; 814 815 console.log('[EXEC ERROR] SQL:', failedSql, 'Error:', executionError); 816 817 // Check if it's a non-retryable error (like security restriction) 818 let isNonRetryableError = response.data && response.data.message && 819 response.data.message.indexOf('Seules les requêtes SELECT sont autorisées') !== -1; 820 821 if (isNonRetryableError) { 822 // Security error - don't retry, just show error and log failure 823 logExecutionResult(false, failedSql, executionError, 0); 824 clearErrorMessages(); 825 renderMessage('<div class="query-error">'+response.data.message+'</div>', 'ai', null, true); 826 } else if (isNewQuery && lastQueryRequest) { 827 // This was a new query that failed on execution - trigger auto-retry with context 828 console.log('[EXEC ERROR] Triggering auto-retry with execution error context'); 829 executionRetryCount++; // Increment retry count 830 retryWithExecutionError(lastQueryRequest, failedSql, executionError); 420 831 } else { 421 errorMsg = askeet_query_assistant_i18n.ai_failed_3_times; 422 } 423 renderMessage('<div class="query-error">'+errorMsg+'</div>', 'ai', null, true); 832 // Not a new query or no original query - just show error with retry button and log failure 833 logExecutionResult(false, failedSql, executionError, executionRetryCount); 834 executionRetryCount = 0; // Reset 835 clearErrorMessages(); 836 renderMessage('<div class="query-error">'+executionError+'</div>', 'ai', null, true); 837 } 424 838 } 425 839 }, … … 427 841 removeLoading(); 428 842 clearErrorMessages(); 843 // Log network/AJAX error 844 logExecutionResult(false, currentSqlQuery, 'Network error: ' + (error || status), executionRetryCount); 845 executionRetryCount = 0; // Reset 846 429 847 try { 430 848 var resp = xhr.responseJSON || JSON.parse(xhr.responseText); … … 453 871 } 454 872 }); 455 $('#wcqa-messages').on('click', '#export-csv', function(e) { 873 // Handle rows per page selection change 874 $('#wcqa-messages').on('change', '#wcqa-per-page-select', function(e) { 875 const newPerPage = parseInt($(this).val()); 876 currentPerPage = newPerPage; 877 878 // Re-execute query with new page size, starting from page 1 879 executeQueryPage(1, false); 880 }); 881 882 // Export Current Page - exports ALL rows from current page (even if display is limited) 883 $('#wcqa-messages').on('click', '#export-current-page', function(e) { 456 884 e.preventDefault(); 885 if (currentSqlQuery === '') { 886 alert(askeet_query_assistant_i18n.no_sql_to_execute); 887 return; 888 } 889 457 890 const $btn = $(this); 458 // Cherche le tableau de résultats associé au bouton cliqué 459 let $table = $btn.closest('.wcqa-message').find('.results-table').first(); 460 if ($table.length === 0) { 461 // Si pas trouvé dans le même message, cherche juste avant 462 $table = $btn.prevAll('.results-block').find('.results-table').first(); 463 } 464 if ($table.length === 0) { 465 // Fallback : cherche dans le parent direct 466 $table = $btn.parent().find('.results-table').first(); 467 } 468 if ($table.length === 0) { 469 alert(askeet_query_assistant_i18n.no_result_to_export); 891 const originalText = $btn.html(); 892 $btn.prop('disabled', true).html('<span class="spinner is-active" style="float:none;"></span> Exporting...'); 893 894 // Fetch the FULL page data from server (all rows for current page) 895 $.ajax({ 896 url: askeet_query_assistant.ajax_url, 897 type: 'POST', 898 data: { 899 action: 'askeet_execute_sql_query', 900 sql_query: currentSqlQuery, 901 nonce: askeet_query_assistant.nonce, 902 page: lastPage, 903 per_page: currentPerPage, 904 export_mode: true // Request full data without display limit 905 }, 906 success: function(response) { 907 if (response.success && response.data && response.data.results) { 908 const results = response.data.results; 909 if (results.length === 0) { 910 alert('No data to export.'); 911 $btn.prop('disabled', false).html(originalText); 912 return; 913 } 914 915 // Convert to CSV 916 let csv = []; 917 const headers = Object.keys(results[0]); 918 csv.push(headers.map(h => '"' + h.replace(/"/g, '""') + '"').join(',')); 919 920 results.forEach(function(row) { 921 const values = headers.map(function(header) { 922 let val = row[header] !== null ? String(row[header]) : ''; 923 val = val.replace(/"/g, '""'); 924 return '"' + val + '"'; 925 }); 926 csv.push(values.join(',')); 927 }); 928 929 const csvContent = csv.join('\n'); 930 const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); 931 const link = document.createElement('a'); 932 const url = URL.createObjectURL(blob); 933 link.setAttribute('href', url); 934 935 var lang = (navigator.language || navigator.userLanguage || 'en').toLowerCase(); 936 var filename = lang.startsWith('fr') 937 ? 'page-' + lastPage + '_' + results.length + '_lignes.csv' 938 : 'page-' + lastPage + '_' + results.length + '_rows.csv'; 939 940 link.setAttribute('download', filename); 941 link.style.visibility = 'hidden'; 942 document.body.appendChild(link); 943 link.click(); 944 document.body.removeChild(link); 945 946 $btn.prop('disabled', false).html('<span class="dashicons dashicons-yes"></span> Done!'); 947 setTimeout(function() { 948 $btn.html(originalText); 949 }, 2000); 950 } else { 951 alert('Export failed. Please try again.'); 952 $btn.prop('disabled', false).html(originalText); 953 } 954 }, 955 error: function() { 956 alert('Export failed. Please try again.'); 957 $btn.prop('disabled', false).html(originalText); 958 } 959 }); 960 }); 961 962 // Export All Results - fetches up to 5000 rows from server 963 $('#wcqa-messages').on('click', '#export-all-results', function(e) { 964 e.preventDefault(); 965 if (currentSqlQuery === '') { 966 alert(askeet_query_assistant_i18n.no_sql_to_execute); 470 967 return; 471 968 } 472 let csv = []; 473 const rows = $table[0].querySelectorAll('tr'); 474 for (const row of rows) { 475 const cells = row.querySelectorAll('th, td'); 476 const csvRow = []; 477 for (const cell of cells) { 478 let text = cell.textContent.trim(); 479 text = text.replace(/"/g, '""'); 480 csvRow.push('"' + text + '"'); 481 } 482 csv.push(csvRow.join(',')); 483 } 484 const csvContent = csv.join('\n'); 485 const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); 486 const link = document.createElement('a'); 487 const url = URL.createObjectURL(blob); 488 link.setAttribute('href', url); 489 // Determine language for filename 490 var lang = (navigator.language || navigator.userLanguage || 'en').toLowerCase(); 491 var filename = ''; 492 if (lang.startsWith('fr')) { 493 filename = 'resultat_requete_page' + lastPage + '.csv'; 494 } else { 495 filename = 'result_query_page' + lastPage + '.csv'; 496 } 497 link.setAttribute('download', filename); 498 link.style.visibility = 'hidden'; 499 document.body.appendChild(link); 500 link.click(); 501 document.body.removeChild(link); 969 970 const $btn = $(this); 971 const originalText = $btn.html(); 972 const maxRows = Math.min(lastTotalCount, 10000); 973 974 // Confirm for large exports 975 if (maxRows > 1000) { 976 const confirmMsg = 'You are about to export ' + maxRows.toLocaleString() + ' rows. This may take up to 60 seconds. Continue?'; 977 if (!confirm(confirmMsg)) { 978 return; 979 } 980 } 981 982 $btn.prop('disabled', true).html('<span class="spinner is-active" style="float:none;"></span> Exporting...'); 983 984 $.ajax({ 985 url: askeet_query_assistant.ajax_url, 986 type: 'POST', 987 data: { 988 action: 'askeet_export_all_results', 989 sql_query: currentSqlQuery, 990 nonce: askeet_query_assistant.nonce, 991 max_rows: 10000 992 }, 993 success: function(response) { 994 if (response.success && response.data && response.data.results) { 995 // Convert results to CSV 996 const results = response.data.results; 997 if (results.length === 0) { 998 alert('No data to export.'); 999 $btn.prop('disabled', false).html(originalText); 1000 return; 1001 } 1002 1003 let csv = []; 1004 // Add header row 1005 const headers = Object.keys(results[0]); 1006 csv.push(headers.map(h => '"' + h.replace(/"/g, '""') + '"').join(',')); 1007 1008 // Add data rows 1009 results.forEach(function(row) { 1010 const values = headers.map(function(header) { 1011 let val = row[header] !== null ? String(row[header]) : ''; 1012 val = val.replace(/"/g, '""'); 1013 return '"' + val + '"'; 1014 }); 1015 csv.push(values.join(',')); 1016 }); 1017 1018 const csvContent = csv.join('\n'); 1019 const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); 1020 const link = document.createElement('a'); 1021 const url = URL.createObjectURL(blob); 1022 link.setAttribute('href', url); 1023 1024 // Filename with row count 1025 var lang = (navigator.language || navigator.userLanguage || 'en').toLowerCase(); 1026 var timestamp = new Date().toISOString().slice(0,10); 1027 var filename = lang.startsWith('fr') 1028 ? 'export_complet_' + results.length + '_lignes_' + timestamp + '.csv' 1029 : 'export_all_' + results.length + '_rows_' + timestamp + '.csv'; 1030 1031 link.setAttribute('download', filename); 1032 link.style.visibility = 'hidden'; 1033 document.body.appendChild(link); 1034 link.click(); 1035 document.body.removeChild(link); 1036 1037 // Show success message 1038 $btn.prop('disabled', false).html('<span class="dashicons dashicons-yes"></span> Done!'); 1039 setTimeout(function() { 1040 $btn.html(originalText); 1041 }, 2000); 1042 } else { 1043 alert('Error exporting data: ' + (response.data && response.data.message ? response.data.message : 'Unknown error')); 1044 $btn.prop('disabled', false).html(originalText); 1045 } 1046 }, 1047 error: function(xhr, status, error) { 1048 alert('Export failed. Please try again or reduce the number of results.'); 1049 $btn.prop('disabled', false).html(originalText); 1050 } 1051 }); 502 1052 }); 503 1053 … … 812 1362 // PATCH: Detect limit errors in AJAX responses and show only one modal 813 1363 function handleLimitModals(response) { 1364 // Check if upgrade is required (even if success is true) 1365 if (response && response.data && response.data.upgrade_required === true) { 1366 const msg = (response.data.error || response.data.message || '').toLowerCase(); 1367 let currentPlan = response.data.current_plan || (typeof askeet_query_assistant !== 'undefined' && askeet_query_assistant.current_plan ? askeet_query_assistant.current_plan : null); 1368 1369 if (msg.includes('daily limit') || msg.includes('50 queries') || msg.includes('upgrade to unlimited')) { 1370 showDailyLimitModal(currentPlan); 1371 return true; 1372 } 1373 if (msg.includes('free limit') || msg.includes('reached your free limit') || msg.includes('upgrade to premium')) { 1374 showPaymentModal(currentPlan); 1375 return true; 1376 } 1377 // Fallback: show payment modal for any upgrade requirement 1378 showPaymentModal(currentPlan); 1379 return true; 1380 } 1381 1382 // Legacy check for success === false 814 1383 if ( 815 1384 response && … … 902 1471 }); 903 1472 }); 1473 1474 // ======================================== 1475 // FEEDBACK MODAL SYSTEM 1476 // ======================================== 1477 1478 let feedbackSatisfaction = null; 1479 1480 // Open feedback modal 1481 $('#askeet-feedback-btn').on('click', function() { 1482 $('#askeet-feedback-overlay').addClass('active'); 1483 resetFeedbackModal(); 1484 }); 1485 1486 // Close feedback modal 1487 $('#askeet-feedback-close, #askeet-cancel-feedback').on('click', function() { 1488 closeFeedbackModal(); 1489 }); 1490 1491 // Close on overlay click 1492 $('#askeet-feedback-overlay').on('click', function(e) { 1493 if ($(e.target).is('#askeet-feedback-overlay')) { 1494 closeFeedbackModal(); 1495 } 1496 }); 1497 1498 // Handle thumb buttons 1499 $('.askeet-thumb-btn').on('click', function() { 1500 $('.askeet-thumb-btn').removeClass('selected'); 1501 $(this).addClass('selected'); 1502 feedbackSatisfaction = $(this).data('satisfaction'); 1503 updateSubmitButton(); 1504 }); 1505 1506 // Character counter 1507 $('#askeet-feedback-text').on('input', function() { 1508 const count = $(this).val().length; 1509 $('#askeet-char-count').text(count); 1510 updateSubmitButton(); 1511 }); 1512 1513 // Submit feedback 1514 $('#askeet-submit-feedback').on('click', function() { 1515 const btn = $(this); 1516 const originalText = btn.text(); 1517 const feedbackText = $('#askeet-feedback-text').val().trim(); 1518 1519 if (!feedbackSatisfaction) { 1520 alert('Please select if you are satisfied or not.'); 1521 return; 1522 } 1523 1524 btn.prop('disabled', true).text('Sending...'); 1525 1526 // Get install_id from localized data 1527 const installId = window.askeet_query_assistant && window.askeet_query_assistant.install_id ? window.askeet_query_assistant.install_id : ''; 1528 const apiUrl = window.askeet_query_assistant && window.askeet_query_assistant.api_url ? window.askeet_query_assistant.api_url : 'https://api.askeet.ai'; 1529 1530 $.ajax({ 1531 url: apiUrl + '/submit-feedback', 1532 type: 'POST', 1533 contentType: 'application/json', 1534 data: JSON.stringify({ 1535 install_id: installId, 1536 satisfaction: feedbackSatisfaction, 1537 feedback_text: feedbackText, 1538 page_url: window.location.href, 1539 user_agent: navigator.userAgent 1540 }), 1541 success: function(response) { 1542 if (response.success) { 1543 // Show success message in modal 1544 showFeedbackSuccess(); 1545 } else { 1546 alert('Error: ' + (response.error || 'Failed to submit feedback')); 1547 btn.prop('disabled', false).text(originalText); 1548 } 1549 }, 1550 error: function() { 1551 alert('Network error. Please try again.'); 1552 btn.prop('disabled', false).text(originalText); 1553 } 1554 }); 1555 }); 1556 1557 function updateSubmitButton() { 1558 const hasSelection = feedbackSatisfaction !== null; 1559 $('#askeet-submit-feedback').prop('disabled', !hasSelection); 1560 } 1561 1562 function closeFeedbackModal() { 1563 $('#askeet-feedback-overlay').removeClass('active'); 1564 } 1565 1566 function resetFeedbackModal() { 1567 feedbackSatisfaction = null; 1568 $('.askeet-thumb-btn').removeClass('selected'); 1569 $('#askeet-feedback-text').val(''); 1570 $('#askeet-char-count').text('0'); 1571 $('#askeet-submit-feedback').prop('disabled', true).text(askeet_query_assistant_i18n.submit_feedback || 'Submit Feedback'); 1572 1573 // Reset to normal view (hide success) 1574 $('.askeet-feedback-body, .askeet-feedback-footer').show(); 1575 $('.askeet-feedback-success').remove(); 1576 } 1577 1578 function showFeedbackSuccess() { 1579 // Hide body and footer 1580 $('.askeet-feedback-body, .askeet-feedback-footer').hide(); 1581 1582 // Show success message 1583 const successHtml = ` 1584 <div class="askeet-feedback-success" style="padding: 60px 30px; text-align: center;"> 1585 <div style="width: 80px; height: 80px; background: linear-gradient(135deg, #4caf50 0%, #45a049 100%); border-radius: 50%; margin: 0 auto 24px; display: flex; align-items: center; justify-content: center;"> 1586 <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> 1587 <polyline points="20 6 9 17 4 12"></polyline> 1588 </svg> 1589 </div> 1590 <h3 style="font-size: 24px; font-weight: 700; color: #1a1a1a; margin: 0 0 12px;">Thank You!</h3> 1591 <p style="font-size: 16px; color: #666; margin: 0 0 24px;">Your feedback has been submitted successfully.</p> 1592 <button type="button" class="askeet-submit-btn" onclick="jQuery('#askeet-feedback-overlay').removeClass('active');" style="background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);"> 1593 Close 1594 </button> 1595 </div> 1596 `; 1597 1598 $('.askeet-feedback-modal').append(successHtml); 1599 1600 // Auto close after 3 seconds 1601 setTimeout(function() { 1602 closeFeedbackModal(); 1603 // Reset after close animation 1604 setTimeout(resetFeedbackModal, 300); 1605 }, 3000); 1606 } 1607 1608 // Close modal on Escape key 1609 $(document).on('keydown', function(e) { 1610 if (e.key === 'Escape' && $('#askeet-feedback-overlay').hasClass('active')) { 1611 closeFeedbackModal(); 1612 } 1613 }); 1614 1615 // ======================================== 1616 // STAR RATING SYSTEM 1617 // ======================================== 1618 1619 // Get install_id and api_url from localized data 1620 const installId = window.askeet_query_assistant && window.askeet_query_assistant.install_id ? window.askeet_query_assistant.install_id : ''; 1621 const apiUrl = window.askeet_query_assistant && window.askeet_query_assistant.api_url ? window.askeet_query_assistant.api_url : 'https://api.askeet.ai'; 1622 1623 // Star hover effect 1624 $(document).on('mouseenter', '.wcqa-star', function() { 1625 const container = $(this).closest('.wcqa-stars-container'); 1626 if (container.hasClass('rated')) return; // Don't change if already rated 1627 1628 const rating = $(this).data('rating'); 1629 container.find('.wcqa-star').each(function() { 1630 const starRating = $(this).data('rating'); 1631 if (starRating <= rating) { 1632 $(this).html('★').addClass('hovered'); // Filled star 1633 } else { 1634 $(this).html('☆').removeClass('hovered'); // Empty star 1635 } 1636 }); 1637 }); 1638 1639 // Star mouse leave - reset if not rated 1640 $(document).on('mouseleave', '.wcqa-stars-container', function() { 1641 if ($(this).hasClass('rated')) return; 1642 1643 $(this).find('.wcqa-star').each(function() { 1644 $(this).html('☆').removeClass('hovered'); 1645 }); 1646 }); 1647 1648 // ======================================== 1649 // EXECUTION RESULT LOGGING 1650 // ======================================== 1651 1652 /** 1653 * Log the actual SQL execution result to the API for tracking. 1654 * Updates the existing query record with WordPress execution results. 1655 * 1656 * @param {boolean} success - Whether the SQL executed successfully 1657 * @param {string} sqlQuery - The SQL that was executed 1658 * @param {string} error - Error message if execution failed 1659 * @param {number} retryCount - Number of retries that were needed 1660 */ 1661 function logExecutionResult(success, sqlQuery, error, retryCount) { 1662 // Don't block execution, fire and forget 1663 let requestData = { 1664 install_id: installId, 1665 sql_query: sqlQuery || '', 1666 execution_success: success, 1667 execution_error: error || '', 1668 retry_count: retryCount || 0 1669 }; 1670 1671 // Include query_id if available for more reliable matching 1672 if (currentQueryId) { 1673 requestData.query_id = currentQueryId; 1674 } 1675 1676 $.ajax({ 1677 url: apiUrl + '/log-execution-result', 1678 type: 'POST', 1679 contentType: 'application/json', 1680 data: JSON.stringify(requestData), 1681 success: function(response) { 1682 console.log('[EXEC LOG] Execution result logged:', success ? 'SUCCESS' : 'FAILED', 'query_id:', currentQueryId); 1683 }, 1684 error: function(xhr, status, err) { 1685 console.log('[EXEC LOG] Failed to log execution result:', err); 1686 } 1687 }); 1688 } 1689 1690 // Star click - submit rating 1691 $(document).on('click', '.wcqa-star', function() { 1692 const container = $(this).closest('.wcqa-stars-container'); 1693 if (container.hasClass('rated')) return; // Already rated 1694 1695 const rating = $(this).data('rating'); 1696 const messageId = container.data('message-id'); 1697 const feedbackSpan = container.siblings('.wcqa-rating-feedback'); 1698 const messageContainer = container.closest('.wcqa-message'); 1699 const messageContent = messageContainer.find('.query-success, .query-error, .wcqa-table-wrapper').first().text().substring(0, 200); 1700 1701 // Show filled stars up to selected rating 1702 container.find('.wcqa-star').each(function() { 1703 const starRating = $(this).data('rating'); 1704 if (starRating <= rating) { 1705 $(this).html('★').addClass('selected'); 1706 } else { 1707 $(this).html('☆').removeClass('selected'); 1708 } 1709 }); 1710 1711 // Mark as rated 1712 container.addClass('rated'); 1713 feedbackSpan.text(askeet_query_assistant_i18n.rating_sending || 'Sending...'); 1714 1715 // Submit rating to API 1716 $.ajax({ 1717 url: apiUrl + '/submit-rating', 1718 type: 'POST', 1719 contentType: 'application/json', 1720 data: JSON.stringify({ 1721 install_id: installId, 1722 rating: rating, 1723 message_id: messageId, 1724 query: lastQueryRequest || '', 1725 response_preview: messageContent 1726 }), 1727 success: function(response) { 1728 if (response.success) { 1729 feedbackSpan.text(askeet_query_assistant_i18n.rating_thank_you || 'Thank you!').addClass('success'); 1730 } else { 1731 feedbackSpan.text(askeet_query_assistant_i18n.rating_error || 'Error').addClass('error'); 1732 // Allow retry 1733 container.removeClass('rated'); 1734 } 1735 }, 1736 error: function() { 1737 feedbackSpan.text(askeet_query_assistant_i18n.rating_error || 'Error').addClass('error'); 1738 // Allow retry 1739 container.removeClass('rated'); 1740 } 1741 }); 1742 }); 904 1743 });
Note: See TracChangeset
for help on using the changeset viewer.