Plugin Directory

Changeset 3415333


Ignore:
Timestamp:
12/09/2025 12:08:38 PM (2 months ago)
Author:
2wstechnologies
Message:

fix css in community page

Location:
askeet/assets
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • askeet/assets/css/style.css

    r3323653 r3415333  
    4545}
    4646
    47 .btn {
    48   color: white;
     47.wp-core-ui .button-group {
     48    font-size: unset !important;
     49}
     50
     51.btn,
     52a.btn,
     53.button-group a.btn,
     54.button-group .btn {
     55  color: #ffffff !important;
    4956  border: none;
    50   padding: 32px 34px;
     57  padding: 17px 34px;
    5158  cursor: pointer;
    5259  font-weight: 600;
     
    5461  text-align: center;
    5562  min-width: 160px;
    56   transition: background 0.3s ease;
     63  transition: all 0.3s ease;
    5764  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,
     74a.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,
     85a.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,
     101body.wp-admin #btn-discord,
     102body.wp-admin #btn-slack,
     103body.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;
    63117}
    64118
     
    528582    margin-bottom: 8px;
    529583}
     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  
    77    let lastPage = 1;
    88    let lastPerPage = 20;
     9    let currentPerPage = 20; // User-selected rows per page
     10    let lastTotalCount = 0; // Total results count
    911    let chatHistory = [];
    1012    let lastQueryRequest = '';
     
    1214    let lastQueryLastSql = '';
    1315    let lastQueryLastColumns = [];
     16    let executionRetryCount = 0; // Track retries for execution logging
     17    let currentQueryId = null; // Track query_id for database updates on retries
    1418
    1519    // Helper: scroll chat to bottom
     
    2226    function renderMessage(content, sender = 'ai', queryText = null, isError = false) {
    2327        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
    2431        if (sender === 'user' && queryText) {
    2532            // Add Save button for user queries
     
    3946        }
    4047        if (isError) {
     48            // Create a container for retry button and AI warning
     49            const retryContainer = $('<div class="wcqa-retry-container"></div>');
    4150            const retryBtn = $('<button class="wcqa-retry-query button">'+askeet_query_assistant_i18n.retry+'</button>');
    4251            retryBtn.on('click', function(e) {
     
    4453                retryLastQuery();
    4554            });
    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+'">&#9734;</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
    4881        $('#wcqa-messages').append(msg);
    4982        scrollChatToBottom();
     
    187220        lastQueryRequest = queryRequest;
    188221        lastQueryChatHistory = JSON.parse(JSON.stringify(chatHistory));
     222        currentQueryId = null; // Reset query_id for new query
     223        executionRetryCount = 0; // Reset execution retry count
    189224        renderMessage(null, 'user', queryRequest);
    190225        $('#query-request').val('');
     
    192227        $('#process-query').prop('disabled', true);
    193228        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
    194233        function tryProcessQuery() {
    195234            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
    196258            $.ajax({
    197259                url: askeet_query_assistant.ajax_url,
    198260                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,
    207262                success: function(response) {
    208                     // // console.log('[WCQA] AJAX success:', response); // Debug log
    209                     removeLoading();
    210                     $('#process-query').prop('disabled', false);
    211263                    // 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                    }
    213269                    let results = (response.data && response.data.results) ? response.data.results : (response.results ? response.results : []);
    214270                    let sql_query = (response.data && response.data.sql_query) ? response.data.sql_query : (response.sql_query ? response.sql_query : '');
    215271                    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
    216281                    // Only execute the paginated query, do NOT render SQL or button
    217282                    if (response.success && currentSqlQuery) {
    218                         // TODO: Hide this in production
     283                        removeLoading();
     284                        $('#process-query').prop('disabled', false);
    219285                        clearResultMessages();
    220286                        executeQueryPage(1, true);
    221287                    } else if (response.success) {
     288                        removeLoading();
     289                        $('#process-query').prop('disabled', false);
    222290                        clearResultMessages();
    223291                        renderMessage('<div class="query-success">'+askeet_query_assistant_i18n.query_executed_successfully+' 0 '+askeet_query_assistant_i18n.results_found+'.</div>', 'ai');
    224292                    } 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);
    229334                        } else {
    230                             errorMsg = askeet_query_assistant_i18n.ai_failed_3_times;
     335                            // Auto-retry silently in background with context
     336                            setTimeout(tryProcessQuery, 1000);
    231337                        }
    232                         renderMessage('<div class="query-error">'+errorMsg+'</div>', 'ai', null, true);
    233338                    }
    234339                },
    235340                error: function(xhr, status, error) {
    236                     removeLoading();
    237                     clearErrorMessages();
     341                    // Store error for retry context
     342                    lastErrorMessage = error || 'Network error';
     343
    238344                    try {
    239345                        var resp = xhr.responseJSON || JSON.parse(xhr.responseText);
    240                         // console.log('[WCQA] AJAX error:', resp); // Debug log
    241346                        // 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                    }
    247376                }
    248377            });
     
    265394        $('#process-query').prop('disabled', true);
    266395        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
    267400        function tryProcessQuery() {
    268401            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
    269417            $.ajax({
    270418                url: askeet_query_assistant.ajax_url,
    271419                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,
    278421                success: function(response) {
    279                     // console.log('[WCQA] AJAX success:', response); // Debug log
    280                     removeLoading();
    281                     $('#process-query').prop('disabled', false);
    282422                    // 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                    }
    284428                    let results = (response.data && response.data.results) ? response.data.results : (response.results ? response.results : []);
    285429                    let sql_query = (response.data && response.data.sql_query) ? response.data.sql_query : (response.sql_query ? response.sql_query : '');
    286430                    currentSqlQuery = sql_query || '';
    287                     if (response.success && results && results.length > 0) {
     431                    if (response.success && currentSqlQuery) {
     432                        removeLoading();
     433                        $('#process-query').prop('disabled', false);
    288434                        executeQueryPage(1, true);
    289435                    } else if (response.success) {
     436                        removeLoading();
     437                        $('#process-query').prop('disabled', false);
    290438                        renderMessage('<div class="query-success">'+askeet_query_assistant_i18n.query_executed_successfully+' 0 '+askeet_query_assistant_i18n.results_found+'.</div>', 'ai');
    291439                    } 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);
    296474                        } else {
    297                             errorMsg = askeet_query_assistant_i18n.ai_failed_3_times;
     475                            // Auto-retry silently in background with context
     476                            setTimeout(tryProcessQuery, 1000);
    298477                        }
    299                         renderMessage('<div class="query-error">'+errorMsg+'</div>', 'ai', null, true);
    300478                    }
    301479                },
    302480                error: function(xhr, status, error) {
    303                     removeLoading();
    304                     clearErrorMessages();
     481                    // Store error for retry context
     482                    lastErrorMessage = error || 'Network error';
     483
    305484                    try {
    306485                        var resp = xhr.responseJSON || JSON.parse(xhr.responseText);
    307                         // console.log('[WCQA] AJAX error:', resp); // Debug log
    308486                        // 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                    }
    314516                }
    315517            });
    316518        }
    317519        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();
    318630    }
    319631
     
    380692                sql_query: currentSqlQuery,
    381693                nonce: askeet_query_assistant.nonce,
    382                 page: pageNum
     694                page: pageNum,
     695                per_page: currentPerPage
    383696            },
    384697            success: function(response) {
    385698                // console.log('[WCQA] AJAX success:', response); // Debug log
    386699                removeLoading();
     700
     701                // Check for upgrade requirement
     702                if (handleLimitModals(response)) return;
     703
    387704                if (response.success) {
     705                    // Log successful execution
     706                    logExecutionResult(true, currentSqlQuery, '', executionRetryCount);
     707                    executionRetryCount = 0; // Reset retry count after success
     708
    388709                    lastResults = response.data.results;
    389710                    lastResultsHtml = response.data.html_results;
     
    396717                    lastPage = response.data.page;
    397718                    lastPerPage = response.data.per_page;
     719                    lastTotalCount = response.data.total_count;
    398720                    const totalPages = Math.max(1, Math.ceil(response.data.total_count / response.data.per_page));
    399721                    let html = '';
    400722                    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">&times;</button>';
     767                            html += '</div>';
     768                        }
     769
    401770                        // Show summary message
    402771                        const startIdx = (response.data.page - 1) * response.data.per_page + 1;
    403772                        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
    405776                        html += '<div class="results-block">' + renderResultsTable(response.data.results, response.data.total_count, response.data.page) + '</div>';
     777
    406778                        if (!response.data.disable_pagination) {
    407779                            html += renderPagination(response.data.total_count, response.data.page, response.data.per_page);
    408780                        }
    409                         html += '<button id="export-csv" class="button" style="margin-top:10px;">'+askeet_query_assistant_i18n.export_csv+'</button>';
    410781                    } else {
    411782                        html += '<div class="query-success">'+askeet_query_assistant_i18n.no_result_found+'</div>';
    412783                    }
    413784                    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                    }
    414809                }
    415810                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);
    420831                    } 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                    }
    424838                }
    425839            },
     
    427841                removeLoading();
    428842                clearErrorMessages();
     843                // Log network/AJAX error
     844                logExecutionResult(false, currentSqlQuery, 'Network error: ' + (error || status), executionRetryCount);
     845                executionRetryCount = 0; // Reset
     846
    429847                try {
    430848                    var resp = xhr.responseJSON || JSON.parse(xhr.responseText);
     
    453871        }
    454872    });
    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) {
    456884        e.preventDefault();
     885        if (currentSqlQuery === '') {
     886            alert(askeet_query_assistant_i18n.no_sql_to_execute);
     887            return;
     888        }
     889
    457890        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);
    470967            return;
    471968        }
    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        });
    5021052    });
    5031053
     
    8121362    // PATCH: Detect limit errors in AJAX responses and show only one modal
    8131363    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
    8141383        if (
    8151384            response &&
     
    9021471        });
    9031472    });
     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('&#9733;').addClass('hovered'); // Filled star
     1633            } else {
     1634                $(this).html('&#9734;').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('&#9734;').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('&#9733;').addClass('selected');
     1706            } else {
     1707                $(this).html('&#9734;').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    });
    9041743});
Note: See TracChangeset for help on using the changeset viewer.